@malloy-publisher/server 0.0.229 → 0.0.230

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 (51) hide show
  1. package/dist/app/api-doc.yaml +18 -9
  2. package/dist/app/assets/{EnvironmentPage-QOoHiVeJ.js → EnvironmentPage-wa_EPkwK.js} +1 -1
  3. package/dist/app/assets/{HomePage-C71GOfVW.js → HomePage-jnCrupQp.js} +1 -1
  4. package/dist/app/assets/{LightMode-CrgCAwLe.js → LightMode-DYbwNULZ.js} +1 -1
  5. package/dist/app/assets/{MainPage-BG5__FN3.js → MainPage-CuJLrPNI.js} +1 -1
  6. package/dist/app/assets/{MaterializationsPage-DE6PnrDR.js → MaterializationsPage-D_67x2ee.js} +1 -1
  7. package/dist/app/assets/{ModelPage-CcBjcbLm.js → ModelPage-D5JtAWqR.js} +1 -1
  8. package/dist/app/assets/{PackagePage-JTy3ztkB.js → PackagePage-BRwtqUSG.js} +1 -1
  9. package/dist/app/assets/{RouteError-Cymbp47a.js → RouteError-CBNNrnSD.js} +1 -1
  10. package/dist/app/assets/{ThemeEditorPage-C_nMnHr8.js → ThemeEditorPage-CTCeBneA.js} +1 -1
  11. package/dist/app/assets/{WorkbookPage-CPQu-DQx.js → WorkbookPage-SN6f1RBm.js} +1 -1
  12. package/dist/app/assets/{core-Coi3caGs.es-CSOmajHS.js → core-Dp3q5Ieu.es-CD5FvM2s.js} +1 -1
  13. package/dist/app/assets/{index-DlWCXghy.js → index-B3Nn8Vm2.js} +459 -446
  14. package/dist/app/assets/index-BLCx1EdC.js +18 -0
  15. package/dist/app/assets/{index-DxArlgRD.js → index-C_tJstcx.js} +4 -4
  16. package/dist/app/assets/{index-CcuuST2X.js → index-CfmBVB6M.js} +1 -1
  17. package/dist/app/assets/{index-CkmABCAw.js → index-DU4r7GdU.js} +435 -422
  18. package/dist/app/index.html +1 -1
  19. package/dist/package_load_worker.mjs +108 -7
  20. package/dist/server.mjs +575 -49
  21. package/package.json +12 -12
  22. package/scripts/bake-duckdb-extensions.js +5 -2
  23. package/src/config.ts +40 -0
  24. package/src/ducklake_version.spec.ts +163 -0
  25. package/src/ducklake_version.ts +153 -0
  26. package/src/errors.ts +12 -0
  27. package/src/malloy_pin_prereqs.spec.ts +25 -0
  28. package/src/mcp/skills/skills_bundle.json +1 -1
  29. package/src/server.ts +7 -0
  30. package/src/service/authorize.spec.ts +22 -0
  31. package/src/service/authorize.ts +267 -10
  32. package/src/service/authorize_integration.spec.ts +1068 -26
  33. package/src/service/connection.spec.ts +71 -0
  34. package/src/service/connection.ts +312 -25
  35. package/src/service/connection_config.spec.ts +21 -0
  36. package/src/service/connection_config.ts +15 -1
  37. package/src/service/ducklake_lazy_attach.spec.ts +110 -0
  38. package/src/service/environment.ts +43 -9
  39. package/src/service/environment_store.spec.ts +197 -12
  40. package/src/service/environment_store.ts +88 -14
  41. package/src/service/extension_fetch_policy.spec.ts +256 -0
  42. package/src/service/materialization_scheduler.spec.ts +29 -0
  43. package/src/service/materialization_service.spec.ts +119 -1
  44. package/src/service/model.spec.ts +67 -0
  45. package/src/service/model.ts +656 -31
  46. package/src/service/package.spec.ts +38 -0
  47. package/src/service/package.ts +12 -1
  48. package/src/storage/duckdb/MaterializationRepository.spec.ts +39 -2
  49. package/src/storage/duckdb/MaterializationRepository.ts +12 -0
  50. package/tests/integration/materialization/scheduler_transitions.integration.spec.ts +256 -0
  51. package/dist/app/assets/index-CM2qhQCI.js +0 -18
package/dist/server.mjs CHANGED
@@ -147403,6 +147403,8 @@ function internalErrorToHttpError(error) {
147403
147403
  return httpError(404, error.message);
147404
147404
  } else if (error instanceof ConnectionAuthError) {
147405
147405
  return httpError(422, error.message);
147406
+ } else if (error instanceof UnsupportedCatalogFormatError) {
147407
+ return httpError(422, error.message);
147406
147408
  } else if (error instanceof ModelCompilationError) {
147407
147409
  return httpError(424, error.message);
147408
147410
  } else if (error instanceof ConnectionError) {
@@ -147432,7 +147434,7 @@ function httpError(code, message) {
147432
147434
  }
147433
147435
  };
147434
147436
  }
