@malloy-publisher/server 0.0.224 → 0.0.226

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 (48) hide show
  1. package/dist/app/api-doc.yaml +76 -13
  2. package/dist/app/assets/{EnvironmentPage-DYTeXDll.js → EnvironmentPage-DvOJ7L_b.js} +1 -1
  3. package/dist/app/assets/{HomePage-pDK2BPJY.js → HomePage-CXguJsXS.js} +1 -1
  4. package/dist/app/assets/{LightMode-C2bwGPY1.js → LightMode-ZsshUznu.js} +1 -1
  5. package/dist/app/assets/{MainPage-WtBulMH_.js → MainPage-BIe0VwBa.js} +2 -2
  6. package/dist/app/assets/{MaterializationsPage-hMgOtflG.js → MaterializationsPage-BuZ6UJVx.js} +1 -1
  7. package/dist/app/assets/{ModelPage-B2N5kYII.js → ModelPage-DsPf-s8B.js} +1 -1
  8. package/dist/app/assets/{PackagePage-CEN90nQG.js → PackagePage-CEVNAKZa.js} +1 -1
  9. package/dist/app/assets/{RouteError-BG2c5Zf0.js → RouteError-Chn7lL96.js} +1 -1
  10. package/dist/app/assets/{ThemeEditorPage-DNfeUwEZ.js → ThemeEditorPage-DWC_FdNU.js} +1 -1
  11. package/dist/app/assets/{WorkbookPage-NKI1BhFS.js → WorkbookPage-CGrsFz8p.js} +1 -1
  12. package/dist/app/assets/{core-C6anj5c0.es-DDLHqpzt.js → core-vVgoO8IR.es-BD_THWs_.js} +1 -1
  13. package/dist/app/assets/{index-DMQtnaf4.js → index-BioohWQj.js} +1 -1
  14. package/dist/app/assets/{index-CzNqKMVl.js → index-D6YtyiJ0.js} +1 -1
  15. package/dist/app/assets/{index-C6gZ6sSY.js → index-DNUZpnaa.js} +4 -4
  16. package/dist/app/assets/{index-JXgvyZna.js → index-gEWxu09x.js} +1 -1
  17. package/dist/app/index.html +1 -1
  18. package/dist/instrumentation.mjs +71 -15
  19. package/dist/package_load_worker.mjs +83 -16
  20. package/dist/server.mjs +249 -46
  21. package/dist/sshcrypto-8m50vnmb.node +0 -0
  22. package/package.json +1 -1
  23. package/src/controller/materialization.controller.spec.ts +72 -0
  24. package/src/controller/materialization.controller.ts +75 -11
  25. package/src/controller/package.controller.spec.ts +17 -17
  26. package/src/controller/package.controller.ts +7 -6
  27. package/src/logger.spec.ts +193 -0
  28. package/src/logger.ts +115 -18
  29. package/src/mcp/skills/skills_bundle.json +1 -1
  30. package/src/package_load/package_load_pool.ts +5 -1
  31. package/src/package_load/package_load_worker.ts +7 -0
  32. package/src/package_load/protocol.ts +5 -1
  33. package/src/service/build_plan.spec.ts +110 -9
  34. package/src/service/build_plan.ts +126 -7
  35. package/src/service/environment.ts +4 -0
  36. package/src/service/materialization_service.spec.ts +42 -0
  37. package/src/service/materialization_service.ts +40 -2
  38. package/src/service/materialization_test_fixtures.ts +46 -15
  39. package/src/service/package.ts +116 -32
  40. package/src/service/package_manifest.spec.ts +22 -1
  41. package/src/service/package_manifest.ts +29 -0
  42. package/src/service/persistence_policy.spec.ts +234 -0
  43. package/src/storage/DatabaseInterface.ts +1 -0
  44. package/tests/fixtures/persist-multi-level/data/orders.csv +5 -0
  45. package/tests/fixtures/persist-multi-level/multi_level.malloy +18 -0
  46. package/tests/fixtures/persist-multi-level/publisher.json +5 -0
  47. package/tests/integration/materialization/reference_manifest.integration.spec.ts +251 -0
  48. package/src/service/materialization_cron_gate.spec.ts +0 -142
package/src/logger.ts CHANGED
@@ -84,6 +84,73 @@ export function formatDuration(durationMs: number): string {
84
84
  return `${durationMs.toFixed(2)}ms`;
85
85
  }
86
86
 
