@leostera/evalkit 0.4.0 → 0.5.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to `@leostera/evalkit`. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses [Semantic Versioning](https://semver.org/). Before 1.0, a minor version may include breaking changes; they are marked **Breaking**.
4
4
 
5
+ ## [0.5.0] - 2026-10-28
6
+
7
+ ### Added
8
+
9
+ - `share.headers` in `evalkit.config.ts`: extra headers for every request to the share server, for an auth proxy such as Cloudflare Access in front of it. It can be an object or a function (sync or async), is called once per `evalkit share`, and its values are never printed.
10
+ - `evalkit share` checks the server before uploading, in a new Server step.
11
+
12
+ ### Fixed
13
+
14
+ - `evalkit share` no longer follows redirects or treats an HTML page as a successful upload. A redirect (for example to a login page), a 401 or 403, or a non-JSON answer stops sharing with an error naming the cause, instead of `JSON Parse error: Unrecognized token '<'` for every run.
15
+
5
16
  ## [0.4.0] - 2026-10-28
6
17
 
7
18
  Commit provenance and team sharing ([RFD0006](docs/rfds/RFD0006-commit-provenance-and-share-server.md)).
@@ -74,7 +85,8 @@ Commit provenance and team sharing ([RFD0006](docs/rfds/RFD0006-commit-provenanc
74
85
  - **Breaking:** the npm package moved to `@leostera/evalkit`. Replace the `@leostera-js/evalkit` dependency and update imports, including `/runner`. Eval definitions and local reports are unchanged.
75
86
  - Dashboard workflow refinements.
76
87
 
77
- [0.4.0]: https://github.com/leostera/evalkit/compare/5e67e72...main
88
+ [0.5.0]: https://github.com/leostera/evalkit/compare/0dd8717...main
89
+ [0.4.0]: https://github.com/leostera/evalkit/compare/5e67e72...0dd8717
78
90
  [0.3.0]: https://github.com/leostera/evalkit/compare/38c34be...5e67e72
79
91
  [0.2.0]: https://github.com/leostera/evalkit/compare/3f8855a...38c34be
80
92
  [0.1.0]: https://github.com/leostera/evalkit/compare/12d5a1e...3f8855a
package/dist/cli.mjs CHANGED
@@ -9152,29 +9152,61 @@ async function runBundle(options) {
9152
9152
  }, { workspaceFiles: options.workspaceFiles });
9153
9153
  return encodeBundle(files);
9154
9154
  }
9155
- /** The server refused the bundle (4xx): retrying the same bundle cannot help. */
9155
+ /** The server refused this bundle (4xx): retrying it cannot help. */
9156
9156
  var UploadRejected = class extends Error {};
9157
+ /**
9158
+ * The URL does not answer as an EvalKit share server (a redirect, an HTML page,
9159
+ * an auth proxy). Every upload would fail the same way, so sharing stops.
9160
+ */
9161
+ var ShareServerError = class extends Error {};
9162
+ /** `share.headers` from the config: called once, checked, never printed. */
9163
+ async function resolveShareHeaders(headers) {
9164
+ let value;
9165
+ try {
9166
+ value = typeof headers === "function" ? await headers() : headers ?? {};
9167
+ } catch (error) {
9168
+ throw new Error(`share.headers failed: ${error instanceof Error ? error.message : String(error)}`);
9169
+ }
9170
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("share.headers must be an object of header names to values");
9171
+ const entries = Object.entries(value);
9172
+ for (const [name, item] of entries) if (typeof item !== "string" || !item) throw new Error(`share.headers "${name}" must be a non-empty string`);
9173
+ return Object.fromEntries(entries);
9174
+ }
9175
+ /** Check that the URL is an EvalKit share server and the headers get through. */
9176
+ async function checkShareServer(serverUrl, headers) {
9177
+ const response = await fetch(new URL("/api/v1/repos", serverUrl), {
9178
+ headers,
9179
+ redirect: "manual"
9180
+ });
9181
+ await expectShareServer(response, serverUrl);
9182
+ await response.body?.cancel();
9183
+ }
9157
9184
  /** POST one bundle, retrying network errors and 5xx responses. */
9158
9185
  async function uploadBundle(serverUrl, bundle, options = {}) {
9159
9186
  const attempts = options.attempts ?? 3;
9160
9187
  let lastError;
9161
9188
  for (let attempt = 1; attempt <= attempts; attempt++) {
9162
9189
  try {
9163
- return await postBundle(serverUrl, bundle);
9190
+ return await postBundle(serverUrl, bundle, options.headers ?? {});
9164
9191
  } catch (error) {
9165
- if (error instanceof UploadRejected) throw error;
9192
+ if (error instanceof UploadRejected || error instanceof ShareServerError) throw error;
9166
9193
  lastError = error;
9167
9194
  }
9168
9195
  if (attempt < attempts) await Bun.sleep((options.delayMs ?? 500) * 2 ** (attempt - 1));
9169
9196
  }
9170
9197
  throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`Upload failed: ${String(lastError)}`);
9171
9198
  }
