@malloy-publisher/server 0.0.196 → 0.0.197

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/README.docker.md +88 -20
  2. package/README.md +15 -0
  3. package/build.ts +16 -0
  4. package/dist/app/api-doc.yaml +20 -3
  5. package/dist/app/assets/EnvironmentPage-BVkQH_xQ.js +1 -0
  6. package/dist/app/assets/HomePage-BgH9UkjK.js +1 -0
  7. package/dist/app/assets/MainPage-DiBxABem.js +2 -0
  8. package/dist/app/assets/ModelPage-oS70fj83.js +1 -0
  9. package/dist/app/assets/PackagePage-F_qLDAdv.js +1 -0
  10. package/dist/app/assets/RouteError-WqpffppN.js +1 -0
  11. package/dist/app/assets/WorkbookPage-_YmC-ebR.js +1 -0
  12. package/dist/app/assets/{core-w79IMXAG.es-Bd0UlzOL.js → core-B8L9xCYT.es-BcRLJTnC.js} +14 -14
  13. package/dist/app/assets/index-BMViiwtJ.js +451 -0
  14. package/dist/app/assets/{index-C513UodQ.js → index-C3XPaTaS.js} +15 -15
  15. package/dist/app/assets/index-rg8Ok8nl.js +1803 -0
  16. package/dist/app/assets/{index.umd-BMeMPq_9.js → index.umd-CCAfKkxY.js} +1 -1
  17. package/dist/app/index.html +2 -3
  18. package/dist/default-publisher.config.json +23 -0
  19. package/dist/instrumentation.mjs +1 -3
  20. package/dist/server.mjs +334 -165
  21. package/package.json +11 -12
  22. package/publisher.config.example.bigquery.json +33 -0
  23. package/publisher.config.example.duckdb.json +23 -0
  24. package/publisher.config.json +1 -11
  25. package/src/config.spec.ts +118 -0
  26. package/src/config.ts +78 -2
  27. package/src/controller/connection.controller.ts +1 -1
  28. package/src/default-publisher.config.json +23 -0
  29. package/src/errors.spec.ts +42 -0
  30. package/src/errors.ts +8 -0
  31. package/src/health.ts +26 -0
  32. package/src/logger.ts +1 -3
  33. package/src/pg_helpers.spec.ts +226 -0
  34. package/src/pg_helpers.ts +129 -0
  35. package/src/server.ts +20 -0
  36. package/src/service/connection.spec.ts +6 -4
  37. package/src/service/connection.ts +8 -3
  38. package/src/service/connection_config.ts +2 -2
  39. package/src/service/environment.ts +53 -25
  40. package/src/service/environment_store.spec.ts +19 -0
  41. package/src/service/environment_store.ts +21 -2
  42. package/src/service/package.ts +4 -3
  43. package/src/storage/StorageManager.ts +71 -11
  44. package/src/utils.ts +11 -0
  45. package/tests/unit/duckdb/attached_databases.test.ts +5 -5
  46. package/tests/unit/storage/StorageManager.test.ts +166 -0
  47. package/dist/app/assets/EnvironmentPage-1j6QDWAy.js +0 -1
  48. package/dist/app/assets/HomePage-DMop21VG.js +0 -1
  49. package/dist/app/assets/MainPage-BbE8ETz1.js +0 -2
  50. package/dist/app/assets/ModelPage-D2jvfe3t.js +0 -1
  51. package/dist/app/assets/PackagePage-BbnhGoD3.js +0 -1
  52. package/dist/app/assets/RouteError-D3LGEZ3i.js +0 -1
  53. package/dist/app/assets/WorkbookPage-DttVIj4u.js +0 -1
  54. package/dist/app/assets/index-5K9YjIxF.js +0 -456
  55. package/dist/app/assets/index-DIgzgp69.js +0 -1742