87
+ // Connection endpoints carry provider credentials in their request/response
88
+ // bodies (BigQuery serviceAccountKeyJson, Snowflake privateKey/password, Postgres
89
+ // password/connectionString, S3 secretAccessKey/sessionToken, Azure clientSecret/
90
+ // sasUrl, Databricks token, ...), and axios error logs can carry credential HTTP
91
+ // headers (Authorization, Cookie). Those values are masked by key name before
92
+ // they reach a log transport. Matching is case-insensitive so header casing
93
+ // (Authorization vs authorization) does not slip through.
94
+ const SENSITIVE_KEY_NAMES = [
95
+ // connection config credentials
96
+ "password",
97
+ "connectionString",
98
+ "serviceAccountKeyJson",
99
+ "privateKey",
100
+ "privateKeyPass",
101
+ "secret",
102
+ "secretAccessKey",
103
+ "sessionToken",
104
+ "sasUrl",
105
+ "clientSecret",
106
+ "oauthClientSecret",
107
+ "peakaKey",
108
+ "token",
109
+ "accessToken",
110
+ // credential-bearing HTTP headers
111
+ "authorization",
112
+ "proxy-authorization",
113
+ "cookie",
114
+ "set-cookie",
115
+ "x-api-key",
116
+ ];
117
+ const SENSITIVE_KEYS: ReadonlySet<string> = new Set(
118
+ SENSITIVE_KEY_NAMES.map((name) => name.toLowerCase()),
119
+ );
120
+
121
+ const REDACTED = "[REDACTED]";
122
+
123
+ /**
124
+ * Deep-copy `value`, replacing any property whose key names a credential
125
+ * (SENSITIVE_KEYS) with a placeholder. The match is by key name (case-
126
+ * insensitive) and recurses through nested objects and arrays, since
127
+ * connections arrive nested under `connections[]`. Primitives and Dates pass
128
+ * through unchanged and the input is never mutated; used to keep secrets out of
129
+ * the request/response and axios-error logs.
130
+ */
131
+ export function redactSensitive(
132
+ value: unknown,
133
+ seen: WeakSet<object> = new WeakSet(),
134
+ ): unknown {
135
+ if (value === null || typeof value !== "object" || value instanceof Date) {
136
+ return value;
137
+ }
138
+ if (seen.has(value)) {
139
+ return "[Circular]";
140
+ }
141
+ seen.add(value);
142
+ if (Array.isArray(value)) {
143
+ return value.map((item) => redactSensitive(item, seen));
144
+ }
145
+ const result: Record<string, unknown> = {};
146
+ for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
147
+ result[key] = SENSITIVE_KEYS.has(key.toLowerCase())
148
+ ? REDACTED
149
+ : redactSensitive(val, seen);
150
+ }
151
+ return result;
152
+ }
153
+
87
154
  export const loggerMiddleware: RequestHandler = (req, res, next) => {
88
155
  const startTime = performance.now();
89
156
  const resJson = res.json;
@@ -102,14 +169,14 @@ export const loggerMiddleware: RequestHandler = (req, res, next) => {
102
169
  const logMetadata: Record<string, unknown> = {
103
170
  statusCode: res.statusCode,
104
171
  duration: formatDuration(durationMs),
105
- payload: req.body,
172
+ payload: redactSensitive(req.body),
106
173
  params: req.params,
107
174
  query: req.query,
108
175
  };
109
176
 
110
177
  // Only include response body if response logging is enabled
111
178
  if (!DISABLE_RESPONSE_LOGGING) {
112
- logMetadata.response = res.locals.body;
179
+ logMetadata.response = redactSensitive(res.locals.body);
113
180
  }
114
181
 
115
182
  // Add traceId to log metadata if present
@@ -130,23 +197,53 @@ export const loggerMiddleware: RequestHandler = (req, res, next) => {
130
197
  next();
131
198
  };
132
199
 
133
- export const logAxiosError = (error: AxiosError) => {
200
+ /**
201
+ * Build the message + metadata logged for an AxiosError, with secrets removed.
202
+ * A failing upstream call can echo a connection config in `response.data` or
203
+ * return credential headers, and the raw `error.request` / `error` objects hold
204
+ * the outgoing Authorization header. Response fields are redacted and the raw
205
+ * request object is reduced to a few safe descriptors, so credentials never
206
+ * reach the log. Split out from `logAxiosError` so it can be unit tested.
207
+ */
208
+ export function buildAxiosErrorLog(error: AxiosError): {
209
+ message: string;
210
+ meta: Record<string, unknown>;
211
+ } {
134
212
  if (error.response) {
135
213
  // The request was made and the server responded with a status code
136
- // that falls out of the range of 2xx
137
- logger.error("Axios server-side error", {
138
- url: error.response.config.url,
139
- status: error.response.status,
140
- headers: error.response.headers,
141
- data: error.response.data,
142
- });
143
- } else if (error.request) {
144
- // The request was made but no response was received
145
- // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
146
- // http.ClientRequest in node.js
147
- logger.error("Axios client-side error", { error: error.request });
148
- } else {
149
- // Something happened in setting up the request that triggered an Error
150
- logger.error("Axios unknown error", { error });
214
+ // that falls out of the range of 2xx.
215
+ return {
216
+ message: "Axios server-side error",
217
+ meta: {
218
+ url: error.response.config.url,
219
+ status: error.response.status,
220
+ headers: redactSensitive(error.response.headers),
221
+ data: redactSensitive(error.response.data),
222
+ },
223
+ };
224
+ }
225
+ if (error.request) {
226
+ // The request was made but no response was received. `error.request` is
227
+ // the raw ClientRequest/XHR and carries the outgoing headers (including
228
+ // Authorization), so log only safe descriptors from the config.
229
+ return {
230
+ message: "Axios client-side error",
231
+ meta: {
232
+ method: error.config?.method,
233
+ url: error.config?.url,
234
+ code: error.code,
235
+ },
236
+ };
151
237
  }
238
+ // Something happened setting up the request. The AxiosError still holds
239
+ // `config.headers`, so log only the message.
240
+ return {
241
+ message: "Axios unknown error",
242
+ meta: { message: error.message },
243
+ };
244
+ }
245
+
246
+ export const logAxiosError = (error: AxiosError) => {
247
+ const { message, meta } = buildAxiosErrorLog(error);
248
+ logger.error(message, meta);
152
249
  };