9172
- async function postBundle(serverUrl, bundle) {
9199
+ async function postBundle(serverUrl, bundle, headers) {
9173
9200
  const response = await fetch(new URL("/api/v1/uploads", serverUrl), {
9174
9201
  method: "POST",
9175
- headers: { "content-type": "application/octet-stream" },
9176
- body: bundle
9202
+ headers: {
9203
+ ...headers,
9204
+ "content-type": "application/octet-stream"
9205
+ },
9206
+ body: bundle,
9207
+ redirect: "manual"
9177
9208
  });
9209
+ await expectShareServer(response, serverUrl);
9178
9210
  if (response.ok) return parseUploadResult(await response.json());
9179
9211
  const reason = await response.text();
9180
9212
  if (response.status < 500) throw new UploadRejected(`Share server rejected the upload: ${reason}`);
@@ -9186,7 +9218,18 @@ function parseUploadResult(value) {
9186
9218
  status: result.status,
9187
9219
  url: result.url
9188
9220
  };
9189
- throw new UploadRejected(`Unexpected share server response: ${JSON.stringify(value)}`);
9221
+ throw new ShareServerError(`Unexpected share server response: ${JSON.stringify(value)}`);
9222
+ }
9223
+ /** Redirects and non-JSON answers come from something in front of the server. */
9224
+ async function expectShareServer(response, serverUrl) {
9225
+ if (response.status >= 300 && response.status < 400) {
9226
+ const location = response.headers.get("location");
9227
+ const target = location ? new URL(location, serverUrl).host : "elsewhere";
9228
+ throw new ShareServerError(`${new URL(serverUrl).host} redirected to ${target} instead of answering. If an auth proxy is in front of the share server, set share.headers in evalkit.config.ts.`);
9229
+ }
9230
+ const type = response.headers.get("content-type") ?? "";
9231
+ const denied = response.status === 401 || response.status === 403;
9232
+ if (denied || response.ok && !type.includes("json")) throw new ShareServerError(`${new URL(serverUrl).host} did not answer as an EvalKit share server (${response.status}${type ? `, ${type.split(";")[0]}` : ""}). ` + (denied ? "Check share.headers." : "Check share.url."));
9190
9233
  }
9191
9234
  async function shareRuns(options) {
9192
9235
  const reports = createDashboardReportReader(options.reportRoot, options.registry);
@@ -9207,10 +9250,11 @@ async function shareRuns(options) {
9207
9250
  run,
9208
9251
  workspaceFiles: options.workspaceFiles
9209
9252
  });
9210
- const uploaded = await uploadBundle(options.serverUrl, bundle);
9253
+ const uploaded = await uploadBundle(options.serverUrl, bundle, { headers: options.headers });
9211
9254
  result.bytes += bundle.byteLength;
9212
9255
  result[uploaded.status] += 1;
9213
9256
  } catch (error) {
9257
+ if (error instanceof ShareServerError) throw error;
9214
9258
  result.failed.push({
9215
9259
  runId: run.id,
9216
9260
  error: error instanceof Error ? error.message : String(error)
@@ -9339,7 +9383,7 @@ scores, evidence, and candidate files (never evaluator workspaces): review it be
9339
9383
  }
9340
9384
  //#endregion
9341
9385
  //#region ../cli/src/new-project.ts
9342
- const npmDependency = "^0.4.0";
9386
+ const npmDependency = "^0.5.0";
9343
9387
  const ignored = [
9344
9388
  "node_modules/",
9345
9389
  "_evalkit-results/",
@@ -9452,7 +9496,7 @@ async function writeStarterReadme(root, name, inPlace) {
9452
9496
  }
9453
9497
  //#endregion
9454
9498
  //#region ../cli/src/index.ts
9455
- const VERSION = "0.4.0";
9499
+ const VERSION = "0.5.0";
9456
9500
  let projectRoot = process.cwd();
9457
9501
  let reportRoot = resolve(projectRoot, "_evalkit-results");
9458
9502
  const sandboxRoot = resolve(projectRoot, "_evalkit-sandbox");
@@ -9891,6 +9935,14 @@ const shareCommand = make$1("share", {
9891
9935
  if (!selection.selected.length) throw new Error(noRunsMessage(selection, head, { all }));
9892
9936
  const result = await shareRuns({
9893
9937
  serverUrl,
9938
+ headers: await step("Server", async () => {
9939
+ const headers = await resolveShareHeaders(project.config.share?.headers);
9940
+ await checkShareServer(serverUrl, headers);
9941
+ return headers;
9942
+ }, (headers) => {
9943
+ const names = Object.keys(headers);
9944
+ return `${new URL(serverUrl).host}${names.length ? dim(` with ${names.join(", ")}`) : ""}`;
9945
+ }),
9894
9946
  reportRoot,
9895
9947
  registry,
9896
9948
  runs: selection.selected,