package/dist/server.mjs CHANGED
@@ -199378,14 +199378,14 @@ var require_brace_expansion = __commonJS((exports, module) => {
199378
199378
  var require_minimatch = __commonJS((exports, module) => {
199379
199379
  module.exports = minimatch;
199380
199380
  minimatch.Minimatch = Minimatch;
199381
- var path6 = function() {
199381
+ var path7 = function() {
199382
199382
  try {
199383
199383
  return __require("path");
199384
199384
  } catch (e) {}
199385
199385
  }() || {
199386
199386
  sep: "/"
199387
199387
  };
199388
- minimatch.sep = path6.sep;
199388
+ minimatch.sep = path7.sep;
199389
199389
  var GLOBSTAR = minimatch.GLOBSTAR = Minimatch.GLOBSTAR = {};
199390
199390
  var expand = require_brace_expansion();
199391
199391
  var plTypes = {
@@ -199476,8 +199476,8 @@ var require_minimatch = __commonJS((exports, module) => {
199476
199476
  if (!options)
199477
199477
  options = {};
199478
199478
  pattern = pattern.trim();
199479
- if (!options.allowWindowsEscape && path6.sep !== "/") {
199480
- pattern = pattern.split(path6.sep).join("/");
199479
+ if (!options.allowWindowsEscape && path7.sep !== "/") {
199480
+ pattern = pattern.split(path7.sep).join("/");
199481
199481
  }
199482
199482
  this.options = options;
199483
199483
  this.set = [];
@@ -199854,8 +199854,8 @@ var require_minimatch = __commonJS((exports, module) => {
199854
199854
  if (f === "/" && partial)
199855
199855
  return true;
199856
199856
  var options = this.options;
199857
- if (path6.sep !== "/") {
199858
- f = f.split(path6.sep).join("/");
199857
+ if (path7.sep !== "/") {
199858
+ f = f.split(path7.sep).join("/");
199859
199859
  }
199860
199860
  f = f.split(slashSplit);
199861
199861
  this.debug(this.pattern, "split", f);
@@ -199966,9 +199966,9 @@ var require_recursive_readdir = __commonJS((exports, module) => {
199966
199966
  var p = __require("path");
199967
199967
  var minimatch = require_minimatch();
199968
199968
  function patternMatcher(pattern) {
199969
- return function(path6, stats) {
199969
+ return function(path7, stats) {
199970
199970
  var minimatcher = new minimatch.Minimatch(pattern, { matchBase: true });
199971
- return (!minimatcher.negate || stats.isFile()) && minimatcher.match(path6);
199971
+ return (!minimatcher.negate || stats.isFile()) && minimatcher.match(path7);
199972
199972
  };
199973
199973
  }
199974
199974
  function toMatcherFunction(ignoreEntry) {
@@ -199978,14 +199978,14 @@ var require_recursive_readdir = __commonJS((exports, module) => {
199978
199978
  return patternMatcher(ignoreEntry);
199979
199979
  }
199980
199980
  }
199981
- function readdir3(path6, ignores, callback) {
199981
+ function readdir3(path7, ignores, callback) {
199982
199982
  if (typeof ignores == "function") {
199983
199983
  callback = ignores;
199984
199984
  ignores = [];
199985
199985
  }
199986
199986
  if (!callback) {
199987
199987
  return new Promise(function(resolve3, reject) {
199988
- readdir3(path6, ignores || [], function(err, data) {
199988
+ readdir3(path7, ignores || [], function(err, data) {
199989
199989
  if (err) {
199990
199990
  reject(err);
199991
199991
  } else {
@@ -199996,7 +199996,7 @@ var require_recursive_readdir = __commonJS((exports, module) => {
199996
199996
  }
199997
199997
  ignores = ignores.map(toMatcherFunction);
199998
199998
  var list = [];
199999
- fs4.readdir(path6, function(err, files) {
199999
+ fs4.readdir(path7, function(err, files) {
200000
200000
  if (err) {
200001
200001
  return callback(err);
200002
200002
  }
@@ -200005,7 +200005,7 @@ var require_recursive_readdir = __commonJS((exports, module) => {
200005
200005
  return callback(null, list);
200006
200006
  }
200007
200007
  files.forEach(function(file) {
200008
- var filePath = p.join(path6, file);
200008
+ var filePath = p.join(path7, file);
200009
200009
  fs4.stat(filePath, function(_err, stats) {
200010
200010
  if (_err) {
200011
200011
  return callback(_err);
@@ -200851,8 +200851,8 @@ var require_uri_all = __commonJS((exports, module) => {
200851
200851
  wsComponents.secure = undefined;
200852
200852
  }
200853
200853
  if (wsComponents.resourceName) {
200854
- var _wsComponents$resourc = wsComponents.resourceName.split("?"), _wsComponents$resourc2 = slicedToArray(_wsComponents$resourc, 2), path11 = _wsComponents$resourc2[0], query = _wsComponents$resourc2[1];
200855
- wsComponents.path = path11 && path11 !== "/" ? path11 : undefined;
200854
+ var _wsComponents$resourc = wsComponents.resourceName.split("?"), _wsComponents$resourc2 = slicedToArray(_wsComponents$resourc, 2), path12 = _wsComponents$resourc2[0], query = _wsComponents$resourc2[1];
200855
+ wsComponents.path = path12 && path12 !== "/" ? path12 : undefined;
200856
200856
  wsComponents.query = query;
200857
200857
  wsComponents.resourceName = undefined;
200858
200858
  }
@@ -201245,12 +201245,12 @@ var require_util13 = __commonJS((exports, module) => {
201245
201245
  return "'" + escapeQuotes(str) + "'";
201246
201246
  }
201247
201247
  function getPathExpr(currentPath, expr, jsonPointers, isNumber2) {
201248
- var path11 = jsonPointers ? "'/' + " + expr + (isNumber2 ? "" : ".replace(/~/g, '~0').replace(/\\//g, '~1')") : isNumber2 ? "'[' + " + expr + " + ']'" : "'[\\'' + " + expr + " + '\\']'";
201249
- return joinPaths(currentPath, path11);
201248
+ var path12 = jsonPointers ? "'/' + " + expr + (isNumber2 ? "" : ".replace(/~/g, '~0').replace(/\\//g, '~1')") : isNumber2 ? "'[' + " + expr + " + ']'" : "'[\\'' + " + expr + " + '\\']'";
201249
+ return joinPaths(currentPath, path12);
201250
201250
  }
201251
201251
  function getPath(currentPath, prop, jsonPointers) {
201252
- var path11 = jsonPointers ? toQuotedString("/" + escapeJsonPointer(prop)) : toQuotedString(getProperty(prop));
201253
- return joinPaths(currentPath, path11);
201252
+ var path12 = jsonPointers ? toQuotedString("/" + escapeJsonPointer(prop)) : toQuotedString(getProperty(prop));
201253
+ return joinPaths(currentPath, path12);
201254
201254
  }
201255
201255
  var JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/;
201256
201256
  var RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/;
@@ -207304,11 +207304,11 @@ var require_ast = __commonJS((exports, module) => {
207304
207304
  helperExpression: function helperExpression(node) {
207305
207305
  return node.type === "SubExpression" || (node.type === "MustacheStatement" || node.type === "BlockStatement") && !!(node.params && node.params.length || node.hash);
207306
207306
  },
207307
- scopedId: function scopedId(path11) {
207308
- return /^\.|this\b/.test(path11.original);
207307
+ scopedId: function scopedId(path12) {
207308
+ return /^\.|this\b/.test(path12.original);
207309
207309
  },
207310
- simpleId: function simpleId(path11) {
207311
- return path11.parts.length === 1 && !AST.helpers.scopedId(path11) && !path11.depth;
207310
+ simpleId: function simpleId(path12) {
207311
+ return path12.parts.length === 1 && !AST.helpers.scopedId(path12) && !path12.depth;
207312
207312
  }
207313
207313
  }
207314
207314
  };
@@ -208368,12 +208368,12 @@ var require_helpers3 = __commonJS((exports) => {
208368
208368
  loc
208369
208369
  };
208370
208370
  }
208371
- function prepareMustache(path11, params, hash, open2, strip, locInfo) {
208371
+ function prepareMustache(path12, params, hash, open2, strip, locInfo) {
208372
208372
  var escapeFlag = open2.charAt(3) || open2.charAt(2), escaped = escapeFlag !== "{" && escapeFlag !== "&";
208373
208373
  var decorator = /\*/.test(open2);
208374
208374
  return {
208375
208375
  type: decorator ? "Decorator" : "MustacheStatement",
208376
- path: path11,
208376
+ path: path12,
208377
208377
  params,
208378
208378
  hash,
208379
208379
  escaped,
@@ -208637,9 +208637,9 @@ var require_compiler = __commonJS((exports) => {
208637
208637
  },
208638
208638
  DecoratorBlock: function DecoratorBlock(decorator) {
208639
208639
  var program = decorator.program && this.compileProgram(decorator.program);
208640
- var params = this.setupFullMustacheParams(decorator, program, undefined), path11 = decorator.path;
208640
+ var params = this.setupFullMustacheParams(decorator, program, undefined), path12 = decorator.path;
208641
208641
  this.useDecorators = true;
208642
- this.opcode("registerDecorator", params.length, path11.original);
208642
+ this.opcode("registerDecorator", params.length, path12.original);
208643
208643
  },
208644
208644
  PartialStatement: function PartialStatement(partial) {
208645
208645
  this.usePartial = true;
@@ -208702,46 +208702,46 @@ var require_compiler = __commonJS((exports) => {
208702
208702
  }
208703
208703
  },
208704
208704
  ambiguousSexpr: function ambiguousSexpr(sexpr, program, inverse) {
208705
- var path11 = sexpr.path, name = path11.parts[0], isBlock = program != null || inverse != null;
208706
- this.opcode("getContext", path11.depth);
208705
+ var path12 = sexpr.path, name = path12.parts[0], isBlock = program != null || inverse != null;
208706
+ this.opcode("getContext", path12.depth);
208707
208707
  this.opcode("pushProgram", program);
208708
208708
  this.opcode("pushProgram", inverse);
208709
- path11.strict = true;
208710
- this.accept(path11);
208709
+ path12.strict = true;
208710
+ this.accept(path12);
208711
208711
  this.opcode("invokeAmbiguous", name, isBlock);
208712
208712
  },
208713
208713
  simpleSexpr: function simpleSexpr(sexpr) {
208714
- var path11 = sexpr.path;
208715
- path11.strict = true;
208716
- this.accept(path11);
208714
+ var path12 = sexpr.path;
208715
+ path12.strict = true;
208716
+ this.accept(path12);
208717
208717
  this.opcode("resolvePossibleLambda");
208718
208718
  },
208719
208719
  helperSexpr: function helperSexpr(sexpr, program, inverse) {
208720
- var params = this.setupFullMustacheParams(sexpr, program, inverse), path11 = sexpr.path, name = path11.parts[0];
208720
+ var params = this.setupFullMustacheParams(sexpr, program, inverse), path12 = sexpr.path, name = path12.parts[0];
208721
208721
  if (this.options.knownHelpers[name]) {
208722
208722
  this.opcode("invokeKnownHelper", params.length, name);
208723
208723
  } else if (this.options.knownHelpersOnly) {
208724
208724
  throw new _exception2["default"]("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr);
208725
208725
  } else {
208726
- path11.strict = true;
208727
- path11.falsy = true;
208728
- this.accept(path11);
208729
- this.opcode("invokeHelper", params.length, path11.original, _ast2["default"].helpers.simpleId(path11));
208726
+ path12.strict = true;
208727
+ path12.falsy = true;
208728
+ this.accept(path12);
208729
+ this.opcode("invokeHelper", params.length, path12.original, _ast2["default"].helpers.simpleId(path12));
208730
208730
  }
208731
208731
  },
208732
- PathExpression: function PathExpression(path11) {
208733
- this.addDepth(path11.depth);
208734
- this.opcode("getContext", path11.depth);
208735
- var name = path11.parts[0], scoped = _ast2["default"].helpers.scopedId(path11), blockParamId = !path11.depth && !scoped && this.blockParamIndex(name);
208732
+ PathExpression: function PathExpression(path12) {
208733
+ this.addDepth(path12.depth);
208734
+ this.opcode("getContext", path12.depth);
208735
+ var name = path12.parts[0], scoped = _ast2["default"].helpers.scopedId(path12), blockParamId = !path12.depth && !scoped && this.blockParamIndex(name);
208736
208736
  if (blockParamId) {
208737
- this.opcode("lookupBlockParam", blockParamId, path11.parts);
208737
+ this.opcode("lookupBlockParam", blockParamId, path12.parts);
208738
208738
  } else if (!name) {
208739
208739
  this.opcode("pushContext");
208740
- } else if (path11.data) {
208740
+ } else if (path12.data) {
208741
208741
  this.options.data = true;
208742
- this.opcode("lookupData", path11.depth, path11.parts, path11.strict);
208742
+ this.opcode("lookupData", path12.depth, path12.parts, path12.strict);
208743
208743
  } else {
208744
- this.opcode("lookupOnContext", path11.parts, path11.falsy, path11.strict, scoped);
208744
+ this.opcode("lookupOnContext", path12.parts, path12.falsy, path12.strict, scoped);
208745
208745
  }
208746
208746
  },
208747
208747
  StringLiteral: function StringLiteral(string2) {
@@ -209085,16 +209085,16 @@ var require_util14 = __commonJS((exports) => {
209085
209085
  }
209086
209086
  exports.urlGenerate = urlGenerate;
209087
209087
  function normalize2(aPath) {
209088
- var path11 = aPath;
209088
+ var path12 = aPath;
209089
209089
  var url2 = urlParse(aPath);
209090
209090
  if (url2) {
209091
209091
  if (!url2.path) {
209092
209092
  return aPath;
209093
209093
  }
209094
- path11 = url2.path;
209094
+ path12 = url2.path;
209095
209095
  }
209096
- var isAbsolute3 = exports.isAbsolute(path11);
209097
- var parts = path11.split(/\/+/);
209096
+ var isAbsolute3 = exports.isAbsolute(path12);
209097
+ var parts = path12.split(/\/+/);
209098
209098
  for (var part, up = 0, i = parts.length - 1;i >= 0; i--) {
209099
209099
  part = parts[i];
209100
209100
  if (part === ".") {
@@ -209111,15 +209111,15 @@ var require_util14 = __commonJS((exports) => {
209111
209111
  }
209112
209112
  }
209113
209113
  }
209114
- path11 = parts.join("/");
209115
- if (path11 === "") {
209116
- path11 = isAbsolute3 ? "/" : ".";
209114
+ path12 = parts.join("/");
209115
+ if (path12 === "") {
209116
+ path12 = isAbsolute3 ? "/" : ".";
209117
209117
  }
209118
209118
  if (url2) {
209119
- url2.path = path11;
209119
+ url2.path = path12;
209120
209120
  return urlGenerate(url2);
209121
209121
  }
209122
- return path11;
209122
+ return path12;
209123
209123
  }
209124
209124
  exports.normalize = normalize2;
209125
209125
  function join9(aRoot, aPath) {
@@ -211676,8 +211676,8 @@ var require_printer = __commonJS((exports) => {
211676
211676
  return this.accept(sexpr.path) + " " + params + hash;
211677
211677
  };
211678
211678
  PrintVisitor.prototype.PathExpression = function(id) {
211679
- var path11 = id.parts.join("/");
211680
- return (id.data ? "@" : "") + "PATH:" + path11;
211679
+ var path12 = id.parts.join("/");
211680
+ return (id.data ? "@" : "") + "PATH:" + path12;
211681
211681
  };
211682
211682
  PrintVisitor.prototype.StringLiteral = function(string2) {
211683
211683
  return '"' + string2.value + '"';
@@ -211754,9 +211754,7 @@ var getLogLevel = () => {
211754
211754
  };
211755
211755
  var logger = import_winston.default.createLogger({
211756
211756
  level: getLogLevel(),
211757
- format: isTelemetryEnabled ? import_winston.default.format.combine(import_winston.default.format.uncolorize(), import_winston.default.format.timestamp(), import_winston.default.format.metadata({
211758
- fillExcept: ["message", "level", "timestamp"]
211759
- }), import_winston.default.format.json()) : import_winston.default.format.combine(import_winston.default.format.colorize(), import_winston.default.format.simple()),
211757
+ format: isTelemetryEnabled ? import_winston.default.format.combine(import_winston.default.format.uncolorize(), import_winston.default.format.timestamp(), import_winston.default.format.errors({ stack: true }), import_winston.default.format.json()) : import_winston.default.format.combine(import_winston.default.format.colorize(), import_winston.default.format.simple()),
211760
211758
  transports: [new import_winston.default.transports.Console]
211761
211759
  });
211762
211760
  function extractTraceIdFromTraceparent(traceparent) {
@@ -216946,8 +216944,8 @@ var import_cors = __toESM(require_lib7(), 1);
216946
216944
  var import_express = __toESM(require_express(), 1);
216947
216945
  var import_http_proxy_middleware = __toESM(require_dist4(), 1);
216948
216946
  import * as http2 from "http";
216949
- import * as path11 from "path";
216950
- import { fileURLToPath as fileURLToPath2 } from "url";
216947
+ import * as path12 from "path";
216948
+ import { fileURLToPath as fileURLToPath3 } from "url";
216951
216949
 
216952
216950
  // src/controller/compile.controller.ts
216953
216951
  class CompileController {
@@ -216998,6 +216996,8 @@ function internalErrorToHttpError(error) {
216998
216996
  return httpError(400, error.message);
216999
216997
  } else if (error instanceof ConnectionNotFoundError) {
217000
216998
  return httpError(404, error.message);
216999
+ } else if (error instanceof ConnectionAuthError) {
217000
+ return httpError(422, error.message);
217001
217001
  } else if (error instanceof ModelCompilationError) {
217002
217002
  return httpError(424, error.message);
217003
217003
  } else if (error instanceof ConnectionError) {
@@ -217064,6 +217064,12 @@ class ConnectionError extends Error {
217064
217064
  }
217065
217065
  }
217066
217066
 
217067
+ class ConnectionAuthError extends Error {
217068
+ constructor(message) {
217069
+ super(message);
217070
+ }
217071
+ }
217072
+
217067
217073
  class ModelCompilationError extends Error {
217068
217074
  constructor(error) {
217069
217075
  super(error.message);
@@ -220380,6 +220386,63 @@ var {
220380
220386
  import fs from "fs/promises";
220381
220387
  import path2 from "path";
220382
220388
 
220389
+ // src/pg_helpers.ts
220390
+ function pgConnectTimeoutSeconds() {
220391
+ const raw = process.env.PG_CONNECT_TIMEOUT_SECONDS;
220392
+ if (!raw)
220393
+ return 5;
220394
+ const parsed = Number.parseInt(raw, 10);
220395
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 5;
220396
+ }
220397
+ var URI_FORM_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
220398
+ var HAS_CONNECT_TIMEOUT_RE = /[?&\s]connect_timeout=|^connect_timeout=/;
220399
+ function withPgConnectTimeout(connectionString, timeout) {
220400
+ if (HAS_CONNECT_TIMEOUT_RE.test(connectionString)) {
220401
+ return connectionString;
220402
+ }
220403
+ if (URI_FORM_RE.test(connectionString)) {
220404
+ if (!connectionString.includes("?")) {
220405
+ return `${connectionString}?connect_timeout=${timeout}`;
220406
+ }
220407
+ if (connectionString.endsWith("?")) {
220408
+ return `${connectionString}connect_timeout=${timeout}`;
220409
+ }
220410
+ return `${connectionString}&connect_timeout=${timeout}`;
220411
+ }
220412
+ return `${connectionString} connect_timeout=${timeout}`;
220413
+ }
220414
+ function redactPgSecrets(s) {
220415
+ return s.replace(/password=('[^']*'|"[^"]*"|\S+)/gi, "password=***");
220416
+ }
220417
+ function classifyPgError(error, context) {
220418
+ if (!(error instanceof Error))
220419
+ return;
220420
+ const msg = error.message;
220421
+ const patterns = [
220422
+ /password authentication failed/i,
220423
+ /pg_hba\.conf/i,
220424
+ /role ".*" does not exist/i,
220425
+ /database ".*" does not exist/i,
220426
+ /permission denied/i
220427
+ ];
220428
+ if (!patterns.some((p) => p.test(msg)))
220429
+ return;
220430
+ return new ConnectionAuthError(`${context}: ${redactPgSecrets(msg)}`);
220431
+ }
220432
+ function handlePgAttachError(error, context) {
220433
+ if (error instanceof Error && (error.message.includes("already exists") || error.message.includes("already attached"))) {
220434
+ return { action: "swallow" };
220435
+ }
220436
+ const authErr = classifyPgError(error, context);
220437
+ if (authErr) {
220438
+ return { action: "throw", error: authErr };
220439
+ }
220440
+ if (error instanceof Error) {
220441
+ return { action: "throw", error };
220442
+ }
220443
+ return { action: "throw", error: new Error(String(error)) };
220444
+ }
220445
+
220383
220446
  // src/service/connection_config.ts
220384
220447
  import { createPrivateKey } from "crypto";
220385
220448
  import path from "path";
@@ -220563,7 +220626,7 @@ function validateConnectionShape(connection) {
220563
220626
  {
220564
220627
  const attached = connection.duckdbConnection.attachedDatabases ?? [];
220565
220628
  if (attached.length === 0) {
220566
- throw new Error("DuckDB connection must have at least one attached database");
220629
+ throw new Error(`DuckDB connection "${connection.name}" has no attached databases. Add at least one foreign database (BigQuery, Snowflake, Postgres, GCS, S3, Azure) to attachedDatabases, or remove this connection entirely — each package already gets a per-package DuckDB sandbox named "duckdb" automatically.`);
220567
220630
  }
220568
220631
  }
220569
220632
  break;
@@ -220636,7 +220699,7 @@ function assembleEnvironmentConnections(connections = [], environmentPath = "")
220636
220699
  continue;
220637
220700
  }
220638
220701
  if (connection.name === "duckdb") {
220639
- throw new Error("DuckDB connection name cannot be 'duckdb'; it is reserved for Publisher package sandboxes.");
220702
+ throw new Error("Connection name 'duckdb' is reserved for per-package sandboxes. Choose a different name for environment-level DuckDB connections (e.g. 'shared_duckdb').");
220640
220703
  }
220641
220704
  processedConnections.add(connection.name);
220642
220705
  validateDuckdbApiSurface(connection);
@@ -221005,13 +221068,13 @@ async function attachDuckLake(connection, dbName, ducklakeConfig) {
221005
221068
  }
221006
221069
  const pg = ducklakeConfig.catalog.postgresConnection;
221007
221070
  const pgConnString = buildPgConnectionString(pg);
221008
- logger.info(`pgConnString: ${pgConnString}`);
221071
+ logger.info(`pgConnString: ${redactPgSecrets(pgConnString)}`);
221009
221072
  const escapedPgConnString = escapeSQL(pgConnString);
221010
- logger.info(`Final escaped connection string: ${escapedPgConnString}`);
221073
+ logger.info(`Final escaped connection string: ${redactPgSecrets(escapedPgConnString)}`);
221011
221074
  const escapedBucketUrl = escapeSQL(ducklakeConfig.storage.bucketUrl);
221012
221075
  logger.info(`escapedBucketUrl: ${escapedBucketUrl}`);
221013
221076
  const attachCommand = `ATTACH OR REPLACE 'ducklake:postgres:${escapedPgConnString}' AS ${dbName} (DATA_PATH '${escapedBucketUrl}', OVERRIDE_DATA_PATH true, READ_ONLY true);`;
221014
- logger.info(`Attaching DuckLake database using command: ${attachCommand}`);
221077
+ logger.info(`Attaching DuckLake database using command: ${redactPgSecrets(attachCommand)}`);
221015
221078
  try {
221016
221079
  await connection.runSQL(attachCommand);
221017
221080
  logger.info(`Successfully attached DuckLake database in READ_ONLY mode: ${dbName}`);
@@ -222783,7 +222846,7 @@ function validateAzureAttachedDatabases(connectionConfig) {
222783
222846
  }
222784
222847
  function validateAdminAuthoredConnection(connectionName, connectionConfig) {
222785
222848
  if (connectionName === "duckdb" || connectionConfig.name === "duckdb") {
222786
- throw new BadRequestError("DuckDB connection name cannot be 'duckdb'; it is reserved for Publisher package sandboxes.");
222849
+ throw new BadRequestError("Connection name 'duckdb' is reserved for per-package sandboxes. Choose a different name for environment-level DuckDB connections (e.g. 'shared_duckdb').");
222787
222850
  }
222788
222851
  try {
222789
222852
  validateDuckdbApiSurface(connectionConfig);
@@ -224801,7 +224864,7 @@ function watch(paths, options = {}) {
224801
224864
  var esm_default = { watch, FSWatcher };
224802
224865
 
224803
224866
  // src/controller/watch-mode.controller.ts
224804
- import path10 from "path";
224867
+ import path11 from "path";
224805
224868
 
224806
224869
  // src/service/environment_store.ts
224807
224870
  var import_client_s32 = __toESM(require_dist_cjs75(), 1);
@@ -225019,7 +225082,7 @@ class Mutex {
225019
225082
  // src/service/environment_store.ts
225020
225083
  import crypto4 from "crypto";
225021
225084
  import * as fs7 from "fs";
225022
- import * as path9 from "path";
225085
+ import * as path10 from "path";
225023
225086
 
225024
225087
  // ../../node_modules/simple-git/dist/esm/index.js
225025
225088
  var import_file_exists = __toESM(require_dist11(), 1);
@@ -229007,6 +229070,25 @@ import { Writable } from "stream";
229007
229070
  // src/config.ts
229008
229071
  import fs2 from "fs";
229009
229072
  import path4 from "path";
229073
+ import { fileURLToPath } from "url";
229074
+ var BUNDLED_DEFAULT_CONFIG_PATH = path4.join(path4.dirname(fileURLToPath(import.meta.url)), "default-publisher.config.json");
229075
+ function resolvePublisherConfigPath(serverRoot) {
229076
+ const explicitPath = process.env.PUBLISHER_CONFIG_PATH;
229077
+ if (explicitPath && explicitPath.length > 0) {
229078
+ if (!fs2.existsSync(explicitPath)) {
229079
+ return null;
229080
+ }
229081
+ return { path: explicitPath, isBundledDefault: false };
229082
+ }
229083
+ const serverRootPath = path4.join(serverRoot, PUBLISHER_CONFIG_NAME);
229084
+ if (fs2.existsSync(serverRootPath)) {
229085
+ return { path: serverRootPath, isBundledDefault: false };
229086
+ }
229087
+ if (process.env.PUBLISHER_USE_BUNDLED_DEFAULT === "true" && fs2.existsSync(BUNDLED_DEFAULT_CONFIG_PATH)) {
229088
+ return { path: BUNDLED_DEFAULT_CONFIG_PATH, isBundledDefault: true };
229089
+ }
229090
+ return null;
229091
+ }
229010
229092
  function substituteEnvVars(value) {
229011
229093
  const envVarPattern = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
229012
229094
  return value.replace(envVarPattern, (_match, varName) => {
@@ -229034,13 +229116,20 @@ function processConfigValue(value) {
229034
229116
  return value;
229035
229117
  }
229036
229118
  var getPublisherConfig = (serverRoot) => {
229037
- const publisherConfigPath = path4.join(serverRoot, PUBLISHER_CONFIG_NAME);
229038
- if (!fs2.existsSync(publisherConfigPath)) {
229119
+ const resolved = resolvePublisherConfigPath(serverRoot);
229120
+ if (!resolved) {
229121
+ if (process.env.PUBLISHER_CONFIG_PATH && process.env.PUBLISHER_CONFIG_PATH.length > 0) {
229122
+ logger.error(`--config path not found: ${process.env.PUBLISHER_CONFIG_PATH}. Using default empty config.`);
229123
+ }
229039
229124
  return {
229040
229125
  frozenConfig: false,
229041
229126
  environments: []
229042
229127
  };
229043
229128
  }
229129
+ const publisherConfigPath = resolved.path;
229130
+ if (resolved.isBundledDefault) {
229131
+ logger.info(`No publisher.config.json found at ${path4.join(serverRoot, PUBLISHER_CONFIG_NAME)}; falling back to bundled DuckDB-only default. Pass --config <path> or place a config in the server root to override.`);
229132
+ }
229044
229133
  let rawConfig;
229045
229134
  try {
229046
229135
  const fileContent = fs2.readFileSync(publisherConfigPath, "utf8");
@@ -229185,6 +229274,15 @@ function markReady() {
229185
229274
  logger.error("Service is already draining - cannot mark as ready");
229186
229275
  }
229187
229276
  }
229277
+ function markDegraded() {
229278
+ if (operationalState !== "draining") {
229279
+ operationalState = "degraded";
229280
+ ready = false;
229281
+ logger.warn("Service marked as degraded; one or more environments failed to initialize. Readiness probe will fail until the config is fixed and the process restarts.");
229282
+ } else {
229283
+ logger.error("Service is already draining - cannot mark as degraded");
229284
+ }
229285
+ }
229188
229286
  function markNotReady() {
229189
229287
  ready = false;
229190
229288
  logger.info("Service marked as not ready - readiness probe will fail");
@@ -230293,6 +230391,7 @@ class StorageManager {
230293
230391
  defaultManifestStore = null;
230294
230392
  environmentManifestStores = new Map;
230295
230393
  attachedCatalogs = new Map;
230394
+ duckLakeAttachMutex = new Mutex;
230296
230395
  config;
230297
230396
  constructor(config) {
230298
230397
  this.config = config;
@@ -230330,12 +230429,15 @@ class StorageManager {
230330
230429
  throw new Error("Storage not initialized. Call initialize() first.");
230331
230430
  }
230332
230431
  const key = configKey(config);
230333
- let catalogName = this.attachedCatalogs.get(key);
230334
- if (!catalogName) {
230335
- catalogName = catalogNameForConfig(config);
230336
- await this.attachDuckLakeCatalog(config, catalogName);
230337
- this.attachedCatalogs.set(key, catalogName);
230338
- }
230432
+ const catalogName = await this.duckLakeAttachMutex.runExclusive(async () => {
230433
+ const existing = this.attachedCatalogs.get(key);
230434
+ if (existing)
230435
+ return existing;
230436
+ const name = catalogNameForConfig(config);
230437
+ await this.attachDuckLakeCatalog(config, name);
230438
+ this.attachedCatalogs.set(key, name);
230439
+ return name;
230440
+ });
230339
230441
  const store = new DuckLakeManifestStore(this.duckDbConnection, catalogName, environmentName);
230340
230442
  await store.bootstrapSchema();
230341
230443
  this.environmentManifestStores.set(environmentId, store);
@@ -230352,9 +230454,13 @@ class StorageManager {
230352
230454
  if (isPostgres) {
230353
230455
  await connection.run("INSTALL postgres; LOAD postgres;");
230354
230456
  }
230355
- const escapedCatalogUrl = escapeSQL2(config.catalogUrl);
230457
+ const catalogUrl = isPostgres ? withPgConnectTimeout(config.catalogUrl, pgConnectTimeoutSeconds()) : config.catalogUrl;
230458
+ const escapedCatalogUrl = escapeSQL2(catalogUrl);
230356
230459
  const escapedDataPath = escapeSQL2(config.dataPath);
230357
230460
  const isCloudStorage = config.dataPath.startsWith("gs://") || config.dataPath.startsWith("s3://");
230461
+ if (isCloudStorage) {
230462
+ await connection.run("INSTALL httpfs; LOAD httpfs;");
230463
+ }
230358
230464
  let attachCmd = `ATTACH 'ducklake:${escapedCatalogUrl}' AS ${catalogName}`;
230359
230465
  const attachOpts = [
230360
230466
  `DATA_PATH '${escapedDataPath}'`,
@@ -230364,8 +230470,22 @@ class StorageManager {
230364
230470
  attachOpts.push("OVERRIDE_DATA_PATH true");
230365
230471
  }
230366
230472
  attachCmd += ` (${attachOpts.join(", ")});`;
230367
- logger.info(`Attaching DuckLake manifest catalog: ${attachCmd}`);
230368
- await connection.run(attachCmd);
230473
+ logger.info(`Attaching DuckLake manifest catalog: ${redactPgSecrets(attachCmd)}`);
230474
+ try {
230475
+ await connection.run(attachCmd);
230476
+ } catch (error) {
230477
+ const outcome = handlePgAttachError(error, `DuckLake catalog credentials rejected for ${catalogName}`);
230478
+ if (outcome.action === "swallow") {
230479
+ logger.info(`DuckLake catalog ${catalogName} is already attached, skipping`);
230480
+ return;
230481
+ }
230482
+ if (outcome.error instanceof ConnectionAuthError) {
230483
+ logger.warn("DuckLake catalog credentials rejected", {
230484
+ catalogName
230485
+ });
230486
+ }
230487
+ throw outcome.error;
230488
+ }
230369
230489
  }
230370
230490
  getRepository() {
230371
230491
  if (!this.repository) {
@@ -230404,26 +230524,30 @@ class StorageManager {
230404
230524
  // src/service/environment.ts
230405
230525
  import { MalloyError as MalloyError3, Runtime as Runtime2 } from "@malloydata/malloy";
230406
230526
  import * as fs6 from "fs";
230407
- import * as path8 from "path";
230527
+ import * as path9 from "path";
230408
230528
 
230409
230529
  // src/utils.ts
230410
230530
  import * as fs3 from "fs";
230411
- import { fileURLToPath } from "url";
230531
+ import * as path6 from "path";
230532
+ import { fileURLToPath as fileURLToPath2 } from "url";
230412
230533
  var URL_READER = {
230413
230534
  readURL: (url2) => {
230414
- let path6 = url2.toString();
230535
+ let path7 = url2.toString();
230415
230536
  if (url2.protocol == "file:") {
230416
- path6 = fileURLToPath(url2);
230537
+ path7 = fileURLToPath2(url2);
230417
230538
  }
230418
- return fs3.promises.readFile(path6, "utf8");
230539
+ return fs3.promises.readFile(path7, "utf8");
230419
230540
  }
230420
230541
  };
230542
+ function ignoreDotfiles(file) {
230543
+ return path6.basename(file).startsWith(".");
230544
+ }
230421
230545
 
230422
230546
  // src/service/package.ts
230423
230547
  var import_api3 = __toESM(require_src(), 1);
230424
230548
  var import_recursive_readdir = __toESM(require_recursive_readdir(), 1);
230425
230549
  import * as fs5 from "fs/promises";
230426
- import * as path7 from "path";
230550
+ import * as path8 from "path";
230427
230551
  import { DuckDBConnection as DuckDBConnection3 } from "@malloydata/db-duckdb";
230428
230552
  import"@malloydata/db-duckdb/native";
230429
230553
  import {
@@ -230451,7 +230575,7 @@ import {
230451
230575
  } from "@malloydata/malloy-sql";
230452
230576
  import * as fs4 from "fs/promises";
230453
230577
  import { createRequire as createRequire2 } from "module";
230454
- import * as path6 from "path";
230578
+ import * as path7 from "path";
230455
230579
 
230456
230580
  // src/data_styles.ts
230457
230581
  function compileDataStyles(styles) {
@@ -231062,7 +231186,7 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
231062
231186
  };
231063
231187
  }
231064
231188
  static async getModelRuntime(packagePath, modelPath, malloyConfig, options) {
231065
- const fullModelPath = path6.join(packagePath, modelPath);
231189
+ const fullModelPath = path7.join(packagePath, modelPath);
231066
231190
  try {
231067
231191
  if (!(await fs4.stat(fullModelPath)).isFile()) {
231068
231192
  throw new ModelNotFoundError(`${modelPath} is not a file.`);
@@ -231275,7 +231399,7 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
231275
231399
  return this.modelType;
231276
231400
  }
231277
231401
  async getFileText(packagePath) {
231278
- const fullPath = path6.join(packagePath, this.modelPath);
231402
+ const fullPath = path7.join(packagePath, this.modelPath);
231279
231403
  try {
231280
231404
  return await fs4.readFile(fullPath, "utf8");
231281
231405
  } catch {
@@ -231510,17 +231634,17 @@ class Package {
231510
231634
  static async getModelPaths(packagePath) {
231511
231635
  let files = undefined;
231512
231636
  try {
231513
- files = await import_recursive_readdir.default(packagePath);
231637
+ files = await import_recursive_readdir.default(packagePath, [ignoreDotfiles]);
231514
231638
  } catch (error) {
231515
231639
  logger.error(error);
231516
231640
  throw new PackageNotFoundError(`Package config for ${packagePath} does not exist.`);
231517
231641
  }
231518
231642
  return files.map((fullPath) => {
231519
- return path7.relative(packagePath, fullPath).replace(/\\/g, "/");
231643
+ return path8.relative(packagePath, fullPath).replace(/\\/g, "/");
231520
231644
  }).filter((modelPath) => modelPath.endsWith(MODEL_FILE_SUFFIX) || modelPath.endsWith(NOTEBOOK_FILE_SUFFIX));
231521
231645
  }
231522
231646
  static async validatePackageManifestExistsOrThrowError(packagePath) {
231523
- const packageConfigPath = path7.join(packagePath, PACKAGE_MANIFEST_NAME);
231647
+ const packageConfigPath = path8.join(packagePath, PACKAGE_MANIFEST_NAME);
231524
231648
  try {
231525
231649
  await fs5.stat(packageConfigPath);
231526
231650
  } catch {
@@ -231529,7 +231653,7 @@ class Package {
231529
231653
  }
231530
231654
  }
231531
231655
  static async readPackageConfig(packagePath) {
231532
- const packageConfigPath = path7.join(packagePath, PACKAGE_MANIFEST_NAME);
231656
+ const packageConfigPath = path8.join(packagePath, PACKAGE_MANIFEST_NAME);
231533
231657
  const packageConfigContents = await fs5.readFile(packageConfigPath);
231534
231658
  const packageManifest = JSON.parse(packageConfigContents.toString());
231535
231659
  return {
@@ -231548,14 +231672,13 @@ class Package {
231548
231672
  }));
231549
231673
  }
231550
231674
  static async getDatabasePaths(packagePath) {
231551
- let files = undefined;
231552
- files = await import_recursive_readdir.default(packagePath);
231675
+ const files = await import_recursive_readdir.default(packagePath, [ignoreDotfiles]);
231553
231676
  return files.map((fullPath) => {
231554
- return path7.relative(packagePath, fullPath).replace(/\\/g, "/");
231677
+ return path8.relative(packagePath, fullPath).replace(/\\/g, "/");
231555
231678
  }).filter((modelPath) => modelPath.endsWith(".parquet") || modelPath.endsWith(".csv"));
231556
231679
  }
231557
231680
  static async getDatabaseInfo(packagePath, databasePath) {
231558
- const fullPath = path7.join(packagePath, databasePath);
231681
+ const fullPath = path8.join(packagePath, databasePath);
231559
231682
  const runtime = new ConnectionRuntime({
231560
231683
  urlReader: new EmptyURLReader,
231561
231684
  connections: [new DuckDBConnection3("duckdb")]
@@ -231612,7 +231735,7 @@ class Environment {
231612
231735
  async writeEnvironmentReadme(readme) {
231613
231736
  if (readme === undefined)
231614
231737
  return;
231615
- const readmePath = path8.join(this.environmentPath, "README.md");
231738
+ const readmePath = path9.join(this.environmentPath, "README.md");
231616
231739
  try {
231617
231740
  await fs6.promises.writeFile(readmePath, readme, "utf-8");
231618
231741
  logger.info(`Updated README.md for environment ${this.environmentName}`);
@@ -231659,7 +231782,7 @@ class Environment {
231659
231782
  async reloadEnvironmentMetadata() {
231660
231783
  let readme = "";
231661
231784
  try {
231662
- readme = (await fs6.promises.readFile(path8.join(this.environmentPath, README_NAME))).toString();
231785
+ readme = (await fs6.promises.readFile(path9.join(this.environmentPath, README_NAME))).toString();
231663
231786
  } catch {}
231664
231787
  this.metadata = {
231665
231788
  ...this.metadata,
@@ -231670,10 +231793,10 @@ class Environment {
231670
231793
  return this.metadata;
231671
231794
  }
231672
231795
  async compileSource(packageName, modelName, source, includeSql = false) {
231673
- const modelDir = path8.dirname(path8.join(this.environmentPath, packageName, modelName));
231674
- const virtualUri = `file://${path8.join(modelDir, "__compile_check.malloy")}`;
231796
+ const modelDir = path9.dirname(path9.join(this.environmentPath, packageName, modelName));
231797
+ const virtualUri = `file://${path9.join(modelDir, "__compile_check.malloy")}`;
231675
231798
  const virtualUrl = new URL(virtualUri);
231676
- const modelPath = path8.join(this.environmentPath, packageName, modelName);
231799
+ const modelPath = path9.join(this.environmentPath, packageName, modelName);
231677
231800
  let modelContent = "";
231678
231801
  try {
231679
231802
  modelContent = await fs6.promises.readFile(modelPath, "utf8");
@@ -231785,24 +231908,30 @@ ${source}` : source;
231785
231908
  throw error;
231786
231909
  }
231787
231910
  }
231911
+ getOrCreatePackageMutex(packageName) {
231912
+ let packageMutex = this.packageMutexes.get(packageName);
231913
+ if (packageMutex === undefined) {
231914
+ packageMutex = new Mutex;
231915
+ this.packageMutexes.set(packageName, packageMutex);
231916
+ }
231917
+ return packageMutex;
231918
+ }
231788
231919
  async getPackage(packageName, reload = false) {
231789
231920
  const _package = this.packages.get(packageName);
231790
231921
  if (_package !== undefined && !reload) {
231791
231922
  return _package;
231792
231923
  }
231793
- let packageMutex = this.packageMutexes.get(packageName);
231794
- if (packageMutex?.isLocked()) {
231924
+ const packageMutex = this.getOrCreatePackageMutex(packageName);
231925
+ if (packageMutex.isLocked()) {
231795
231926
  logger.debug(`Package ${packageName} is being loaded, waiting for unlock...`);
231796
231927
  await packageMutex.waitForUnlock();
231797
231928
  logger.debug(`Package ${packageName} unlocked`);
231798
231929
  const existingPackage = this.packages.get(packageName);
231799
- if (existingPackage) {
231930
+ if (existingPackage !== undefined && !reload) {
231800
231931
  logger.debug(`Package ${packageName} loaded by another request`);
231801
231932
  return existingPackage;
231802
231933
  }
231803
231934
  }
231804
- packageMutex = new Mutex;
231805
- this.packageMutexes.set(packageName, packageMutex);
231806
231935
  return packageMutex.runExclusive(async () => {
231807
231936
  const existingPackage = this.packages.get(packageName);
231808
231937
  if (existingPackage !== undefined && !reload) {
@@ -231811,7 +231940,7 @@ ${source}` : source;
231811
231940
  this.setPackageStatus(packageName, "loading" /* LOADING */);
231812
231941
  try {
231813
231942
  logger.debug(`Loading package ${packageName}...`);
231814
- const packagePath = path8.join(this.environmentPath, packageName);
231943
+ const packagePath = path9.join(this.environmentPath, packageName);
231815
231944
  const _package2 = await Package.create(this.environmentName, packageName, packagePath, () => this.malloyConfig.malloyConfig);
231816
231945
  if (existingPackage !== undefined && reload) {
231817
231946
  this.retireConnectionGeneration(`package ${packageName}`, () => existingPackage.getMalloyConfig().releaseConnections());
@@ -231829,7 +231958,7 @@ ${source}` : source;
231829
231958
  });
231830
231959
  }
231831
231960
  async addPackage(packageName) {
231832
- const packagePath = path8.join(this.environmentPath, packageName);
231961
+ const packagePath = path9.join(this.environmentPath, packageName);
231833
231962
  if (!await fs6.promises.access(packagePath).then(() => true).catch(() => false) || !(await fs6.promises.stat(packagePath))?.isDirectory()) {
231834
231963
  throw new PackageNotFoundError(`Package ${packageName} not found`);
231835
231964
  }
@@ -231837,20 +231966,35 @@ ${source}` : source;
231837
231966
  packagePath,
231838
231967
  malloyConfig: this.malloyConfig.malloyConfig
231839
231968
  });
231840
- this.setPackageStatus(packageName, "loading" /* LOADING */);
231841
- try {
231842
- this.packages.set(packageName, await Package.create(this.environmentName, packageName, packagePath, () => this.malloyConfig.malloyConfig));
231843
- } catch (error) {
231844
- logger.error("Error adding package", { error });
231845
- this.deletePackageStatus(packageName);
231846
- throw error;
231969
+ const packageMutex = this.getOrCreatePackageMutex(packageName);
231970
+ if (packageMutex.isLocked()) {
231971
+ logger.debug(`Package ${packageName} is being loaded, waiting before addPackage...`);
231972
+ await packageMutex.waitForUnlock();
231973
+ const alreadyLoaded = this.packages.get(packageName);
231974
+ if (alreadyLoaded !== undefined) {
231975
+ return alreadyLoaded;
231976
+ }
231847
231977
  }
231848
- this.setPackageStatus(packageName, "serving" /* SERVING */);
231849
- return this.packages.get(packageName);
231978
+ return packageMutex.runExclusive(async () => {
231979
+ const existingPackage = this.packages.get(packageName);
231980
+ if (existingPackage !== undefined) {
231981
+ return existingPackage;
231982
+ }
231983
+ this.setPackageStatus(packageName, "loading" /* LOADING */);
231984
+ try {
231985
+ this.packages.set(packageName, await Package.create(this.environmentName, packageName, packagePath, () => this.malloyConfig.malloyConfig));
231986
+ } catch (error) {
231987
+ logger.error("Error adding package", { error });
231988
+ this.deletePackageStatus(packageName);
231989
+ throw error;
231990
+ }
231991
+ this.setPackageStatus(packageName, "serving" /* SERVING */);
231992
+ return this.packages.get(packageName);
231993
+ });
231850
231994
  }
231851
231995
  async writePackageManifest(packageName, metadata) {
231852
- const packagePath = path8.join(this.environmentPath, packageName);
231853
- const manifestPath = path8.join(packagePath, "publisher.json");
231996
+ const packagePath = path9.join(this.environmentPath, packageName);
231997
+ const manifestPath = path9.join(packagePath, "publisher.json");
231854
231998
  try {
231855
231999
  let existingManifest = {};
231856
232000
  try {
@@ -231922,7 +232066,7 @@ ${source}` : source;
231922
232066
  }
231923
232067
  await _package.getMalloyConfig().releaseConnections();
231924
232068
  try {
231925
- await fs6.promises.rm(path8.join(this.environmentPath, packageName), {
232069
+ await fs6.promises.rm(path9.join(this.environmentPath, packageName), {
231926
232070
  recursive: true,
231927
232071
  force: true
231928
232072
  });
@@ -231986,7 +232130,7 @@ ${source}` : source;
231986
232130
  };
231987
232131
  }
231988
232132
  async deleteDuckDBConnection(connectionName) {
231989
- const duckdbPath = path8.join(this.environmentPath, `${connectionName}.duckdb`);
232133
+ const duckdbPath = path9.join(this.environmentPath, `${connectionName}.duckdb`);
231990
232134
  try {
231991
232135
  await fs6.promises.rm(duckdbPath, { force: true });
231992
232136
  logger.info(`Removed DuckDB connection file ${connectionName} from environment ${this.environmentName}`);
@@ -232055,6 +232199,7 @@ class EnvironmentStore {
232055
232199
  publisherConfigIsFrozen;
232056
232200
  finishedInitialization;
232057
232201
  isInitialized = false;
232202
+ failedEnvironments = [];
232058
232203
  storageManager;
232059
232204
  s3Client = new import_client_s32.S3({
232060
232205
  followRegionRedirects: true
@@ -232066,7 +232211,7 @@ class EnvironmentStore {
232066
232211
  const storageConfig = {
232067
232212
  type: "duckdb",
232068
232213
  duckdb: {
232069
- path: path9.join(serverRootPath, "publisher.db")
232214
+ path: path10.join(serverRootPath, "publisher.db")
232070
232215
  }
232071
232216
  };
232072
232217
  this.storageManager = new StorageManager(storageConfig);
@@ -232087,6 +232232,10 @@ class EnvironmentStore {
232087
232232
  logEnvironmentInitializationError(environmentName, error) {
232088
232233
  const label = environmentName ? ` "${environmentName}"` : "";
232089
232234
  logger.error(`Error initializing environment${label}; skipping environment`, this.extractErrorDataFromError(error));
232235
+ this.failedEnvironments.push({
232236
+ name: environmentName ?? "<unknown>",
232237
+ error: error instanceof Error ? error.message : String(error)
232238
+ });
232090
232239
  }
232091
232240
  async initialize() {
232092
232241
  const reInit = process.env.INITIALIZE_STORAGE === "true";
@@ -232149,7 +232298,11 @@ class EnvironmentStore {
232149
232298
  }
232150
232299
  }
232151
232300
  this.isInitialized = true;
232152
- markReady();
232301
+ if (this.failedEnvironments.length > 0) {
232302
+ markDegraded();
232303
+ } else {
232304
+ markReady();
232305
+ }
232153
232306
  const initializationDuration = performance.now() - initialTime;
232154
232307
  logger.info(`Environment store successfully initialized in ${formatDuration(initializationDuration)}`);
232155
232308
  } catch (error) {
@@ -232353,7 +232506,7 @@ class EnvironmentStore {
232353
232506
  const reInit = process.env.INITIALIZE_STORAGE === "true";
232354
232507
  await fs7.promises.mkdir(this.serverRootPath, { recursive: true });
232355
232508
  if (reInit) {
232356
- const uploadDocsPath2 = path9.join(this.serverRootPath, PUBLISHER_DATA_DIR);
232509
+ const uploadDocsPath2 = path10.join(this.serverRootPath, PUBLISHER_DATA_DIR);
232357
232510
  logger.info(`Reinitialization mode: Cleaning up upload documents path ${uploadDocsPath2}`);
232358
232511
  try {
232359
232512
  await fs7.promises.rm(uploadDocsPath2, {
@@ -232370,7 +232523,7 @@ class EnvironmentStore {
232370
232523
  } else {
232371
232524
  logger.info(`Using existing publisher path`);
232372
232525
  }
232373
- const uploadDocsPath = path9.join(this.serverRootPath, PUBLISHER_DATA_DIR);
232526
+ const uploadDocsPath = path10.join(this.serverRootPath, PUBLISHER_DATA_DIR);
232374
232527
  await fs7.promises.mkdir(uploadDocsPath, { recursive: true });
232375
232528
  }
232376
232529
  async listEnvironments(skipInitializationCheck = false) {
@@ -232385,7 +232538,12 @@ class EnvironmentStore {
232385
232538
  environments: [],
232386
232539
  initialized: this.isInitialized,
232387
232540
  frozenConfig: isPublisherConfigFrozen(this.serverRootPath),
232388
- operationalState: getOperationalState()
232541
+ operationalState: getOperationalState(),
232542
+ ...this.failedEnvironments.length > 0 && {
232543
+ failedEnvironments: [
232544
+ ...this.failedEnvironments
232545
+ ]
232546
+ }
232389
232547
  };
232390
232548
  const environments = await this.listEnvironments(true);
232391
232549
  await Promise.all(environments.map(async (environment) => {
@@ -232602,12 +232760,12 @@ class EnvironmentStore {
232602
232760
  const absoluteEnvironmentPath = `${this.serverRootPath}/${PUBLISHER_DATA_DIR}/${environmentName}`;
232603
232761
  await fs7.promises.mkdir(absoluteEnvironmentPath, { recursive: true });
232604
232762
  if (environment.readme) {
232605
- await fs7.promises.writeFile(path9.join(absoluteEnvironmentPath, "README.md"), environment.readme);
232763
+ await fs7.promises.writeFile(path10.join(absoluteEnvironmentPath, "README.md"), environment.readme);
232606
232764
  }
232607
232765
  return absoluteEnvironmentPath;
232608
232766
  }
232609
232767
  isLocalPath(location) {
232610
- return location.startsWith("./") || location.startsWith("../") || location.startsWith("~/") || location.startsWith("/") || path9.isAbsolute(location);
232768
+ return location.startsWith("./") || location.startsWith("../") || location.startsWith("~/") || location.startsWith("/") || path10.isAbsolute(location);
232611
232769
  }
232612
232770
  isGitHubURL(location) {
232613
232771
  return location.startsWith("https://github.com/") || location.startsWith("git@github.com:");
@@ -232663,7 +232821,7 @@ class EnvironmentStore {
232663
232821
  if (githubInfo && githubInfo.packagePath) {
232664
232822
  const subPathMatch = _package.location.match(/\/tree\/[^/]+\/(.+)$/);
232665
232823
  if (subPathMatch) {
232666
- sourcePath = path9.join(tempDownloadPath, subPathMatch[1]);
232824
+ sourcePath = path10.join(tempDownloadPath, subPathMatch[1]);
232667
232825
  } else {
232668
232826
  sourcePath = tempDownloadPath;
232669
232827
  }
@@ -232674,7 +232832,7 @@ class EnvironmentStore {
232674
232832
  if (this.isLocalPath(_package.location)) {
232675
232833
  sourcePath = _package.location;
232676
232834
  } else {
232677
- sourcePath = path9.join(tempDownloadPath, groupedLocation);
232835
+ sourcePath = path10.join(tempDownloadPath, groupedLocation);
232678
232836
  }
232679
232837
  }
232680
232838
  const sourceExists = await fs7.promises.access(sourcePath).then(() => true).catch(() => false);
@@ -232750,7 +232908,7 @@ class EnvironmentStore {
232750
232908
  }
232751
232909
  }
232752
232910
  if (this.isLocalPath(location)) {
232753
- const packagePath = path9.isAbsolute(location) ? location : path9.join(this.serverRootPath, location);
232911
+ const packagePath = path10.isAbsolute(location) ? location : path10.join(this.serverRootPath, location);
232754
232912
  try {
232755
232913
  logger.info(`Mounting local directory at "${packagePath}" to "${targetPath}"`);
232756
232914
  await this.mountLocalDirectory(packagePath, targetPath, environmentName, packageName);
@@ -232804,11 +232962,11 @@ class EnvironmentStore {
232804
232962
  }
232805
232963
  await Promise.all(files.map(async (file) => {
232806
232964
  const relativeFilePath = file.name.replace(prefix, "");
232807
- const absoluteFilePath = isCompressedFile ? absoluteDirPath : path9.join(absoluteDirPath, relativeFilePath);
232965
+ const absoluteFilePath = isCompressedFile ? absoluteDirPath : path10.join(absoluteDirPath, relativeFilePath);
232808
232966
  if (file.name.endsWith("/")) {
232809
232967
  return;
232810
232968
  }
232811
- await fs7.promises.mkdir(path9.dirname(absoluteFilePath), {
232969
+ await fs7.promises.mkdir(path10.dirname(absoluteFilePath), {
232812
232970
  recursive: true
232813
232971
  });
232814
232972
  return fs7.promises.writeFile(absoluteFilePath, await file.download());
@@ -232824,7 +232982,7 @@ class EnvironmentStore {
232824
232982
  const prefix = prefixParts.join("/");
232825
232983
  if (isCompressedFile) {
232826
232984
  const zipFilePath = `${absoluteDirPath}.zip`;
232827
- await fs7.promises.mkdir(path9.dirname(zipFilePath), {
232985
+ await fs7.promises.mkdir(path10.dirname(zipFilePath), {
232828
232986
  recursive: true
232829
232987
  });
232830
232988
  const command = new import_client_s32.GetObjectCommand({
@@ -232863,8 +233021,8 @@ class EnvironmentStore {
232863
233021
  if (!relativeFilePath || relativeFilePath.endsWith("/")) {
232864
233022
  return;
232865
233023
  }
232866
- const absoluteFilePath = path9.join(absoluteDirPath, relativeFilePath);
232867
- await fs7.promises.mkdir(path9.dirname(absoluteFilePath), {
233024
+ const absoluteFilePath = path10.join(absoluteDirPath, relativeFilePath);
233025
+ await fs7.promises.mkdir(path10.dirname(absoluteFilePath), {
232868
233026
  recursive: true
232869
233027
  });
232870
233028
  const command = new import_client_s32.GetObjectCommand({
@@ -232926,7 +233084,7 @@ class EnvironmentStore {
232926
233084
  logger.info(`Successfully cloned entire repository to: ${absoluteDirPath}`);
232927
233085
  return;
232928
233086
  }
232929
- const packageFullPath = path9.join(absoluteDirPath, cleanPackagePath);
233087
+ const packageFullPath = path10.join(absoluteDirPath, cleanPackagePath);
232930
233088
  const packageExists = await fs7.promises.access(packageFullPath).then(() => true).catch(() => false);
232931
233089
  if (!packageExists) {
232932
233090
  throw new Error(`Package path "${cleanPackagePath}" does not exist in the cloned repository.`);
@@ -232934,7 +233092,7 @@ class EnvironmentStore {
232934
233092
  const dirContents = await fs7.promises.readdir(absoluteDirPath);
232935
233093
  for (const entry of dirContents) {
232936
233094
  if (entry !== cleanPackagePath.replace(/^\/+/, "").split("/")[0]) {
232937
- await fs7.promises.rm(path9.join(absoluteDirPath, entry), {
233095
+ await fs7.promises.rm(path10.join(absoluteDirPath, entry), {
232938
233096
  recursive: true,
232939
233097
  force: true
232940
233098
  });
@@ -232942,7 +233100,7 @@ class EnvironmentStore {
232942
233100
  }
232943
233101
  const packageContents = await fs7.promises.readdir(packageFullPath);
232944
233102
  for (const entry of packageContents) {
232945
- await fs7.promises.rename(path9.join(packageFullPath, entry), path9.join(absoluteDirPath, entry));
233103
+ await fs7.promises.rename(path10.join(packageFullPath, entry), path10.join(absoluteDirPath, entry));
232946
233104
  }
232947
233105
  await fs7.promises.rm(packageFullPath, { recursive: true, force: true });
232948
233106
  }
@@ -232990,9 +233148,9 @@ class WatchModeController {
232990
233148
  });
232991
233149
  return;
232992
233150
  }
232993
- this.watchingPath = path10.join(this.environmentStore.serverRootPath, watchName);
233151
+ this.watchingPath = path11.join(this.environmentStore.serverRootPath, watchName);
232994
233152
  this.watcher = esm_default.watch(this.watchingPath, {
232995
- ignored: (path11, stats) => !!stats?.isFile() && !path11.endsWith(".malloy") && !path11.endsWith(".md"),
233153
+ ignored: (path12, stats) => !!stats?.isFile() && !path12.endsWith(".malloy") && !path12.endsWith(".md"),
232996
233154
  ignoreInitial: true
232997
233155
  });
232998
233156
  const reloadEnvironment = async () => {
@@ -233000,16 +233158,16 @@ class WatchModeController {
233000
233158
  await this.environmentStore.addEnvironment(environment2.metadata);
233001
233159
  logger.info(`Reloaded environment ${watchName}`);
233002
233160
  };
233003
- this.watcher.on("add", async (path11) => {
233004
- logger.info(`Detected new file ${path11}, reloading environment ${watchName}`);
233161
+ this.watcher.on("add", async (path12) => {
233162
+ logger.info(`Detected new file ${path12}, reloading environment ${watchName}`);
233005
233163
  await reloadEnvironment();
233006
233164
  });
233007
- this.watcher.on("unlink", async (path11) => {
233008
- logger.info(`Detected deletion of ${path11}, reloading environment ${watchName}`);
233165
+ this.watcher.on("unlink", async (path12) => {
233166
+ logger.info(`Detected deletion of ${path12}, reloading environment ${watchName}`);
233009
233167
  await reloadEnvironment();
233010
233168
  });
233011
- this.watcher.on("change", async (path11) => {
233012
- logger.info(`Detected change on ${path11}, reloading environment ${watchName}`);
233169
+ this.watcher.on("change", async (path12) => {
233170
+ logger.info(`Detected change on ${path12}, reloading environment ${watchName}`);
233013
233171
  await reloadEnvironment();
233014
233172
  });
233015
233173
  res.json();
@@ -236100,25 +236258,25 @@ async function getModelForQuery(environmentStore, environmentName, packageName,
236100
236258
  }
236101
236259
  }
236102
236260
  function buildMalloyUri(components, fragment) {
236103
- let path11 = "/environment/";
236261
+ let path12 = "/environment/";
236104
236262
  if (components.environment) {
236105
- path11 += encodeURIComponent(components.environment);
236263
+ path12 += encodeURIComponent(components.environment);
236106
236264
  } else {
236107
- path11 += "home";
236265
+ path12 += "home";
236108
236266
  }
236109
236267
  if (components.package) {
236110
- path11 += "/package/" + encodeURIComponent(components.package);
236268
+ path12 += "/package/" + encodeURIComponent(components.package);
236111
236269
  }
236112
236270
  if (components.resourceType) {
236113
- path11 += "/" + components.resourceType;
236271
+ path12 += "/" + components.resourceType;
236114
236272
  if (components.resourceName) {
236115
- path11 += "/" + encodeURIComponent(components.resourceName);
236273
+ path12 += "/" + encodeURIComponent(components.resourceName);
236116
236274
  if (components.subResourceType && components.subResourceName) {
236117
- path11 += "/" + components.subResourceType + "/" + encodeURIComponent(components.subResourceName);
236275
+ path12 += "/" + components.subResourceType + "/" + encodeURIComponent(components.subResourceName);
236118
236276
  }
236119
236277
  }
236120
236278
  }
236121
- let uriString = "malloy:/" + path11;
236279
+ let uriString = "malloy:/" + path12;
236122
236280
  if (fragment) {
236123
236281
  uriString += "#" + fragment;
236124
236282
  }
@@ -237637,8 +237795,8 @@ import {
237637
237795
  } from "@malloydata/malloy";
237638
237796
 
237639
237797
  // src/service/quoting.ts
237640
- function quoteTablePath(path11, dialect) {
237641
- return path11.split(".").map((seg) => dialect.quoteTablePath(seg)).join(".");
237798
+ function quoteTablePath(path12, dialect) {
237799
+ return path12.split(".").map((seg) => dialect.quoteTablePath(seg)).join(".");
237642
237800
  }
237643
237801
  function splitTablePath(tableName) {
237644
237802
  const lastDot = tableName.lastIndexOf(".");
@@ -238236,6 +238394,8 @@ function normalizeQueryArray(value) {
238236
238394
  }
238237
238395
  function parseArgs() {
238238
238396
  const args = process.argv.slice(2);
238397
+ let sawServerRoot = false;
238398
+ let sawConfig = false;
238239
238399
  for (let i = 0;i < args.length; i++) {
238240
238400
  const arg = args[i];
238241
238401
  if (arg === "--port" && args[i + 1]) {
@@ -238245,8 +238405,13 @@ function parseArgs() {
238245
238405
  process.env.PUBLISHER_HOST = args[i + 1];
238246
238406
  i++;
238247
238407
  } else if (arg === "--server_root" && args[i + 1]) {
238408
+ sawServerRoot = true;
238248
238409
  process.env.SERVER_ROOT = args[i + 1];
238249
238410
  i++;
238411
+ } else if (arg === "--config" && args[i + 1]) {
238412
+ sawConfig = true;
238413
+ process.env.PUBLISHER_CONFIG_PATH = args[i + 1];
238414
+ i++;
238250
238415
  } else if (arg === "--mcp_port" && args[i + 1]) {
238251
238416
  process.env.MCP_PORT = args[i + 1];
238252
238417
  i++;
@@ -238267,6 +238432,7 @@ function parseArgs() {
238267
238432
  console.log(" --port <number> Port to run the server on (default: 4000)");
238268
238433
  console.log(" --host <string> Host to bind the server to (default: localhost)");
238269
238434
  console.log(" --server_root <path> Root directory to serve files from (default: .)");
238435
+ console.log(" --config <path> Path to publisher.config.json (default: <server_root>/publisher.config.json; falls back to bundled DuckDB-only sample config if missing)");
238270
238436
  console.log(" --mcp_port <number> Port for MCP server (default: 4040)");
238271
238437
  console.log(" --shutdown_drain_duration_seconds <number> Time in seconds to keep service in draining state before closing servers (default: 0)");
238272
238438
  console.log(" --shutdown_graceful_close_timeout_seconds <number> Time in seconds to wait after closing servers before exit (default: 0)");
@@ -238275,6 +238441,9 @@ function parseArgs() {
238275
238441
  process.exit(0);
238276
238442
  }
238277
238443
  }
238444
+ if (!sawServerRoot && !sawConfig && true) {
238445
+ process.env.PUBLISHER_USE_BUNDLED_DEFAULT = "true";
238446
+ }
238278
238447
  }
238279
238448
  parseArgs();
238280
238449
  var PUBLISHER_PORT = Number(process.env.PUBLISHER_PORT || 4000);
@@ -238283,9 +238452,9 @@ var MCP_PORT = Number(process.env.MCP_PORT || 4040);
238283
238452
  var MCP_ENDPOINT = "/mcp";
238284
238453
  var SHUTDOWN_DRAIN_DURATION_SECONDS = Number(process.env.SHUTDOWN_DRAIN_DURATION_SECONDS || 0);
238285
238454
  var SHUTDOWN_GRACEFUL_CLOSE_TIMEOUT_SECONDS = Number(process.env.SHUTDOWN_GRACEFUL_CLOSE_TIMEOUT_SECONDS || 0);
238286
- var __filename_esm = fileURLToPath2(import.meta.url);
238287
- var ROOT = path11.join(path11.dirname(__filename_esm), "app");
238288
- var SERVER_ROOT = path11.resolve(process.cwd(), process.env.SERVER_ROOT || ".");
238455
+ var __filename_esm = fileURLToPath3(import.meta.url);
238456
+ var ROOT = path12.join(path12.dirname(__filename_esm), "app");
238457
+ var SERVER_ROOT = path12.resolve(process.cwd(), process.env.SERVER_ROOT || ".");
238289
238458
  var API_PREFIX2 = "/api/v0";
238290
238459
  var isDevelopment = process.env["NODE_ENV"] === "development";
238291
238460
  var app = import_express.default();
@@ -238366,14 +238535,14 @@ mcpApp.all(MCP_ENDPOINT, async (req, res) => {
238366
238535
  });
238367
238536
  if (!isDevelopment) {
238368
238537
  app.use("/", import_express.default.static(ROOT));
238369
- app.use("/api-doc.html", import_express.default.static(path11.join(ROOT, "api-doc.html")));
238538
+ app.use("/api-doc.html", import_express.default.static(path12.join(ROOT, "api-doc.html")));
238370
238539
  } else {
238371
238540
  app.use(`${API_PREFIX2}`, loggerMiddleware);
238372
238541
  app.use(import_http_proxy_middleware.createProxyMiddleware({
238373
238542
  target: "http://localhost:5173",
238374
238543
  changeOrigin: true,
238375
238544
  ws: true,
238376
- pathFilter: (path12) => !path12.startsWith("/api/") && !path12.startsWith("/metrics") && !path12.startsWith("/health")
238545
+ pathFilter: (path13) => !path13.startsWith("/api/") && !path13.startsWith("/metrics") && !path13.startsWith("/health")
238377
238546
  }));
238378
238547
  }
238379
238548
  var setVersionIdError2 = (res) => {
@@ -238973,7 +239142,7 @@ registerLegacyRoutes(app, {
238973
239142
  manifestController
238974
239143
  });
238975
239144
  if (!isDevelopment) {
238976
- app.get("*", (_req, res) => res.sendFile(path11.resolve(ROOT, "index.html")));
239145
+ app.get("*", (_req, res) => res.sendFile(path12.resolve(ROOT, "index.html")));
238977
239146
  }
238978
239147
  app.use((err, _req, res, _next) => {
238979
239148
  logger.error("Unhandled error:", err);