147435
- var NotImplementedError, BadRequestError, EnvironmentNotFoundError, PackageNotFoundError, ModelNotFoundError, ConnectionNotFoundError, ConnectionError, ConnectionAuthError, ModelCompilationError, FrozenConfigError, AccessDeniedError, NotQueryableError, MaterializationNotFoundError, MaterializationConflictError, InvalidStateTransitionError, ServiceUnavailableError, PayloadTooLargeError, QueryTimeoutError;
147437
+ var NotImplementedError, BadRequestError, EnvironmentNotFoundError, PackageNotFoundError, ModelNotFoundError, ConnectionNotFoundError, ConnectionError, ConnectionAuthError, UnsupportedCatalogFormatError, ModelCompilationError, FrozenConfigError, AccessDeniedError, NotQueryableError, MaterializationNotFoundError, MaterializationConflictError, InvalidStateTransitionError, ServiceUnavailableError, PayloadTooLargeError, QueryTimeoutError;
147436
147438
  var init_errors = __esm(() => {
147437
147439
  init_constants();
147438
147440
  NotImplementedError = class NotImplementedError extends Error {
@@ -147475,6 +147477,11 @@ var init_errors = __esm(() => {
147475
147477
  super(message);
147476
147478
  }
147477
147479
  };
147480
+ UnsupportedCatalogFormatError = class UnsupportedCatalogFormatError extends Error {
147481
+ constructor(message) {
147482
+ super(message);
147483
+ }
147484
+ };
147478
147485
  ModelCompilationError = class ModelCompilationError extends Error {
147479
147486
  constructor(error) {
147480
147487
  super(error.message);
@@ -244084,6 +244091,16 @@ var getMaxConcurrentQueries = () => {
244084
244091
  }
244085
244092
  return raw;
244086
244093
  };
244094
+ var getExtensionFetchPolicy = () => {
244095
+ const raw = process.env.EXTENSION_FETCH_POLICY;
244096
+ if (raw === undefined || raw.trim() === "")
244097
+ return "on-demand";
244098
+ const normalised = raw.trim().toLowerCase();
244099
+ if (normalised === "on-demand" || normalised === "local-only") {
244100
+ return normalised;
244101
+ }
244102
+ throw new Error(`Invalid value for EXTENSION_FETCH_POLICY: expected "on-demand" or "local-only", got "${raw}"`);
244103
+ };
244087
244104
  function substituteEnvVars(value) {
244088
244105
  const envVarPattern = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
244089
244106
  return value.replace(envVarPattern, (_match, varName) => {
@@ -247757,9 +247774,68 @@ var {
247757
247774
  } = axios_default;
247758
247775
 
247759
247776
  // src/service/connection.ts
247760
- init_logger();
247761
247777
  import fs2 from "fs/promises";
247762
247778
 
247779
+ // src/ducklake_version.ts
247780
+ var MIN_CATALOG_FORMAT = "1.0";
247781
+ var ENGINE_MAX_FORMAT = [{ engine: { major: 1, minor: 5 }, maxFormat: "1.0" }];
247782
+ var CATALOG_FORMAT_RE = /^(\d+)\.(\d+)(?:-([0-9A-Za-z.]+))?$/;
247783
+ function parseCatalogFormat(raw) {
247784
+ if (typeof raw !== "string")
247785
+ return null;
247786
+ const m = CATALOG_FORMAT_RE.exec(raw);
247787
+ if (!m)
247788
+ return null;
247789
+ const format = { major: Number(m[1]), minor: Number(m[2]) };
247790
+ if (m[3] !== undefined)
247791
+ format.prerelease = m[3];
247792
+ return format;
247793
+ }
247794
+ function compareCatalogFormat(a, b) {
247795
+ if (a.major !== b.major)
247796
+ return a.major < b.major ? -1 : 1;
247797
+ if (a.minor !== b.minor)
247798
+ return a.minor < b.minor ? -1 : 1;
247799
+ if (a.prerelease === b.prerelease)
247800
+ return 0;
247801
+ if (a.prerelease === undefined)
247802
+ return 1;
247803
+ if (b.prerelease === undefined)
247804
+ return -1;
247805
+ return a.prerelease < b.prerelease ? -1 : 1;
247806
+ }
247807
+ function parseEngineMajorMinor(raw) {
247808
+ const m = /(\d+)\.(\d+)\.(\d+)/.exec(String(raw));
247809
+ if (!m)
247810
+ return null;
247811
+ return { major: Number(m[1]), minor: Number(m[2]) };
247812
+ }
247813
+ function catalogFormatRangeForEngine(engineVersion) {
247814
+ const e = parseEngineMajorMinor(engineVersion);
247815
+ if (!e)
247816
+ return null;
247817
+ const entry = ENGINE_MAX_FORMAT.find((r) => r.engine.major === e.major && r.engine.minor === e.minor);
247818
+ if (!entry)
247819
+ return null;
247820
+ return {
247821
+ min: MIN_CATALOG_FORMAT,
247822
+ max: entry.maxFormat,
247823
+ engineVersion: `${e.major}.${e.minor}`
247824
+ };
247825
+ }
247826
+ function isCatalogFormatInRange(format, range) {
247827
+ const f = parseCatalogFormat(format);
247828
+ const min = parseCatalogFormat(range.min);
247829
+ const max = parseCatalogFormat(range.max);
247830
+ if (!f || !min || !max)
247831
+ return false;
247832
+ return compareCatalogFormat(f, min) >= 0 && compareCatalogFormat(f, max) <= 0;
247833
+ }
247834
+
247835
+ // src/service/connection.ts
247836
+ init_errors();
247837
+ init_logger();
247838
+
247763
247839
  // src/pg_helpers.ts
247764
247840
  function redactPgSecrets(s) {
247765
247841
  return s.replace(/password=('[^']*'|"[^"]*"|\S+)/gi, "password=***");
@@ -247951,7 +248027,12 @@ function openSshProxy(ssh, target) {
247951
248027
  }
247952
248028
 
247953
248029
  // src/service/connection_config.ts
247954
- var PROXIED_SSLMODES = ["disable", "no-verify", "verify-ca"];
248030
+ var PROXIED_SSLMODES = [
248031
+ "disable",
248032
+ "no-verify",
248033
+ "verify-ca",
248034
+ "verify-full"
248035
+ ];
247955
248036
  var PUBLISHER_DUCKDB_API_FIELDS = new Set(["attachedDatabases"]);
247956
248037
  function normalizeSnowflakePrivateKey(privateKey) {
247957
248038
  let privateKeyContent = privateKey.trim();
@@ -248436,16 +248517,55 @@ function validateAndBuildTrinoCoreConfig(trinoConfig) {
248436
248517
  }
248437
248518
 
248438
248519
  // src/service/connection.ts
248520
+ var extensionSessionPinned = new WeakSet;
248521
+ async function applyExtensionSessionSettings(connection, {
248522
+ alwaysDisableAutoinstall = false
248523
+ } = {}) {
248524
+ const policy = getExtensionFetchPolicy();
248525
+ const disableAutoinstall = alwaysDisableAutoinstall || policy === "local-only";
248526
+ if (!disableAutoinstall || extensionSessionPinned.has(connection)) {
248527
+ return;
248528
+ }
248529
+ try {
248530
+ await connection.runSQL("SET autoinstall_known_extensions=false;");
248531
+ await connection.runSQL("SET autoload_known_extensions=true;");
248532
+ extensionSessionPinned.add(connection);
248533
+ } catch (error) {
248534
+ const detail = error instanceof Error ? error.message : String(error);
248535
+ if (policy === "local-only") {
248536
+ throw new Error(`Failed to disable DuckDB implicit extension auto-install under ` + `EXTENSION_FETCH_POLICY=local-only; refusing to open a session that ` + `could still fetch from the network. Underlying error: ${detail}`);
248537
+ }
248538
+ logger.warn("Failed to pin DuckDB extension session settings", {
248539
+ error: detail
248540
+ });
248541
+ }
248542
+ }
248439
248543
  async function installAndLoadExtension(connection, extensionName, fromCommunity = false) {
248544
+ const policy = getExtensionFetchPolicy();
248545
+ if (policy === "on-demand") {
248546
+ try {
248547
+ const installCommand = fromCommunity ? `INSTALL ${extensionName} FROM community;` : `INSTALL ${extensionName};`;
248548
+ await connection.runSQL(installCommand);
248549
+ logger.info(`${extensionName} extension installed`);
248550
+ } catch (error) {
248551
+ logger.info(`${extensionName} extension already installed or install skipped`, { error });
248552
+ }
248553
+ }
248440
248554
  try {
248441
- const installCommand = fromCommunity ? `FORCE INSTALL '${extensionName}' FROM community;` : `INSTALL ${extensionName};`;
248442
- await connection.runSQL(installCommand);
248443
- logger.info(`${extensionName} extension installed`);
248555
+ await connection.runSQL(`LOAD ${extensionName};`);
248556
+ logger.info(`${extensionName} extension loaded`);
248444
248557
  } catch (error) {
248445
- logger.info(`${extensionName} extension already installed or install skipped`, { error });
248558
+ if (policy === "local-only") {
248559
+ const detail = error instanceof Error ? error.message : String(error);
248560
+ logger.warn("DuckDB extension unavailable under local-only policy", {
248561
+ extension: extensionName,
248562
+ policy,
248563
+ error: detail
248564
+ });
248565
+ throw new Error(`DuckDB extension '${extensionName}' is not available locally and ` + `EXTENSION_FETCH_POLICY=local-only forbids fetching it. Bake the ` + `'${extensionName}' extension into the image (or a pre-populated ` + `~/.duckdb/extensions directory), or set ` + `EXTENSION_FETCH_POLICY=on-demand to allow fetching missing ` + `extensions on first use. Underlying LOAD error: ${detail}`);
248566
+ }
248567
+ throw error;
248446
248568
  }
248447
- await connection.runSQL(`LOAD ${extensionName};`);
248448
- logger.info(`${extensionName} extension loaded`);
248449
248569
  }
248450
248570
  async function isDatabaseAttached(connection, dbName) {
248451
248571
  try {
@@ -248617,7 +248737,67 @@ async function attachPostgres(connection, attachedDb) {
248617
248737
  await connection.runSQL(attachCommand);
248618
248738
  logger.info(`Successfully attached PostgreSQL database: ${attachedDb.name}`);
248619
248739
  }
248740
+ function runSQLRows(result) {
248741
+ if (Array.isArray(result))
248742
+ return result;
248743
+ const rows = result.rows;
248744
+ return Array.isArray(rows) ? rows : [];
248745
+ }
248746
+ var ducklakePreflightSeq = 0;
248747
+ async function preflightDuckLakeCatalogFormat(connection, dbName, pgConnString) {
248748
+ const tempDb = `${dbName}_fmt_preflight_${++ducklakePreflightSeq}`;
248749
+ let catalogFormat;
248750
+ try {
248751
+ await connection.runSQL(`ATTACH '${escapeSQL(pgConnString)}' AS ${tempDb} (TYPE postgres, READ_ONLY);`);
248752
+ const result = await connection.runSQL(`SELECT value FROM ${tempDb}.ducklake_metadata WHERE key = 'version' LIMIT 1;`);
248753
+ const value = runSQLRows(result)[0]?.value;
248754
+ catalogFormat = typeof value === "string" ? value : undefined;
248755
+ } catch (error) {
248756
+ logger.warn("DuckLake catalog-format preflight read failed; falling back to ATTACH", {
248757
+ dbName,
248758
+ error: redactPgSecrets(error instanceof Error ? error.message : String(error))
248759
+ });
248760
+ return;
248761
+ } finally {
248762
+ try {
248763
+ await connection.runSQL(`DETACH ${tempDb};`);
248764
+ } catch {}
248765
+ }
248766
+ if (!catalogFormat) {
248767
+ return;
248768
+ }
248769
+ let engineVersion = "";
248770
+ try {
248771
+ const versionResult = await connection.runSQL("SELECT version() AS v;");
248772
+ engineVersion = String(runSQLRows(versionResult)[0]?.v ?? "");
248773
+ } catch (error) {
248774
+ logger.warn("DuckLake catalog-format preflight: could not read engine version; skipping", {
248775
+ dbName,
248776
+ error: error instanceof Error ? error.message : String(error)
248777
+ });
248778
+ return;
248779
+ }
248780
+ const range = catalogFormatRangeForEngine(engineVersion);
248781
+ if (!range) {
248782
+ logger.warn("DuckLake catalog-format preflight: engine not covered by the format matrix; skipping", { dbName, engineVersion });
248783
+ return;
248784
+ }
248785
+ if (!isCatalogFormatInRange(catalogFormat, range)) {
248786
+ logger.warn("DuckLake catalog format outside supported range; rejecting", {
248787
+ dbName,
248788
+ catalogFormat,
248789
+ supportedMin: range.min,
248790
+ supportedMax: range.max,
248791
+ engineVersion: range.engineVersion
248792
+ });
248793
+ throw new UnsupportedCatalogFormatError(`DuckLake catalog '${dbName}' has format version ${catalogFormat}, ` + `which is outside the range this Publisher supports ` + `(${range.min} ≤ format ≤ ${range.max}). The DuckLake ` + `extension bundled with DuckDB v${range.engineVersion} attaches only ` + `catalogs in that range without migration. Migrate the catalog to a ` + `supported format (see the DuckLake catalog-migration docs at ` + `https://ducklake.select/docs), or run a Publisher built against a ` + `DuckDB engine whose supported range includes ${catalogFormat}.`);
248794
+ }
248795
+ logger.info(`DuckLake catalog '${dbName}' format ${catalogFormat} is within supported range ` + `${range.min}..${range.max} (engine v${range.engineVersion})`);
248796
+ }
248620
248797
  async function attachDuckLake(connection, dbName, ducklakeConfig) {
248798
+ await applyExtensionSessionSettings(connection, {
248799
+ alwaysDisableAutoinstall: true
248800
+ });
248621
248801
  await installAndLoadExtension(connection, "ducklake");
248622
248802
  await installAndLoadExtension(connection, "postgres");
248623
248803
  await installAndLoadExtension(connection, "aws");
@@ -248650,6 +248830,7 @@ async function attachDuckLake(connection, dbName, ducklakeConfig) {
248650
248830
  logger.info(`Final escaped connection string: ${redactPgSecrets(escapedPgConnString)}`);
248651
248831
  const escapedBucketUrl = escapeSQL(ducklakeConfig.storage.bucketUrl);
248652
248832
  logger.info(`escapedBucketUrl: ${escapedBucketUrl}`);
248833
+ await preflightDuckLakeCatalogFormat(connection, dbName, pgConnString);
248653
248834
  const attachCommand = `ATTACH OR REPLACE 'ducklake:postgres:${escapedPgConnString}' AS ${dbName} (DATA_PATH '${escapedBucketUrl}', OVERRIDE_DATA_PATH true, READ_ONLY true);`;
248654
248835
  logger.info(`Attaching DuckLake database using command: ${redactPgSecrets(attachCommand)}`);
248655
248836
  try {
@@ -248815,6 +248996,7 @@ async function attachDatabasesToDuckDB(duckdbConnection, attachedDatabases) {
248815
248996
  s3: attachCloudStorage,
248816
248997
  azure: attachAzureStorage
248817
248998
  };
248999
+ await applyExtensionSessionSettings(duckdbConnection);
248818
249000
  const hasAzure = attachedDatabases.some((db) => db.type === "azure");
248819
249001
  if (hasAzure) {
248820
249002
  await installAndLoadExtension(duckdbConnection, "azure");
@@ -249001,8 +249183,17 @@ function buildProxiedSslQuery(name, sslmode) {
249001
249183
  }
249002
249184
  return `?uselibpqcompat=true&sslmode=verify-ca&sslrootcert=${encodeURIComponent(caBundle)}`;
249003
249185
  default:
249004
- throw new Error(`Connection proxy on '${name}' has unsupported sslmode '${mode}' (expected disable | no-verify | verify-ca).`);
249186
+ throw new Error(`Connection proxy on '${name}' has unsupported sslmode '${mode}' (expected disable | no-verify | verify-ca; verify-full is applied via the ssl object in resolveProxiedTls, not this query).`);
249187
+ }
249188
+ }
249189
+ function resolveProxiedTls(name, host, sslmode) {
249190
+ if (sslmode === "verify-full") {
249191
+ if (!host) {
249192
+ throw new Error(`Connection proxy on '${name}' uses sslmode 'verify-full' but has no host to verify against.`);
249193
+ }
249194
+ return { query: "", ssl: { servername: host, rejectUnauthorized: true } };
249005
249195
  }
249196
+ return { query: buildProxiedSslQuery(name, sslmode) };
249006
249197
  }
249007
249198
  function buildProxiedPostgresConnection(metadata, endpoint) {
249008
249199
  const name = metadata.apiConnection.name;
@@ -249013,11 +249204,12 @@ function buildProxiedPostgresConnection(metadata, endpoint) {
249013
249204
  const enc = encodeURIComponent;
249014
249205
  const auth = pg.userName ? `${enc(pg.userName)}${pg.password ? `:${enc(pg.password)}` : ""}@` : "";
249015
249206
  const db = pg.databaseName ? `/${enc(pg.databaseName)}` : "";
249016
- const ssl = buildProxiedSslQuery(name, pg.sslmode);
249017
- const connectionString = `postgresql://${auth}${endpoint.host}:${endpoint.port}${db}${ssl}`;
249207
+ const { query, ssl } = resolveProxiedTls(name, pg.host, pg.sslmode);
249208
+ const connectionString = `postgresql://${auth}${endpoint.host}:${endpoint.port}${db}${query}`;
249018
249209
  return new PooledPostgresConnection({
249019
249210
  name,
249020
249211
  connectionString,
249212
+ ...ssl ? { ssl } : {},
249021
249213
  poolMin: 1,
249022
249214
  poolMax: 20
249023
249215
  });
@@ -249140,7 +249332,11 @@ function buildEnvironmentMalloyConfig(connections = [], environmentPath = "", is
249140
249332
  return {
249141
249333
  lookupConnection: async (name) => {
249142
249334
  const metadata = getMetadataForLookup(assembled.metadata, name);
249143
- return applyConnectionFingerprint(await resolveConnection(name, metadata), metadata);
249335
+ const connection = await resolveConnection(name, metadata);
249336
+ if (isDuckDBConnection(connection)) {
249337
+ await applyExtensionSessionSettings(connection);
249338
+ }
249339
+ return applyConnectionFingerprint(connection, metadata);
249144
249340
  }
249145
249341
  };
249146
249342
  });
@@ -258062,6 +258258,7 @@ async function fetchManifestEntries(uri) {
258062
258258
  import * as fs6 from "fs/promises";
258063
258259
  import * as path7 from "path";
258064
258260
  import"@malloydata/db-duckdb/native";
258261
+ import { DuckDBConnection as DuckDBConnection3 } from "@malloydata/db-duckdb";
258065
258262
  import {
258066
258263
  ConnectionRuntime,
258067
258264
  contextOverlay as contextOverlay2,
@@ -258073,8 +258270,8 @@ import {
258073
258270
  init_package_load_pool();
258074
258271
  init_constants();
258075
258272
  init_errors();
258076
- init_logger();
258077
258273
  var import_recursive_readdir = __toESM(require_recursive_readdir(), 1);
258274
+ init_logger();
258078
258275
 
258079
258276
  // src/package_load_metrics.ts
258080
258277
  var LOAD_DURATION_BUCKETS_MS = [
@@ -258135,6 +258332,10 @@ import {
258135
258332
  Annotations as Annotations2,
258136
258333
  API,
258137
258334
  FixedConnectionMap,
258335
+ isBasicArray,
258336
+ isJoined,
258337
+ isRepeatedRecord,
258338
+ isSourceDef as isSourceDef2,
258138
258339
  MalloyConfig as MalloyConfig2,
258139
258340
  MalloyError as MalloyError2,
258140
258341
  modelDefToModelInfo,
@@ -258245,24 +258446,95 @@ function annotationTexts(annote) {
258245
258446
  init_errors();
258246
258447
  var SOURCE_PREFIX = "#(authorize)";
258247
258448
  var FILE_PREFIX = "##(authorize)";
258248
- function buildAuthorizeProbe(exprs) {
258449
+ function buildAuthorizeProbe(exprs, givenDecls = []) {
258249
258450
  const selects = exprs.map((expr, i) => `__auth_${i} is (${expr})`).join(`
258250
258451
  `);
258251
- return `run: duckdb.sql("SELECT 1 AS __authorize_probe_row") -> {
258452
+ const givenBlock = givenDecls.length > 0 ? `given:
258453
+ ${givenDecls.map((g) => ` ${g.name} :: ${g.type}`).join(`
258454
+ `)}
258455
+
258456
+ ` : "";
258457
+ return `${givenBlock}run: duckdb.sql("SELECT 1 AS __authorize_probe_row") -> {
258252
258458
  select:
258253
258459
  ${selects}
258254
258460
  limit: 1
258255
258461
  }`;
258256
258462
  }
258463
+ var GIVEN_REF_PATTERN = /\$([A-Za-z_][A-Za-z0-9_]*)/g;
258464
+ var STRING_LITERAL_PATTERN = /'(?:\\.|[^'\\])*'/g;
258465
+ function referencedGivenNames(expr) {
258466
+ const scanned = expr.replace(STRING_LITERAL_PATTERN, "''");
258467
+ const names = [];
258468
+ const seen = new Set;
258469
+ for (const match of scanned.matchAll(GIVEN_REF_PATTERN)) {
258470
+ const name = match[1];
258471
+ if (!seen.has(name)) {
258472
+ seen.add(name);
258473
+ names.push(name);
258474
+ }
258475
+ }
258476
+ return names;
258477
+ }
258478
+ function inferGivenType(value) {
258479
+ if (typeof value === "string")
258480
+ return "string";
258481
+ if (typeof value === "number" || typeof value === "bigint")
258482
+ return "number";
258483
+ if (typeof value === "boolean")
258484
+ return "boolean";
258485
+ if (value instanceof Date)
258486
+ return "timestamp";
258487
+ if (Array.isArray(value)) {
258488
+ if (value.length === 0)
258489
+ return null;
258490
+ const elementType = inferGivenType(value[0]);
258491
+ return elementType ? `${elementType}[]` : null;
258492
+ }
258493
+ return null;
258494
+ }
258495
+ function bindProbeGivens(expr, givens, declaredTypes) {
258496
+ const decls = [];
258497
+ const bound = {};
258498
+ for (const name of referencedGivenNames(expr)) {
258499
+ if (!(name in givens))
258500
+ continue;
258501
+ const value = givens[name];
258502
+ const type = declaredTypes?.get(name) ?? inferGivenType(value);
258503
+ if (!type)
258504
+ continue;
258505
+ decls.push({ name, type });
258506
+ bound[name] = value;
258507
+ }
258508
+ return { decls, bound };
258509
+ }
258257
258510
  function isProbeTrue(cell) {
258258
258511
  return cell === true || cell === 1 || cell === "true";
258259
258512
  }
258260
- async function evaluateAuthorize(executor, exprs, givens) {
258513
+ async function runProbe(executor, probeText, givens) {
258514
+ const result = await executor.loadQuery(probeText).run({ rowLimit: 1, givens });
258515
+ const row = result?.data?.value?.[0];
258516
+ return !!(row && isProbeTrue(row.__auth_0));
258517
+ }
258518
+ async function evaluateAuthorize(executor, exprs, givens, declaredTypes, options) {
258519
+ const selfContainedFirst = options?.selfContainedFirst ?? false;
258261
258520
  for (const expr of exprs) {
258521
+ if (selfContainedFirst) {
258522
+ if (await evaluateSelfContainedFirst(executor, expr, givens, declaredTypes)) {
258523
+ return true;
258524
+ }
258525
+ continue;
258526
+ }
258527
+ try {
258528
+ if (await runProbe(executor, buildAuthorizeProbe([expr]), givens)) {
258529
+ return true;
258530
+ }
258531
+ continue;
258532
+ } catch {}
258262
258533
  try {
258263
- const result = await executor.loadQuery(buildAuthorizeProbe([expr])).run({ rowLimit: 1, givens });
258264
- const row = result?.data?.value?.[0];
258265
- if (row && isProbeTrue(row.__auth_0)) {
258534
+ const { decls, bound } = bindProbeGivens(expr, givens, declaredTypes);
258535
+ if (decls.length === 0)
258536
+ continue;
258537
+ if (await runProbe(executor, buildAuthorizeProbe([expr], decls), bound)) {
258266
258538
  return true;
258267
258539
  }
258268
258540
  } catch {
@@ -258271,6 +258543,29 @@ async function evaluateAuthorize(executor, exprs, givens) {
258271
258543
  }
258272
258544
  return false;
258273
258545
  }
258546
+ async function evaluateSelfContainedFirst(executor, expr, givens, declaredTypes) {
258547
+ const referenced = referencedGivenNames(expr);
258548
+ if (referenced.length === 0) {
258549
+ try {
258550
+ return await runProbe(executor, buildAuthorizeProbe([expr]), {});
258551
+ } catch {
258552
+ return false;
258553
+ }
258554
+ }
258555
+ const { decls, bound } = bindProbeGivens(expr, givens, declaredTypes);
258556
+ if (decls.length !== referenced.length) {
258557
+ return false;
258558
+ }
258559
+ try {
258560
+ return await runProbe(executor, buildAuthorizeProbe([expr], decls), bound);
258561
+ } catch {
258562
+ try {
258563
+ return await runProbe(executor, buildAuthorizeProbe([expr]), givens);
258564
+ } catch {
258565
+ return false;
258566
+ }
258567
+ }
258568
+ }
258274
258569
  async function validateAuthorizeProbes(compiler, sources) {
258275
258570
  for (const source of sources) {
258276
258571
  const exprs = source.authorize;
@@ -258665,6 +258960,9 @@ function extractQueriesFromModelDef(modelDef) {
258665
258960
 
258666
258961
  // src/service/model.ts
258667
258962
  var MALLOY_VERSION = createRequire2(import.meta.url)("@malloydata/malloy/package.json").version;
258963
+ function isRecordOrArrayField(field) {
258964
+ return field.type === "record" || isBasicArray(field) || isRepeatedRecord(field);
258965
+ }
258668
258966
  function quoteMalloyIdentifier(name) {
258669
258967
  return "`" + (name ?? "").replace(/\\/g, "\\\\").replace(/`/g, "\\`") + "`";
258670
258968
  }
@@ -258685,6 +258983,7 @@ class Model {
258685
258983
  filterMap;
258686
258984
  givens;
258687
258985
  fileLevelAuthorize = [];
258986
+ authorizeReferencedGivenNames = new Set;
258688
258987
  discoveryCurationEnabled = false;
258689
258988
  queryBoundary = { mode: "all", exploresDeclared: false, isQueryEntryPoint: true };
258690
258989
  freshnessResolver;
@@ -258712,6 +259011,11 @@ class Model {
258712
259011
  } catch {
258713
259012
  this.fileLevelAuthorize = [];
258714
259013
  }
259014
+ try {
259015
+ this.authorizeReferencedGivenNames = this.computeAuthorizeReferencedGivenNames();
259016
+ } catch {
259017
+ this.authorizeReferencedGivenNames = new Set;
259018
+ }
258715
259019
  this.modelInfo = modelInfo ?? (this.modelDef ? modelDefToModelInfo(this.modelDef) : undefined);
258716
259020
  if (this.filterMap.size > 0) {
258717
259021
  logger.warn(`Model "${packageName}/${modelPath}" uses deprecated #(filter) annotations. Migrate to given: — see https://github.com/malloydata/publisher/blob/main/docs/givens.md`, {
@@ -258724,9 +259028,45 @@ class Model {
258724
259028
  getFilters(sourceName) {
258725
259029
  return this.filterMap.get(sourceName) ?? [];
258726
259030
  }
259031
+ givenDeclaredTypes() {
259032
+ return new Map((this.givens ?? []).filter((g) => g.name != null && g.type != null).map((g) => [g.name, g.type]));
259033
+ }
258727
259034
  getAuthorize(sourceName) {
258728
259035
  return this.sources?.find((source) => source.name === sourceName)?.authorize ?? [];
258729
259036
  }
259037
+ filterGivensToModelSurface(givens) {
259038
+ if (!givens)
259039
+ return givens;
259040
+ const surfaceNames = new Set((this.givens ?? []).map((g) => g.name));
259041
+ const filtered = {};
259042
+ for (const [name, value] of Object.entries(givens)) {
259043
+ const authorizeOnly = !surfaceNames.has(name) && this.authorizeReferencedGivenNames.has(name);
259044
+ if (!authorizeOnly)
259045
+ filtered[name] = value;
259046
+ }
259047
+ return filtered;
259048
+ }
259049
+ computeAuthorizeReferencedGivenNames() {
259050
+ const names = new Set;
259051
+ const addExprs = (exprs) => {
259052
+ for (const expr of exprs) {
259053
+ for (const name of referencedGivenNames(expr))
259054
+ names.add(name);
259055
+ }
259056
+ };
259057
+ addExprs(this.fileLevelAuthorize);
259058
+ const modelDef = this.modelDef;
259059
+ if (!modelDef)
259060
+ return names;
259061
+ for (const entry of Object.values(modelDef.contents)) {
259062
+ if (!isSourceDef2(entry))
259063
+ continue;
259064
+ for (const { exprs } of this.collectAllReachableGates(entry, modelDef, new Set)) {
259065
+ addExprs(exprs);
259066
+ }
259067
+ }
259068
+ return names;
259069
+ }
258730
259070
  hasAuthorize() {
258731
259071
  return this.fileLevelAuthorize.length > 0 || (this.sources?.some((s) => (s.authorize?.length ?? 0) > 0) ?? false);
258732
259072
  }
@@ -258737,10 +259077,11 @@ class Model {
258737
259077
  return this.fileLevelAuthorize;
258738
259078
  }
258739
259079
  async assertAuthorized(sourceName, givens) {
258740
- const exprs = this.effectiveAuthorizeFor(sourceName);
259080
+ await this.assertAuthorizedExprs(sourceName ?? "(query)", this.effectiveAuthorizeFor(sourceName), givens);
259081
+ }
259082
+ async assertAuthorizedExprs(label, exprs, givens, selfContainedFirst = false) {
258741
259083
  if (exprs.length === 0)
258742
259084
  return;
258743
- const label = sourceName ?? "(query)";
258744
259085
  const deny = () => {
258745
259086
  throw new AccessDeniedError(`Access denied for source "${label}".`);
258746
259087
  };
@@ -258748,7 +259089,7 @@ class Model {
258748
259089
  deny();
258749
259090
  let passed = false;
258750
259091
  try {
258751
- passed = await evaluateAuthorize(this.modelMaterializer, exprs, givens);
259092
+ passed = await evaluateAuthorize(this.modelMaterializer, exprs, givens, this.givenDeclaredTypes(), { selfContainedFirst });
258752
259093
  } catch (err) {
258753
259094
  logger.debug("Authorize probe failed; denying", {
258754
259095
  sourceName: label,
@@ -258760,11 +259101,155 @@ class Model {
258760
259101
  if (!passed)
258761
259102
  deny();
258762
259103
  }
259104
+ async assertAuthorizedForAllSources(runnable, givens) {
259105
+ const ownSourceName = await this.resolveAuthorizeSourceFromRunnable(runnable);
259106
+ await this.assertAuthorized(ownSourceName, givens);
259107
+ const { struct, modelDef, compositeResolvedSourceDef, extendSources } = await this.resolveRunTargetStruct(runnable);
259108
+ const seen = new Set;
259109
+ const joinedGates = this.collectAllReachableGates(struct, modelDef, seen, true);
259110
+ if (modelDef) {
259111
+ const runTargetLabel = ownSourceName ?? "(run target)";
259112
+ for (const field of extendSources) {
259113
+ const { resolved, denyGate } = this.classifyJoinedField(field, runTargetLabel);
259114
+ if (denyGate) {
259115
+ joinedGates.push(denyGate);
259116
+ continue;
259117
+ }
259118
+ if (!resolved)
259119
+ continue;
259120
+ joinedGates.push(...this.collectAllReachableGates(resolved, modelDef, seen));
259121
+ }
259122
+ }
259123
+ if (compositeResolvedSourceDef && modelDef) {
259124
+ joinedGates.push(...this.collectAllReachableGates(compositeResolvedSourceDef, modelDef, seen, true));
259125
+ }
259126
+ for (const { label, exprs, selfContained } of joinedGates) {
259127
+ await this.assertAuthorizedExprs(label, exprs, givens, selfContained);
259128
+ }
259129
+ }
259130
+ async resolveRunTargetStruct(runnable) {
259131
+ try {
259132
+ const prepared = await runnable.getPreparedQuery();
259133
+ const modelDef = prepared._modelDef ?? this.modelDef;
259134
+ if (!modelDef)
259135
+ return {
259136
+ struct: undefined,
259137
+ modelDef: undefined,
259138
+ compositeResolvedSourceDef: undefined,
259139
+ extendSources: []
259140
+ };
259141
+ const structRef = prepared._query?.structRef;
259142
+ const struct = typeof structRef === "string" ? modelDef.contents[structRef] : structRef;
259143
+ const extendSources = (prepared._query?.pipeline ?? []).flatMap((segment) => segment.extendSource ?? []);
259144
+ return {
259145
+ struct: struct && typeof struct === "object" ? struct : undefined,
259146
+ modelDef,
259147
+ compositeResolvedSourceDef: prepared._query?.compositeResolvedSourceDef,
259148
+ extendSources
259149
+ };
259150
+ } catch {
259151
+ return {
259152
+ struct: undefined,
259153
+ modelDef: undefined,
259154
+ compositeResolvedSourceDef: undefined,
259155
+ extendSources: []
259156
+ };
259157
+ }
259158
+ }
259159
+ gateExprsForOwnAnnotations(struct) {
259160
+ const ownNotes = (struct.annotations?.blockNotes ?? []).map((note) => note.text);
259161
+ try {
259162
+ return [
259163
+ ...this.fileLevelAuthorize,
259164
+ ...collectAuthorizeExprs(ownNotes)
259165
+ ];
259166
+ } catch {
259167
+ return ["false"];
259168
+ }
259169
+ }
259170
+ classifyJoinedField(field, parentLabel) {
259171
+ if (!isJoined(field))
259172
+ return {};
259173
+ if (isRecordOrArrayField(field))
259174
+ return {};
259175
+ if (!isSourceDef2(field)) {
259176
+ logger.error("authorize: joined field failed to resolve to a walkable SourceDef; denying rather than silently skipping its gate (possible Malloy struct-shape drift)", {
259177
+ modelPath: this.modelPath,
259178
+ parentSource: parentLabel,
259179
+ fieldName: field.name,
259180
+ fieldType: field.type
259181
+ });
259182
+ return {
259183
+ denyGate: {
259184
+ label: `${parentLabel} (unresolvable joined source)`,
259185
+ exprs: ["false"],
259186
+ selfContained: false
259187
+ }
259188
+ };
259189
+ }
259190
+ return { resolved: field };
259191
+ }
259192
+ collectAllReachableGates(struct, modelDef, seen = new Set, treatAsOwnGate = false) {
259193
+ if (!struct || !modelDef || seen.has(struct))
259194
+ return [];
259195
+ seen.add(struct);
259196
+ const results = [];
259197
+ const label = struct.as ?? struct.name;
259198
+ const ownExprs = this.gateExprsForOwnAnnotations(struct);
259199
+ if (ownExprs.length > 0) {
259200
+ results.push({
259201
+ label,
259202
+ exprs: ownExprs,
259203
+ selfContained: !treatAsOwnGate
259204
+ });
259205
+ }
259206
+ for (const field of struct.fields) {
259207
+ const { resolved, denyGate } = this.classifyJoinedField(field, label);
259208
+ if (denyGate) {
259209
+ results.push(denyGate);
259210
+ continue;
259211
+ }
259212
+ if (!resolved)
259213
+ continue;
259214
+ const joinedSource = resolved;
259215
+ results.push(...this.collectAllReachableGates(joinedSource, modelDef, seen));
259216
+ if (joinedSource.type === "composite") {
259217
+ const members = joinedSource.sources;
259218
+ for (const member of members) {
259219
+ results.push(...this.collectAllReachableGates(member, modelDef, seen));
259220
+ }
259221
+ }
259222
+ }
259223
+ const duck = struct;
259224
+ if (duck.type === "query_source") {
259225
+ const ref = duck.query?.structRef;
259226
+ const base = typeof ref === "string" ? modelDef.contents[ref] : ref;
259227
+ if (base && isSourceDef2(base)) {
259228
+ results.push(...this.collectAllReachableGates(base, modelDef, seen));
259229
+ }
259230
+ const resolved = duck.query?.compositeResolvedSourceDef;
259231
+ if (resolved) {
259232
+ results.push(...this.collectAllReachableGates(resolved, modelDef, seen));
259233
+ }
259234
+ const innerJoins = (duck.query?.pipeline ?? []).flatMap((segment) => segment.extendSource ?? []);
259235
+ for (const field of innerJoins) {
259236
+ const { resolved: innerJoinSource, denyGate: innerJoinDenyGate } = this.classifyJoinedField(field, label);
259237
+ if (innerJoinDenyGate) {
259238
+ results.push(innerJoinDenyGate);
259239
+ continue;
259240
+ }
259241
+ if (!innerJoinSource)
259242
+ continue;
259243
+ results.push(...this.collectAllReachableGates(innerJoinSource, modelDef, seen));
259244
+ }
259245
+ }
259246
+ return results;
259247
+ }
258763
259248
  async assertAuthorizedForText(text, givens) {
258764
259249
  await this.assertAuthorized(extractRunTargetSourceName(text), givens);
258765
259250
  }
258766
259251
  async assertAuthorizedForRunnable(runnable, givens) {
258767
- await this.assertAuthorized(await this.resolveAuthorizeSourceFromRunnable(runnable), givens);
259252
+ await this.assertAuthorizedForAllSources(runnable, givens);
258768
259253
  }
258769
259254
  async resolveAuthorizeSourceFromRunnable(runnable) {
258770
259255
  try {
@@ -259146,21 +259631,23 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
259146
259631
  if (boundary === "deferred") {
259147
259632
  this.assertQueryBoundaryCompiled(compiledSource, query);
259148
259633
  }
259149
- if (!(compiledSource && compiledSource === earlySource)) {
259150
- await this.assertAuthorized(compiledSource, givens ?? {});
259151
- }
259634
+ await this.assertAuthorizedForAllSources(runnable, givens ?? {});
259152
259635
  const maxRows = getMaxQueryRows();
259153
259636
  const maxBytes = getMaxResponseBytes();
259154
259637
  const buildManifest = this.resolveFreshBuildManifest();
259155
259638
  let rowLimit = 0;
259156
259639
  let executionTime = 0;
259157
259640
  let queryResults;
259641
+ const querySurfaceGivens = this.filterGivensToModelSurface(givens);
259158
259642
  try {
259159
- rowLimit = resolveModelQueryRowLimit((await runnable.getPreparedResult({ givens, buildManifest })).resultExplore.limit, { defaultLimit: getDefaultQueryRowLimit(), maxRows });
259643
+ rowLimit = resolveModelQueryRowLimit((await runnable.getPreparedResult({
259644
+ givens: querySurfaceGivens,
259645
+ buildManifest
259646
+ })).resultExplore.limit, { defaultLimit: getDefaultQueryRowLimit(), maxRows });
259160
259647
  executionTime = performance.now() - startTime;
259161
259648
  queryResults = await runnable.run({
259162
259649
  rowLimit,
259163
- givens,
259650
+ givens: querySurfaceGivens,
259164
259651
  abortSignal,
259165
259652
  buildManifest
259166
259653
  });
@@ -259276,8 +259763,7 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
259276
259763
  };
259277
259764
  }
259278
259765
  if (cell.runnable) {
259279
- const authorizeSource = await this.resolveAuthorizeSourceFromRunnable(cell.runnable);
259280
- await this.assertAuthorized(authorizeSource, givens ?? {});
259766
+ await this.assertAuthorizedForAllSources(cell.runnable, givens ?? {});
259281
259767
  }
259282
259768
  let queryName = undefined;
259283
259769
  let queryResult = undefined;
@@ -259300,8 +259786,9 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
259300
259786
  const cellMaxRows = getMaxQueryRows();
259301
259787
  const cellMaxBytes = getMaxResponseBytes();
259302
259788
  const buildManifest = this.resolveFreshBuildManifest();
259789
+ const cellSurfaceGivens = this.filterGivensToModelSurface(givens);
259303
259790
  const rowLimit = resolveModelQueryRowLimit((await runnableToExecute.getPreparedResult({
259304
- givens,
259791
+ givens: cellSurfaceGivens,
259305
259792
  buildManifest
259306
259793
  })).resultExplore.limit, {
259307
259794
  defaultLimit: getDefaultQueryRowLimit(),
@@ -259309,7 +259796,7 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
259309
259796
  });
259310
259797
  const result = await runnableToExecute.run({
259311
259798
  rowLimit,
259312
- givens,
259799
+ givens: cellSurfaceGivens,
259313
259800
  abortSignal,
259314
259801
  buildManifest
259315
259802
  });
@@ -260491,7 +260978,11 @@ class Package {
260491
260978
  malloyConfig.wrapConnections((base) => ({
260492
260979
  lookupConnection: async (name) => {
260493
260980
  if (!name || name === "duckdb") {
260494
- return base.lookupConnection(name);
260981
+ const connection = await base.lookupConnection(name);
260982
+ if (connection instanceof DuckDBConnection3) {
260983
+ await applyExtensionSessionSettings(connection);
260984
+ }
260985
+ return connection;
260495
260986
  }
260496
260987
  return getEnvironmentMalloyConfig().connections.lookupConnection(name);
260497
260988
  }
@@ -260593,6 +261084,7 @@ class Environment {
260593
261084
  packageMutexes = new Map;
260594
261085
  packageStatuses = new Map;
260595
261086
  failedPackages = new Map;
261087
+ mountErrors = new Map;
260596
261088
  malloyConfig;
260597
261089
  connectionMutex = new Mutex;
260598
261090
  retiredConnectionGenerations = new Set;
@@ -260729,7 +261221,7 @@ ${source}` : source;
260729
261221
  if (queryMaterializer && gateModel) {
260730
261222
  await gateModel.assertQueryBoundaryForRunnable(queryMaterializer, source);
260731
261223
  }
260732
- if (queryMaterializer && gateModel?.hasAuthorize()) {
261224
+ if (queryMaterializer && gateModel) {
260733
261225
  await gateModel.assertAuthorizedForRunnable(queryMaterializer, givens ?? {});
260734
261226
  }
260735
261227
  let sql;
@@ -260915,7 +261407,7 @@ ${source}` : source;
260915
261407
  }
260916
261408
  this.packages.set(packageName, _package);
260917
261409
  this.setPackageStatus(packageName, "serving" /* SERVING */);
260918
- this.failedPackages.delete(packageName);
261410
+ this.clearPackageLoadFailure(packageName);
260919
261411
  logger.debug(`Successfully loaded package ${packageName}`);
260920
261412
  return _package;
260921
261413
  } catch (error) {
@@ -260956,7 +261448,7 @@ ${source}` : source;
260956
261448
  throw error;
260957
261449
  }
260958
261450
  this.setPackageStatus(packageName, "serving" /* SERVING */);
260959
- this.failedPackages.delete(packageName);
261451
+ this.clearPackageLoadFailure(packageName);
260960
261452
  return this.packages.get(packageName);
260961
261453
  }
260962
261454
  async installPackage(packageName, downloader, validate) {
@@ -261051,7 +261543,7 @@ ${source}` : source;
261051
261543
  await this.bindManifestIfConfigured(newPackage);
261052
261544
  this.packages.set(packageName, newPackage);
261053
261545
  this.setPackageStatus(packageName, "serving" /* SERVING */);
261054
- this.failedPackages.delete(packageName);
261546
+ this.clearPackageLoadFailure(packageName);
261055
261547
  if (oldPackage) {
261056
261548
  this.retireConnectionGeneration(`package ${packageName}`, () => oldPackage.getMalloyConfig().shutdown("close"));
261057
261549
  }
@@ -261229,8 +261721,17 @@ ${source}` : source;
261229
261721
  getPackageStatus(packageName) {
261230
261722
  return this.packageStatuses.get(packageName);
261231
261723
  }
261724
+ setPackageMountError(packageName, message) {
261725
+ this.mountErrors.set(packageName, message);
261726
+ }
261727
+ clearPackageLoadFailure(packageName) {
261728
+ this.failedPackages.delete(packageName);
261729
+ this.mountErrors.delete(packageName);
261730
+ }
261232
261731
  getFailedPackages() {
261233
- return this.failedPackages;
261732
+ if (this.mountErrors.size === 0)
261733
+ return this.failedPackages;
261734
+ return new Map([...this.failedPackages, ...this.mountErrors]);
261234
261735
  }
261235
261736
  setPackageStatus(packageName, status) {
261236
261737
  const currentStatus = this.packageStatuses.get(packageName);
@@ -261246,7 +261747,7 @@ ${source}` : source;
261246
261747
  async deletePackage(packageName) {
261247
261748
  assertSafePackageName(packageName);
261248
261749
  return this.withPackageLock(packageName, async () => {
261249
- this.failedPackages.delete(packageName);
261750
+ this.clearPackageLoadFailure(packageName);
261250
261751
  const _package = this.packages.get(packageName);
261251
261752
  if (!_package) {
261252
261753
  return;
@@ -261900,9 +262401,12 @@ class EnvironmentStore {
261900
262401
  const environmentConfig = environmentManifest.environments.find((e) => e.name === environmentName);
261901
262402
  const hasPackages = environment?.packages && environment.packages.length > 0 || environmentConfig?.packages && environmentConfig.packages.length > 0;
261902
262403
  let absoluteEnvironmentPath;
262404
+ let mountErrors = new Map;
261903
262405
  if (hasPackages) {
261904
262406
  const packagesToProcess = environment?.packages || environmentConfig?.packages || [];
261905
- absoluteEnvironmentPath = await this.loadEnvironmentIntoDisk(environmentName, packagesToProcess);
262407
+ const loaded = await this.loadEnvironmentIntoDisk(environmentName, packagesToProcess);
262408
+ absoluteEnvironmentPath = loaded.path;
262409
+ mountErrors = loaded.mountErrors;
261906
262410
  if (absoluteEnvironmentPath.endsWith(".zip")) {
261907
262411
  absoluteEnvironmentPath = await this.unzipEnvironment(absoluteEnvironmentPath);
261908
262412
  }
@@ -261914,6 +262418,9 @@ class EnvironmentStore {
261914
262418
  if (!newEnvironment.metadata)
261915
262419
  newEnvironment.metadata = {};
261916
262420
  newEnvironment.metadata.location = absoluteEnvironmentPath;
262421
+ for (const [packageName, message] of mountErrors) {
262422
+ newEnvironment.setPackageMountError(packageName, message);
262423
+ }
261917
262424
  this.environments.set(environmentName, newEnvironment);
261918
262425
  this.failedEnvironments.delete(environmentName);
261919
262426
  environment?.packages?.forEach((_package) => {
@@ -262058,9 +262565,17 @@ class EnvironmentStore {
262058
262565
  isS3URL(location) {
262059
262566
  return location.startsWith("s3://");
262060
262567
  }
262568
+ recordMountFailure(error, location, packageNames, mountErrors) {
262569
+ logger.error(`Failed to download or mount location "${location}"`, this.extractErrorDataFromError(error));
262570
+ const message = redactPgSecrets(error instanceof Error ? error.message : String(error));
262571
+ for (const packageName of packageNames) {
262572
+ mountErrors.set(packageName, message);
262573
+ }
262574
+ }
262061
262575
  async loadEnvironmentIntoDisk(environmentName, packages) {
262062
262576
  assertSafePackageName(environmentName);
262063
262577
  const absoluteTargetPath = safeJoinUnderRoot(this.serverRootPath, PUBLISHER_DATA_DIR, environmentName);
262578
+ const mountErrors = new Map;
262064
262579
  await fs8.promises.mkdir(absoluteTargetPath, { recursive: true });
262065
262580
  if (!packages || packages.length === 0) {
262066
262581
  throw new PackageNotFoundError(`No packages found for environment ${environmentName}`);
@@ -262093,12 +262608,19 @@ class EnvironmentStore {
262093
262608
  const tempDownloadPath = safeJoinUnderRoot(absoluteTargetPath, `.temp_${locationHash}`);
262094
262609
  await fs8.promises.mkdir(tempDownloadPath, { recursive: true });
262095
262610
  logger.info(`Created temporary directory: ${tempDownloadPath}`);
262611
+ let downloaded = true;
262096
262612
  try {
262097
262613
  await this.downloadOrMountLocation(groupedLocation, tempDownloadPath, environmentName, "shared");
262098
- for (const _package of packagesForLocation) {
262614
+ } catch (error) {
262615
+ downloaded = false;
262616
+ this.recordMountFailure(error, groupedLocation, packagesForLocation.map((p) => p.name), mountErrors);
262617
+ }
262618
+ for (const _package of downloaded ? packagesForLocation : []) {
262619
+ let absolutePackagePath;
262620
+ try {
262099
262621
  const packageDir = _package.name;
262100
262622
  assertSafePackageName(packageDir);
262101
- const absolutePackagePath = safeJoinUnderRoot(absoluteTargetPath, packageDir);
262623
+ absolutePackagePath = safeJoinUnderRoot(absoluteTargetPath, packageDir);
262102
262624
  let sourcePath;
262103
262625
  if (this.isGitHubURL(_package.location)) {
262104
262626
  const githubInfo = this.parseGitHubUrl(_package.location);
@@ -262163,11 +262685,12 @@ class EnvironmentStore {
262163
262685
  });
262164
262686
  logger.info(`Copied entire download as package "${packageDir}"`);
262165
262687
  }
262688
+ } catch (error) {
262689
+ this.recordMountFailure(error, groupedLocation, [_package.name], mountErrors);
262690
+ if (absolutePackagePath) {
262691
+ await clearMountTarget(absolutePackagePath);
262692
+ }
262166
262693
  }
262167
- } catch (error) {
262168
- const errorData = this.extractErrorDataFromError(error);
262169
- logger.error(`Failed to download or mount location "${groupedLocation}"`, errorData);
262170
- throw new PackageNotFoundError(`Failed to download or mount location: ${groupedLocation}`);
262171
262694
  }
262172
262695
  try {
262173
262696
  await fs8.promises.rm(tempDownloadPath, {
@@ -262180,7 +262703,7 @@ class EnvironmentStore {
262180
262703
  });
262181
262704
  }
262182
262705
  }
262183
- return absoluteTargetPath;
262706
+ return { path: absoluteTargetPath, mountErrors };
262184
262707
  }
262185
262708
  async downloadOrMountLocation(location, targetPath, environmentName, packageName) {
262186
262709
  const isCompressedFile = location.endsWith(".zip");
@@ -266948,6 +267471,8 @@ my-package/
266948
267471
  public/ # ONLY this directory is web-served
266949
267472
  index.html
266950
267473
  app.js
267474
+ vendor/ # chart library, vendored rather than loaded from a CDN
267475
+ chart.umd.js
266951
267476
  \`\`\`
266952
267477
 
266953
267478
  Only \`public/\` is reachable over the web, at \`/environments/<env>/packages/<pkg>/<file>\`. Models, data, and \`publisher.json\` are private and reached only through the query API, which still applies the model's filters, access modifiers, and authorize rules. There is no flag to set: a \`public/\` directory is what makes a package an app.
@@ -266959,7 +267484,7 @@ The agent orchestrates these. Each query and chart step hands off to a focused s
266959
267484
  1. READ THE MODEL FIRST. Get the model's real source and view names, through your environment's context tool if it has one, or by opening the \`.malloy\` file directly. Never guess field or view names.
266960
267485
  2. SCAFFOLD the package (template below).
266961
267486
  3. WRITE THE QUERIES with \`skill:malloy-html-data-app-runtime\`. Validate each before pasting it into the page, using whatever query tool your environment provides or a running Publisher (see \`skill:malloy-html-data-app-runtime\`). Malloy syntax questions go to \`skill:malloy-queries\`.
266962
- 4. CHOOSE CHARTS with \`skill:malloy-charts\` when rendering through \`<malloy-render>\`; otherwise it is your own chart library drawing the returned rows. Vendor any chart library into \`public/\` and load it locally, not from a CDN, because embedded author JavaScript runs with the viewing user's data authority. (The shipped example loads its chart library from a CDN for brevity; a published or embedded app should vendor it.)
267487
+ 4. CHOOSE CHARTS with \`skill:malloy-charts\` when rendering through \`<malloy-render>\`; otherwise it is your own chart library drawing the returned rows. Vendor any chart library into \`public/\` and load it locally, not from a CDN. Two reasons: embedded author JavaScript runs with the viewing user's data authority, and a blocked CDN (agent sandboxes and many corporate networks block them) is easy to miss, because the script never runs and the charts come up empty. The \`html-data-app\` and \`storefront\` examples both ship their chart library in \`public/vendor/\` and load it from \`public/index.html\` as \`./vendor/chart.umd.js\`. Copy that, but resolve the path against the page's own directory: a page in a subdirectory (\`public/reports/index.html\`) needs \`../vendor/chart.umd.js\`. A wrong relative path 404s and leaves the charts blank, which is the failure you are trying to avoid.
266963
267488
  5. EMBED (optional) with \`skill:malloy-html-data-app-embedding\`.
266964
267489
  6. PREVIEW with the local authoring loop (below).
266965
267490
  7. VERIFY before you call it done (see "What 'done' means" below). This step is not optional.
@@ -269451,6 +269976,7 @@ var environmentStore = new EnvironmentStore(SERVER_ROOT);
269451
269976
  var watchModeController = new WatchModeController(environmentStore);
269452
269977
  var connectionController = new ConnectionController(environmentStore);
269453
269978
  var modelController = new ModelController(environmentStore);
269979
+ logger.info(`DuckDB extension-fetch policy: ${getExtensionFetchPolicy()}`);
269454
269980
  var memoryGovernorConfig = getMemoryGovernorConfig();
269455
269981
  var memoryGovernor = memoryGovernorConfig ? new PackageMemoryGovernor(memoryGovernorConfig) : null;
269456
269982
  memoryGovernor?.start();