@lchase/kiln 0.8.0 → 0.9.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/dist/redact.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Masks secrets in text Kiln writes to the event log, the ledger, and a
3
+ * pull request body. One function owns that. A log or a PR is public-shaped
4
+ * even when a test command or a check excerpt is not.
5
+ *
6
+ * Token shapes cover the common issued-credential prefixes. A value is also
7
+ * masked when it belongs to an env assignment, a JSON field, or an auth
8
+ * header whose name contains TOKEN, KEY, SECRET, or PASSWORD.
9
+ */
10
+ const SECRET_NAME = /token|key|secret|password/i;
11
+ const TOKEN_SHAPES = [
12
+ /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b/g,
13
+ /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
14
+ /\bglpat-[A-Za-z0-9\-_]{16,}\b/g,
15
+ /\bxox[baprs]-[A-Za-z0-9-]{10,}/g,
16
+ /\bAKIA[0-9A-Z]{16}\b/g,
17
+ /\bsk-ant-[A-Za-z0-9\-_]{10,}\b/g,
18
+ /\bsk-[A-Za-z0-9]{20,}\b/g,
19
+ /\bnpm_[A-Za-z0-9]{20,}\b/g,
20
+ /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
21
+ ];
22
+ const AUTH_HEADER = /\b(authorization|proxy-authorization|x-api-key|x-auth-token)\s*:[^\n]*/gi;
23
+ const BEARER = /\bBearer\s+[A-Za-z0-9\-._~+/]{8,}={0,2}/g;
24
+ const ENV_ASSIGN = /(^|[\n\r;\s])((?:export[ \t]+)?)([A-Za-z_][A-Za-z0-9_]*)([ \t]*=[ \t]*)("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\s#]+)/g;
25
+ const JSON_FIELD = /"([A-Za-z_][A-Za-z0-9_]*)"(\s*:\s*)("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g;
26
+ /** Replace secret-shaped text with `[redacted]`. Idempotent on its own output. */
27
+ export function redact(text) {
28
+ let out = text.replace(ENV_ASSIGN, (full, lead, prefix, name, eq, value) => {
29
+ if (!SECRET_NAME.test(name))
30
+ return full;
31
+ return `${lead}${prefix}${name}${eq}[redacted]`;
32
+ });
33
+ out = out.replace(JSON_FIELD, (full, name, mid, value) => {
34
+ if (!SECRET_NAME.test(name))
35
+ return full;
36
+ const quote = value.startsWith("'") ? "'" : '"';
37
+ return `"${name}"${mid}${quote}[redacted]${quote}`;
38
+ });
39
+ out = out.replace(AUTH_HEADER, (_full, name) => `${name}: [redacted]`);
40
+ out = out.replace(BEARER, "Bearer [redacted]");
41
+ for (const pattern of TOKEN_SHAPES) {
42
+ out = out.replace(pattern, "[redacted]");
43
+ }
44
+ return out;
45
+ }
46
+ /** Walk a JSON value and redact every string. Object keys that name a secret drop the value. */
47
+ export function redactDeep(value) {
48
+ if (typeof value === "string")
49
+ return redact(value);
50
+ if (typeof value !== "object" || value === null)
51
+ return value;
52
+ if (Array.isArray(value))
53
+ return value.map((item) => redactDeep(item));
54
+ const out = {};
55
+ for (const [key, inner] of Object.entries(value)) {
56
+ if (typeof inner === "string" && SECRET_NAME.test(key)) {
57
+ out[key] = "[redacted]";
58
+ continue;
59
+ }
60
+ out[key] = redactDeep(inner);
61
+ }
62
+ return out;
63
+ }
@@ -4,6 +4,7 @@ import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readdirSync
4
4
  import { dirname, join, resolve, sep } from "node:path";
5
5
  import { z } from "zod";
6
6
  import { authenticationFailureLine } from "./auth.js";
7
+ import { appendEvent } from "./events.js";
7
8
  import { jobBasename } from "./paths.js";
8
9
  import { FAILURE_REASONS } from "./reconcile.js";
9
10
  /**
@@ -105,6 +106,17 @@ export function recoverRuns(config, jobs, options) {
105
106
  const recorded = recordFailure(config, job.jobId, existing, pending, options.now);
106
107
  if (recorded.created) {
107
108
  appendLog(job.logPath, recorded.ledger.attempts.at(-1));
109
+ const runId = pending.runId ?? job.run?.runId;
110
+ appendEvent(config.kilnDir, {
111
+ job_id: job.jobId,
112
+ run_id: runId ?? null,
113
+ event: "run.failed",
114
+ properties: {
115
+ reason: pending.reason,
116
+ ...(pending.detail ? { detail: pending.detail } : {}),
117
+ once: runId ? `run.failed:${runId}` : `run.failed:launch:${recorded.ledger.attempts.length}`,
118
+ },
119
+ });
108
120
  notes.push({
109
121
  jobId: job.jobId,
110
122
  kind: kindFor(pending.reason),
@@ -115,8 +127,17 @@ export function recoverRuns(config, jobs, options) {
115
127
  }
116
128
  const checkout = job.run?.checkout ?? config.repos[job.repo]?.path;
117
129
  const runId = pending.runId ?? job.run?.runId;
118
- if (checkout && runId)
119
- cleanupRunWorktree(checkout, runId);
130
+ if (checkout && runId) {
131
+ const outcome = cleanupRunWorktree(checkout, runId);
132
+ if (outcome === "removed" || outcome === "kept") {
133
+ appendEvent(config.kilnDir, {
134
+ job_id: job.jobId,
135
+ run_id: runId,
136
+ event: outcome === "removed" ? "worktree.cleaned" : "worktree.kept",
137
+ properties: { once: `worktree.${outcome}:${runId}` },
138
+ });
139
+ }
140
+ }
120
141
  }
121
142
  return notes;
122
143
  }
package/dist/serve.js CHANGED
@@ -7,8 +7,9 @@ import { AUTH_PREFLIGHT_PREFIX } from "./auth.js";
7
7
  import { loadKilnConfig } from "./config.js";
8
8
  import { answerJob, readStatus, retryJob, runTick, startupRecovery } from "./driver.js";
9
9
  import { browse } from "./fs-browse.js";
10
- import { defaultHost } from "./host.js";
10
+ import { appendEvent, eventsForJob } from "./events.js";
11
11
  import { INVESTIGATION_WORKFLOW } from "./findings.js";
12
+ import { defaultHost } from "./host.js";
12
13
  import { intakeJobs } from "./intake-http.js";
13
14
  import { packageRoot } from "./paths.js";
14
15
  import { SANDBOX_PREFLIGHT_PREFIX } from "./sandbox.js";
@@ -189,6 +190,15 @@ async function handle(req, res, state, boundPort, css, csrf) {
189
190
  sendJson(res, 200, statusDocument(state), secrets);
190
191
  return;
191
192
  }
193
+ if (path === "/api/events") {
194
+ const jobId = url.searchParams.get("job_id");
195
+ if (!jobId || !JOB_ID.test(jobId)) {
196
+ sendJson(res, 400, { error: "job_id is required." }, secrets);
197
+ return;
198
+ }
199
+ sendJson(res, 200, { job_id: jobId, events: eventsForJob(config.kilnDir, jobId) }, secrets);
200
+ return;
201
+ }
192
202
  const apiJob = /^\/api\/jobs\/([^/]+)$/.exec(path);
193
203
  if (apiJob?.[1]) {
194
204
  const jobId = parseJobId(apiJob[1]);
@@ -503,6 +513,11 @@ function refreshLiveConfig(state) {
503
513
  state.configError = undefined;
504
514
  state.mtimeMs = file.mtimeMs;
505
515
  state.size = file.size;
516
+ appendEvent(next.kilnDir, {
517
+ job_id: null,
518
+ event: "config.reloaded",
519
+ properties: {},
520
+ });
506
521
  }
507
522
  catch (error) {
508
523
  const message = error instanceof Error ? error.message : String(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lchase/kiln",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Run a workflow on any registered repo. A markdown spec is the input.",
5
5
  "license": "MIT",
6
6
  "type": "module",