@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.
- package/dist/app/api-doc.yaml +76 -13
- package/dist/app/assets/{EnvironmentPage-DYTeXDll.js → EnvironmentPage-DvOJ7L_b.js} +1 -1
- package/dist/app/assets/{HomePage-pDK2BPJY.js → HomePage-CXguJsXS.js} +1 -1
- package/dist/app/assets/{LightMode-C2bwGPY1.js → LightMode-ZsshUznu.js} +1 -1
- package/dist/app/assets/{MainPage-WtBulMH_.js → MainPage-BIe0VwBa.js} +2 -2
- package/dist/app/assets/{MaterializationsPage-hMgOtflG.js → MaterializationsPage-BuZ6UJVx.js} +1 -1
- package/dist/app/assets/{ModelPage-B2N5kYII.js → ModelPage-DsPf-s8B.js} +1 -1
- package/dist/app/assets/{PackagePage-CEN90nQG.js → PackagePage-CEVNAKZa.js} +1 -1
- package/dist/app/assets/{RouteError-BG2c5Zf0.js → RouteError-Chn7lL96.js} +1 -1
- package/dist/app/assets/{ThemeEditorPage-DNfeUwEZ.js → ThemeEditorPage-DWC_FdNU.js} +1 -1
- package/dist/app/assets/{WorkbookPage-NKI1BhFS.js → WorkbookPage-CGrsFz8p.js} +1 -1
- package/dist/app/assets/{core-C6anj5c0.es-DDLHqpzt.js → core-vVgoO8IR.es-BD_THWs_.js} +1 -1
- package/dist/app/assets/{index-DMQtnaf4.js → index-BioohWQj.js} +1 -1
- package/dist/app/assets/{index-CzNqKMVl.js → index-D6YtyiJ0.js} +1 -1
- package/dist/app/assets/{index-C6gZ6sSY.js → index-DNUZpnaa.js} +4 -4
- package/dist/app/assets/{index-JXgvyZna.js → index-gEWxu09x.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/instrumentation.mjs +71 -15
- package/dist/package_load_worker.mjs +83 -16
- package/dist/server.mjs +249 -46
- package/dist/sshcrypto-8m50vnmb.node +0 -0
- package/package.json +1 -1
- package/src/controller/materialization.controller.spec.ts +72 -0
- package/src/controller/materialization.controller.ts +75 -11
- package/src/controller/package.controller.spec.ts +17 -17
- package/src/controller/package.controller.ts +7 -6
- package/src/logger.spec.ts +193 -0
- package/src/logger.ts +115 -18
- package/src/mcp/skills/skills_bundle.json +1 -1
- package/src/package_load/package_load_pool.ts +5 -1
- package/src/package_load/package_load_worker.ts +7 -0
- package/src/package_load/protocol.ts +5 -1
- package/src/service/build_plan.spec.ts +110 -9
- package/src/service/build_plan.ts +126 -7
- package/src/service/environment.ts +4 -0
- package/src/service/materialization_service.spec.ts +42 -0
- package/src/service/materialization_service.ts +40 -2
- package/src/service/materialization_test_fixtures.ts +46 -15
- package/src/service/package.ts +116 -32
- package/src/service/package_manifest.spec.ts +22 -1
- package/src/service/package_manifest.ts +29 -0
- package/src/service/persistence_policy.spec.ts +234 -0
- package/src/storage/DatabaseInterface.ts +1 -0
- package/tests/fixtures/persist-multi-level/data/orders.csv +5 -0
- package/tests/fixtures/persist-multi-level/multi_level.malloy +18 -0
- package/tests/fixtures/persist-multi-level/publisher.json +5 -0
- package/tests/integration/materialization/reference_manifest.integration.spec.ts +251 -0
- 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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
//
|
|
150
|
-
|
|
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
|
};
|