@forumone/throughline-core 0.9.0 → 0.10.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 +112 -0
- package/README.md +192 -33
- package/bin/payload-cli.mjs +443 -0
- package/bin/throughline-payload.mjs +6 -0
- package/dist/audit/collection.d.ts +3 -0
- package/dist/audit/collection.d.ts.map +1 -1
- package/dist/audit/collection.js +2 -0
- package/dist/audit/collection.js.map +1 -1
- package/dist/env/index.d.ts +54 -0
- package/dist/env/index.d.ts.map +1 -0
- package/dist/env/index.js +113 -0
- package/dist/env/index.js.map +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/api-key-access.d.ts +50 -0
- package/dist/mcp/api-key-access.d.ts.map +1 -0
- package/dist/mcp/api-key-access.js +94 -0
- package/dist/mcp/api-key-access.js.map +1 -0
- package/dist/mcp/envelope.d.ts +6 -0
- package/dist/mcp/envelope.d.ts.map +1 -1
- package/dist/mcp/envelope.js +6 -0
- package/dist/mcp/envelope.js.map +1 -1
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/payload-mcp.d.ts +1 -15
- package/dist/mcp/payload-mcp.d.ts.map +1 -1
- package/dist/mcp/payload-mcp.js +40 -1
- package/dist/mcp/payload-mcp.js.map +1 -1
- package/dist/observability/collection.d.ts +13 -0
- package/dist/observability/collection.d.ts.map +1 -0
- package/dist/observability/collection.js +76 -0
- package/dist/observability/collection.js.map +1 -0
- package/dist/observability/index.d.ts +9 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +5 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/plugin.d.ts +21 -0
- package/dist/observability/plugin.d.ts.map +1 -0
- package/dist/observability/plugin.js +51 -0
- package/dist/observability/plugin.js.map +1 -0
- package/dist/observability/report.d.ts +175 -0
- package/dist/observability/report.d.ts.map +1 -0
- package/dist/observability/report.js +274 -0
- package/dist/observability/report.js.map +1 -0
- package/dist/observability/writer.d.ts +24 -0
- package/dist/observability/writer.d.ts.map +1 -0
- package/dist/observability/writer.js +72 -0
- package/dist/observability/writer.js.map +1 -0
- package/dist/testing/anonymousAccess.d.ts +111 -0
- package/dist/testing/anonymousAccess.d.ts.map +1 -0
- package/dist/testing/anonymousAccess.js +185 -0
- package/dist/testing/anonymousAccess.js.map +1 -0
- package/dist/testing/describeAnonymousAccess.d.ts +26 -0
- package/dist/testing/describeAnonymousAccess.d.ts.map +1 -0
- package/dist/testing/describeAnonymousAccess.js +76 -0
- package/dist/testing/describeAnonymousAccess.js.map +1 -0
- package/dist/testing/index.d.ts +11 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +9 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +30 -7
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The Payload CLI, wrapped so a hung run cannot outlive the shell that started it.
|
|
3
|
+
`bin/throughline-payload.mjs` is the entry point; this module is the runner and
|
|
4
|
+
the pure pieces its tests exercise.
|
|
5
|
+
|
|
6
|
+
`pnpm payload generate:types` is three processes deep — shell, pnpm, node — and
|
|
7
|
+
pnpm (like npm and yarn) does not forward signals to the script child it
|
|
8
|
+
spawns. Kill the shell (a Ctrl-C, an agent tool-call timeout, a closed
|
|
9
|
+
terminal, a cancelled CI step) and pnpm dies while node keeps running,
|
|
10
|
+
reparented to PID 1. When the run was hung rather than merely slow, that orphan
|
|
11
|
+
spins on a core indefinitely: 48 of them piled up on one machine inside a day,
|
|
12
|
+
~90 CPU-minutes each, at a load average of 300.
|
|
13
|
+
|
|
14
|
+
Three layers, because no single one covers every way this exits.
|
|
15
|
+
|
|
16
|
+
1. The child is spawned `detached`, so it leads its own process group. That is
|
|
17
|
+
what makes it killable as a *group* by pid — the whole subtree dies with it,
|
|
18
|
+
and nothing is ever matched by process name, which would put unrelated node
|
|
19
|
+
work on the same machine at risk.
|
|
20
|
+
2. SIGINT, SIGTERM and SIGHUP are forwarded to that group, and a wall-clock
|
|
21
|
+
timeout kills it as well. Between them that covers Ctrl-C, a tool-call
|
|
22
|
+
timeout, and a hangup from a closing terminal.
|
|
23
|
+
3. If this runner is itself SIGKILLed, no handler of ours can run. So each
|
|
24
|
+
spawn is recorded in `.payload-cli-pids` at the workspace root and the
|
|
25
|
+
*next* run (or `--reap`) sweeps the file. See `verdict` for what an entry
|
|
26
|
+
must satisfy before anything is signalled.
|
|
27
|
+
|
|
28
|
+
PAYLOAD_CLI_TIMEOUT_MS sets the wall clock; 0 disables it. The default is five
|
|
29
|
+
minutes, except for the `migrate` family (`migrate`, `migrate:create`, …),
|
|
30
|
+
which gets none: a large migration is legitimately long, and `migrate:create`
|
|
31
|
+
can stop at an interactive prompt. Layers 1 and 3 apply regardless.
|
|
32
|
+
*/
|
|
33
|
+
import { execFile, spawn } from 'node:child_process'
|
|
34
|
+
import { createRequire } from 'node:module'
|
|
35
|
+
import { promisify } from 'node:util'
|
|
36
|
+
import fs from 'node:fs'
|
|
37
|
+
import os from 'node:os'
|
|
38
|
+
import path from 'node:path'
|
|
39
|
+
|
|
40
|
+
const execFileAsync = promisify(execFile)
|
|
41
|
+
|
|
42
|
+
export const PID_FILE_NAME = '.payload-cli-pids'
|
|
43
|
+
export const DEFAULT_TIMEOUT_MS = 300_000
|
|
44
|
+
export const DEFAULT_GRACE_MS = 5_000
|
|
45
|
+
/** Exit code for "gave up waiting", as coreutils `timeout` uses. */
|
|
46
|
+
export const TIMEOUT_EXIT_CODE = 124
|
|
47
|
+
|
|
48
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
49
|
+
const log = (message) => process.stderr.write(`throughline-payload: ${message}\n`)
|
|
50
|
+
|
|
51
|
+
/** Collapse runs of whitespace, so `ps` column padding never decides a match. */
|
|
52
|
+
export const norm = (value) => (value ?? '').trim().replace(/\s+/g, ' ')
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Pure pieces
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* `--reap` alone sweeps and exits. Anything else is passed to the Payload CLI
|
|
60
|
+
* untouched, including flags that look like ours further along the argv.
|
|
61
|
+
*/
|
|
62
|
+
export function parseArgs(argv) {
|
|
63
|
+
if (argv[0] === '--reap') return { mode: 'reap', args: [] }
|
|
64
|
+
return { mode: 'run', args: [...argv] }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `migrate`, `migrate:create`, `migrate:status`, `migrate:fresh`, … */
|
|
68
|
+
export const isMigrateCommand = (args) => /^migrate(:|$)/.test(args[0] ?? '')
|
|
69
|
+
|
|
70
|
+
function readMs(env, name, fallback) {
|
|
71
|
+
const raw = env[name]
|
|
72
|
+
if (raw === undefined || raw.trim() === '') return fallback
|
|
73
|
+
const value = Number(raw)
|
|
74
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
75
|
+
throw new Error(`${name} must be a whole number of milliseconds (0 disables it), got '${raw}'`)
|
|
76
|
+
}
|
|
77
|
+
return value
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The wall clock for a run, in ms; 0 means none. An explicit
|
|
82
|
+
* PAYLOAD_CLI_TIMEOUT_MS always wins. A malformed one is an error rather than
|
|
83
|
+
* silently "no timeout", which is the failure this exists to prevent.
|
|
84
|
+
*/
|
|
85
|
+
export function selectTimeout(args, env) {
|
|
86
|
+
return readMs(env, 'PAYLOAD_CLI_TIMEOUT_MS', isMigrateCommand(args) ? 0 : DEFAULT_TIMEOUT_MS)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** How long a group gets between the polite signal and SIGKILL. */
|
|
90
|
+
export const selectGrace = (env) => readMs(env, 'PAYLOAD_CLI_GRACE_MS', DEFAULT_GRACE_MS)
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The directory that owns the pid file: the nearest ancestor of `cwd` holding
|
|
94
|
+
* `pnpm-workspace.yaml` — the workspace root, so every app in a monorepo
|
|
95
|
+
* shares one file and one `payload:reap` — or, outside a pnpm workspace, the
|
|
96
|
+
* nearest directory with a `package.json`. Real path, because that is what
|
|
97
|
+
* the spawned command line contains.
|
|
98
|
+
*/
|
|
99
|
+
export function findRoot(cwd, exists = fs.existsSync) {
|
|
100
|
+
const real = fs.realpathSync(cwd)
|
|
101
|
+
let nearestPackage
|
|
102
|
+
for (let dir = real; ; dir = path.dirname(dir)) {
|
|
103
|
+
if (exists(path.join(dir, 'pnpm-workspace.yaml'))) return dir
|
|
104
|
+
if (!nearestPackage && exists(path.join(dir, 'package.json'))) nearestPackage = dir
|
|
105
|
+
if (path.dirname(dir) === dir) break
|
|
106
|
+
}
|
|
107
|
+
return nearestPackage ?? real
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const ENTRY_FIELDS = {
|
|
111
|
+
pid: 'number',
|
|
112
|
+
pgid: 'number',
|
|
113
|
+
start: 'string',
|
|
114
|
+
command: 'string',
|
|
115
|
+
runnerPid: 'number',
|
|
116
|
+
runnerStart: 'string',
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* One JSON object per line. A torn line from an abrupt exit, or anything
|
|
121
|
+
* missing a field verification needs, is dropped — never guessed at.
|
|
122
|
+
*/
|
|
123
|
+
export function parsePidFile(text) {
|
|
124
|
+
return text
|
|
125
|
+
.split('\n')
|
|
126
|
+
.filter((line) => line.trim())
|
|
127
|
+
.flatMap((line) => {
|
|
128
|
+
let entry
|
|
129
|
+
try {
|
|
130
|
+
entry = JSON.parse(line)
|
|
131
|
+
} catch {
|
|
132
|
+
return []
|
|
133
|
+
}
|
|
134
|
+
if (!entry || typeof entry !== 'object') return []
|
|
135
|
+
for (const [field, type] of Object.entries(ENTRY_FIELDS)) {
|
|
136
|
+
if (typeof entry[field] !== type) return []
|
|
137
|
+
}
|
|
138
|
+
if (!Number.isInteger(entry.pid) || entry.pid <= 1) return []
|
|
139
|
+
if (entry.pgid !== entry.pid) return [] // We only ever record group leaders.
|
|
140
|
+
return [entry]
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const serializeEntries = (entries) =>
|
|
145
|
+
entries.map((entry) => `${JSON.stringify(entry)}\n`).join('')
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* What the sweep does with one recorded entry, given what `ps` reports now
|
|
149
|
+
* (`undefined` fields mean the pid is gone):
|
|
150
|
+
*
|
|
151
|
+
* 'forget' — the pid is gone, or it is no longer the process we recorded:
|
|
152
|
+
* its start time differs (the OS recycled the pid), its command
|
|
153
|
+
* line differs, or that command line is not rooted in this
|
|
154
|
+
* workspace. Nothing is signalled.
|
|
155
|
+
* 'keep' — it is ours and its runner is still alive: a concurrent run in
|
|
156
|
+
* this workspace, which keeps its child.
|
|
157
|
+
* 'reap' — it is ours and its runner is gone: an orphan.
|
|
158
|
+
*
|
|
159
|
+
* Only 'reap' leads to a signal, and only to the recorded process group.
|
|
160
|
+
*/
|
|
161
|
+
export function verdict(entry, observed, root) {
|
|
162
|
+
if (!observed.start) return 'forget'
|
|
163
|
+
if (norm(observed.start) !== norm(entry.start)) return 'forget'
|
|
164
|
+
if (norm(observed.command) !== norm(entry.command)) return 'forget'
|
|
165
|
+
if (!entry.command.includes(`${root}${path.sep}`)) return 'forget'
|
|
166
|
+
if (observed.runnerStart && norm(observed.runnerStart) === norm(entry.runnerStart)) return 'keep'
|
|
167
|
+
return 'reap'
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 128 + signal number, as a shell reports a signalled exit. */
|
|
171
|
+
export const signalExitCode = (signal) => 128 + (os.constants.signals[signal] ?? 0)
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Processes
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
/*
|
|
178
|
+
`payload/bin.js` is not in the package's `exports`, so it cannot be resolved
|
|
179
|
+
directly. Resolve the package entry from the app's directory — never from this
|
|
180
|
+
package's own install — and walk up to the directory that owns it. The result
|
|
181
|
+
is a real path inside this workspace, which is what makes the spawned argv
|
|
182
|
+
unambiguous: one node process whose command line names this workspace.
|
|
183
|
+
*/
|
|
184
|
+
export function resolvePayloadBin(fromDir) {
|
|
185
|
+
const require = createRequire(path.join(fromDir, 'package.json'))
|
|
186
|
+
let entry
|
|
187
|
+
try {
|
|
188
|
+
entry = require.resolve('payload')
|
|
189
|
+
} catch {
|
|
190
|
+
throw new Error(`could not resolve 'payload' from ${fromDir} — is it a dependency of this app?`)
|
|
191
|
+
}
|
|
192
|
+
for (let dir = path.dirname(entry); ; dir = path.dirname(dir)) {
|
|
193
|
+
const manifest = path.join(dir, 'package.json')
|
|
194
|
+
if (fs.existsSync(manifest)) {
|
|
195
|
+
const { name, bin } = JSON.parse(fs.readFileSync(manifest, 'utf8'))
|
|
196
|
+
if (name === 'payload') {
|
|
197
|
+
const relative = typeof bin === 'string' ? bin : bin?.payload
|
|
198
|
+
if (relative) return fs.realpathSync(path.join(dir, relative))
|
|
199
|
+
break
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (path.dirname(dir) === dir) break
|
|
203
|
+
}
|
|
204
|
+
throw new Error(`could not find the payload CLI's bin from ${fromDir}`)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/*
|
|
208
|
+
One `ps` field for a set of pids, as a map; a pid that is gone is absent. `-ww`
|
|
209
|
+
so a long command line is never truncated. Every process is listed and then
|
|
210
|
+
filtered, rather than asking for the pids with `-p`: one implausible pid in the
|
|
211
|
+
file ("process id too large" on macOS) makes `ps -p` fail for the whole batch,
|
|
212
|
+
which would blind the sweep.
|
|
213
|
+
*/
|
|
214
|
+
async function psField(pids, field) {
|
|
215
|
+
const found = new Map()
|
|
216
|
+
if (!pids.length) return found
|
|
217
|
+
const wanted = new Set(pids)
|
|
218
|
+
let stdout
|
|
219
|
+
try {
|
|
220
|
+
;({ stdout } = await execFileAsync('ps', ['-A', '-ww', '-o', `pid=,${field}=`], {
|
|
221
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
222
|
+
}))
|
|
223
|
+
} catch (error) {
|
|
224
|
+
stdout = error?.stdout ?? ''
|
|
225
|
+
}
|
|
226
|
+
for (const line of stdout.split('\n')) {
|
|
227
|
+
const match = line.match(/^\s*(\d+)\s+(.*)$/)
|
|
228
|
+
if (match && wanted.has(Number(match[1]))) found.set(Number(match[1]), match[2])
|
|
229
|
+
}
|
|
230
|
+
return found
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const groupAlive = (pgid) => {
|
|
234
|
+
try {
|
|
235
|
+
process.kill(-pgid, 0)
|
|
236
|
+
return true
|
|
237
|
+
} catch {
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/*
|
|
243
|
+
`signal` to the group, then SIGKILL to whatever is left after the grace period.
|
|
244
|
+
The wait is on the whole group, not just its leader, so a grandchild that
|
|
245
|
+
ignores the first signal is still caught.
|
|
246
|
+
*/
|
|
247
|
+
async function killGroup(pgid, signal, graceMs) {
|
|
248
|
+
try {
|
|
249
|
+
process.kill(-pgid, signal)
|
|
250
|
+
} catch {
|
|
251
|
+
return // Already gone.
|
|
252
|
+
}
|
|
253
|
+
for (let waited = 0; waited < graceMs; waited += 100) {
|
|
254
|
+
if (!groupAlive(pgid)) return
|
|
255
|
+
await delay(100)
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
process.kill(-pgid, 'SIGKILL')
|
|
259
|
+
} catch {
|
|
260
|
+
// Exited during the grace period.
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function pidFileStore(root) {
|
|
265
|
+
const file = path.join(root, PID_FILE_NAME)
|
|
266
|
+
const read = () => {
|
|
267
|
+
try {
|
|
268
|
+
return parsePidFile(fs.readFileSync(file, 'utf8'))
|
|
269
|
+
} catch {
|
|
270
|
+
return []
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const write = (entries) => {
|
|
274
|
+
if (!entries.length) fs.rmSync(file, { force: true })
|
|
275
|
+
else fs.writeFileSync(file, serializeEntries(entries))
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
read,
|
|
279
|
+
write,
|
|
280
|
+
append: (entry) => fs.appendFileSync(file, serializeEntries([entry])),
|
|
281
|
+
drop: (pid) => write(read().filter((entry) => entry.pid !== pid)),
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** The backstop: reap orphans this workspace recorded. Returns how many. */
|
|
286
|
+
export async function sweep(root, graceMs) {
|
|
287
|
+
const store = pidFileStore(root)
|
|
288
|
+
const entries = store.read()
|
|
289
|
+
if (!entries.length) return 0
|
|
290
|
+
|
|
291
|
+
const pids = [...new Set(entries.flatMap((entry) => [entry.pid, entry.runnerPid]))]
|
|
292
|
+
const [starts, commands] = await Promise.all([psField(pids, 'lstart'), psField(pids, 'args')])
|
|
293
|
+
|
|
294
|
+
const keep = []
|
|
295
|
+
let reaped = 0
|
|
296
|
+
for (const entry of entries) {
|
|
297
|
+
const result = verdict(
|
|
298
|
+
entry,
|
|
299
|
+
{
|
|
300
|
+
start: starts.get(entry.pid),
|
|
301
|
+
command: commands.get(entry.pid),
|
|
302
|
+
runnerStart: starts.get(entry.runnerPid),
|
|
303
|
+
},
|
|
304
|
+
root,
|
|
305
|
+
)
|
|
306
|
+
if (result === 'keep') keep.push(entry)
|
|
307
|
+
if (result !== 'reap') continue
|
|
308
|
+
log(
|
|
309
|
+
`reaping orphaned '${entry.script ?? 'payload'}' (pid ${entry.pid}, started ${entry.at ?? entry.start})`,
|
|
310
|
+
)
|
|
311
|
+
await killGroup(entry.pgid, 'SIGTERM', graceMs)
|
|
312
|
+
reaped += 1
|
|
313
|
+
}
|
|
314
|
+
// Re-read so an entry a concurrent run appended meanwhile is not lost.
|
|
315
|
+
const handled = new Set(entries.map((entry) => entry.pid))
|
|
316
|
+
const kept = new Set(keep.map((entry) => entry.pid))
|
|
317
|
+
store.write(store.read().filter((entry) => !handled.has(entry.pid) || kept.has(entry.pid)))
|
|
318
|
+
return reaped
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export async function main({
|
|
322
|
+
argv = process.argv.slice(2),
|
|
323
|
+
cwd = process.cwd(),
|
|
324
|
+
env = process.env,
|
|
325
|
+
} = {}) {
|
|
326
|
+
let parsed, timeoutMs, graceMs, root
|
|
327
|
+
try {
|
|
328
|
+
parsed = parseArgs(argv)
|
|
329
|
+
timeoutMs = selectTimeout(parsed.args, env)
|
|
330
|
+
graceMs = selectGrace(env)
|
|
331
|
+
root = findRoot(cwd)
|
|
332
|
+
} catch (error) {
|
|
333
|
+
log(error.message)
|
|
334
|
+
process.exit(2)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (process.platform === 'win32') {
|
|
338
|
+
// No process groups and no `ps`: run it plainly rather than not at all.
|
|
339
|
+
if (parsed.mode === 'reap') return
|
|
340
|
+
const child = spawn(process.execPath, [resolvePayloadBin(cwd), ...parsed.args], {
|
|
341
|
+
stdio: 'inherit',
|
|
342
|
+
cwd,
|
|
343
|
+
})
|
|
344
|
+
child.on('exit', (code) => process.exit(code ?? 1))
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const reaped = await sweep(root, graceMs)
|
|
349
|
+
if (parsed.mode === 'reap') {
|
|
350
|
+
log(reaped ? `reaped ${reaped} orphaned run(s)` : 'nothing to reap')
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
let bin
|
|
355
|
+
try {
|
|
356
|
+
bin = resolvePayloadBin(cwd)
|
|
357
|
+
} catch (error) {
|
|
358
|
+
log(error.message)
|
|
359
|
+
process.exit(1)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const store = pidFileStore(root)
|
|
363
|
+
const childArgv = [bin, ...parsed.args]
|
|
364
|
+
const child = spawn(process.execPath, childArgv, { stdio: 'inherit', detached: true, cwd, env })
|
|
365
|
+
|
|
366
|
+
let timer
|
|
367
|
+
let finished = false
|
|
368
|
+
let stopping = false
|
|
369
|
+
const finish = (code) => {
|
|
370
|
+
if (finished) return
|
|
371
|
+
finished = true
|
|
372
|
+
clearTimeout(timer)
|
|
373
|
+
if (child.pid) store.drop(child.pid)
|
|
374
|
+
process.exit(code)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/*
|
|
378
|
+
Kill the group and then exit with `code`. Exiting is deliberately left to
|
|
379
|
+
this, not to the child's 'exit' event: the leader usually dies on the first
|
|
380
|
+
signal, and exiting then would abandon the grace period — and with it the
|
|
381
|
+
SIGKILL for a grandchild that ignored that signal.
|
|
382
|
+
*/
|
|
383
|
+
const stop = async (signal, code) => {
|
|
384
|
+
if (stopping || finished) return
|
|
385
|
+
stopping = true
|
|
386
|
+
if (child.pid) await killGroup(child.pid, signal, graceMs)
|
|
387
|
+
finish(code)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
child.on('error', (error) => {
|
|
391
|
+
log(error.message)
|
|
392
|
+
if (!stopping) finish(1)
|
|
393
|
+
})
|
|
394
|
+
child.on('exit', async (code, signal) => {
|
|
395
|
+
if (stopping) return
|
|
396
|
+
// Payload is done. Anything it left running in its group goes with it.
|
|
397
|
+
if (groupAlive(child.pid)) {
|
|
398
|
+
stopping = true
|
|
399
|
+
await killGroup(child.pid, 'SIGTERM', graceMs)
|
|
400
|
+
}
|
|
401
|
+
finish(signal ? signalExitCode(signal) : (code ?? 0))
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
/*
|
|
405
|
+
The child is in its own group, so a terminal Ctrl-C reaches this runner
|
|
406
|
+
alone. Forwarding the same signal lets Payload clean up as it would have;
|
|
407
|
+
the grace period then escalates to SIGKILL.
|
|
408
|
+
*/
|
|
409
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
410
|
+
process.on(signal, () => stop(signal, signalExitCode(signal)))
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (timeoutMs > 0) {
|
|
414
|
+
timer = setTimeout(() => {
|
|
415
|
+
log(
|
|
416
|
+
`no exit after ${Math.round(timeoutMs / 1000)}s — killing process group ${child.pid}. ` +
|
|
417
|
+
'Set PAYLOAD_CLI_TIMEOUT_MS higher, or 0, if this run is legitimately slow.',
|
|
418
|
+
)
|
|
419
|
+
stop('SIGTERM', TIMEOUT_EXIT_CODE)
|
|
420
|
+
}, timeoutMs)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (!child.pid) return // Spawn failed; 'error' handles it.
|
|
424
|
+
|
|
425
|
+
/*
|
|
426
|
+
Recorded after the spawn, so a run that exits before this lands leaves an
|
|
427
|
+
entry the next sweep forgets on its first condition. The command is the argv
|
|
428
|
+
we spawned, not what `ps` shows this instant — just after fork the child can
|
|
429
|
+
still carry this runner's argv.
|
|
430
|
+
*/
|
|
431
|
+
const starts = await psField([child.pid, process.pid], 'lstart')
|
|
432
|
+
if (finished) return
|
|
433
|
+
store.append({
|
|
434
|
+
pid: child.pid,
|
|
435
|
+
pgid: child.pid, // `detached` makes the child its own group leader.
|
|
436
|
+
start: norm(starts.get(child.pid)),
|
|
437
|
+
command: [process.execPath, ...childArgv].join(' '),
|
|
438
|
+
runnerPid: process.pid,
|
|
439
|
+
runnerStart: norm(starts.get(process.pid)),
|
|
440
|
+
script: parsed.args[0] ?? 'payload',
|
|
441
|
+
at: new Date().toISOString(),
|
|
442
|
+
})
|
|
443
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Access, CollectionConfig } from 'payload';
|
|
2
|
+
import { type PluginAdminOptions } from '@forumone/throughline-plugin-contract';
|
|
2
3
|
export interface AuditCollectionOptions {
|
|
3
4
|
/** Override the collection slug. Default: `'audit-events'`. */
|
|
4
5
|
slug?: string;
|
|
@@ -7,6 +8,8 @@ export interface AuditCollectionOptions {
|
|
|
7
8
|
* Defaults to admin/editor roles.
|
|
8
9
|
*/
|
|
9
10
|
readAccess?: Access;
|
|
11
|
+
/** Sidebar placement. Default: the `'Throughline'` group; `group: false` leaves it ungrouped. */
|
|
12
|
+
admin?: PluginAdminOptions;
|
|
10
13
|
}
|
|
11
14
|
export declare const DEFAULT_AUDIT_SLUG = "audit-events";
|
|
12
15
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../../src/audit/collection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../../src/audit/collection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AACvD,OAAO,EAAE,KAAK,kBAAkB,EAAqB,MAAM,uCAAuC,CAAA;AAGlG,MAAM,WAAW,sBAAsB;IACrC,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iGAAiG;IACjG,KAAK,CAAC,EAAE,kBAAkB,CAAA;CAC3B;AAED,eAAO,MAAM,kBAAkB,iBAAiB,CAAA;AAOhD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,sBAA2B,GAAG,gBAAgB,CAgG5F"}
|
package/dist/audit/collection.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveAdminGroup } from '@forumone/throughline-plugin-contract';
|
|
1
2
|
import { AUDIT_ACTIONS, AUDIT_MCP_SERVERS } from './types.js';
|
|
2
3
|
export const DEFAULT_AUDIT_SLUG = 'audit-events';
|
|
3
4
|
const defaultReadAccess = ({ req }) => {
|
|
@@ -15,6 +16,7 @@ export function createAuditCollection(options = {}) {
|
|
|
15
16
|
return {
|
|
16
17
|
slug,
|
|
17
18
|
admin: {
|
|
19
|
+
...resolveAdminGroup(options.admin),
|
|
18
20
|
useAsTitle: 'summary',
|
|
19
21
|
defaultColumns: ['createdAt', 'actor', 'action', 'targetCollection', 'targetId'],
|
|
20
22
|
description: 'Immutable record of every consequential action in the system. Read-only through the admin.',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection.js","sourceRoot":"","sources":["../../src/audit/collection.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"collection.js","sourceRoot":"","sources":["../../src/audit/collection.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { EnvRequirement } from '@forumone/throughline-plugin-contract';
|
|
2
|
+
/** What is wrong with one value. `undefined` from `checkEnvValue` means nothing. */
|
|
3
|
+
export type EnvValueProblem = {
|
|
4
|
+
kind: 'missing';
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'too-short';
|
|
7
|
+
length: number;
|
|
8
|
+
minLength: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Checks one value against one requirement, without saying where the value
|
|
12
|
+
* came from. A plugin calls it on `options.x ?? process.env[requirement.name]`
|
|
13
|
+
* and words the error its own way; `assertEnvironment` calls it on the
|
|
14
|
+
* environment alone.
|
|
15
|
+
*
|
|
16
|
+
* Empty and whitespace-only count as missing. The length is the value's own,
|
|
17
|
+
* untrimmed, which is what the code that uses it will see.
|
|
18
|
+
*/
|
|
19
|
+
export declare function checkEnvValue(requirement: EnvRequirement, value: string | undefined): EnvValueProblem | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Anything `assertEnvironment` accepts: a requirement, a plugin's list of
|
|
22
|
+
* them, or a function that throws when something is wrong — for a rule that is
|
|
23
|
+
* not "this name, this long", such as a database URL that may arrive under any
|
|
24
|
+
* of several names. The thrown message becomes the problem's line, so it must
|
|
25
|
+
* not contain the value either.
|
|
26
|
+
*/
|
|
27
|
+
export type EnvironmentCheck = EnvRequirement | readonly EnvRequirement[] | (() => unknown);
|
|
28
|
+
/** The error `assertEnvironment` throws. `problems` holds one line per problem. */
|
|
29
|
+
export declare class EnvironmentError extends Error {
|
|
30
|
+
readonly problems: readonly string[];
|
|
31
|
+
constructor(problems: readonly string[]);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Checks every requirement against `process.env` and throws one
|
|
35
|
+
* {@link EnvironmentError} listing every missing or too-short value, each with
|
|
36
|
+
* its `why`. Values are never printed — a too-short one is reported by length.
|
|
37
|
+
*
|
|
38
|
+
* Call it first in `payload.config.ts`, at module scope, with every plugin's
|
|
39
|
+
* list and the site's own variables:
|
|
40
|
+
*
|
|
41
|
+
* ```ts
|
|
42
|
+
* assertEnvironment(
|
|
43
|
+
* approvalsEnv,
|
|
44
|
+
* emailEnv,
|
|
45
|
+
* formsEnv,
|
|
46
|
+
* { name: 'PAYLOAD_SECRET', minLength: 32, why: 'Signs Payload sessions.' },
|
|
47
|
+
* () => databaseConnectionString(),
|
|
48
|
+
* )
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* A name listed twice is reported once, against the strictest `minLength`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function assertEnvironment(...checks: readonly EnvironmentCheck[]): void;
|
|
54
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/env/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAA;AAoB3E,oFAAoF;AACpF,MAAM,MAAM,eAAe,GACzB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAA;AAEhF;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,cAAc,EAC3B,KAAK,EAAE,MAAM,GAAG,SAAS,GACxB,eAAe,GAAG,SAAS,CAO7B;AAED;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG,cAAc,GAAG,SAAS,cAAc,EAAE,GAAG,CAAC,MAAM,OAAO,CAAC,CAAA;AAE3F,mFAAmF;AACnF,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;gBAExB,QAAQ,EAAE,SAAS,MAAM,EAAE;CAkBxC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,MAAM,EAAE,SAAS,gBAAgB,EAAE,GAAG,IAAI,CA2C9E"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks one value against one requirement, without saying where the value
|
|
3
|
+
* came from. A plugin calls it on `options.x ?? process.env[requirement.name]`
|
|
4
|
+
* and words the error its own way; `assertEnvironment` calls it on the
|
|
5
|
+
* environment alone.
|
|
6
|
+
*
|
|
7
|
+
* Empty and whitespace-only count as missing. The length is the value's own,
|
|
8
|
+
* untrimmed, which is what the code that uses it will see.
|
|
9
|
+
*/
|
|
10
|
+
export function checkEnvValue(requirement, value) {
|
|
11
|
+
if (value === undefined || value.trim() === '')
|
|
12
|
+
return { kind: 'missing' };
|
|
13
|
+
const { minLength } = requirement;
|
|
14
|
+
if (minLength !== undefined && value.length < minLength) {
|
|
15
|
+
return { kind: 'too-short', length: value.length, minLength };
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
/** The error `assertEnvironment` throws. `problems` holds one line per problem. */
|
|
20
|
+
export class EnvironmentError extends Error {
|
|
21
|
+
problems;
|
|
22
|
+
constructor(problems) {
|
|
23
|
+
const count = problems.length;
|
|
24
|
+
super([
|
|
25
|
+
`Configuration problem: ${count} environment variable${count === 1 ? '' : 's'} ` +
|
|
26
|
+
`${count === 1 ? 'is' : 'are'} missing or invalid. This is not a code or build ` +
|
|
27
|
+
`failure; nothing will start until ${count === 1 ? 'it is' : 'they are'} set.`,
|
|
28
|
+
'',
|
|
29
|
+
...problems.map((line) => ` - ${line}`),
|
|
30
|
+
'',
|
|
31
|
+
"Set each in the environment this process runs in (.env locally, your host's " +
|
|
32
|
+
'settings when deployed). One that is set and still listed is not reaching the ' +
|
|
33
|
+
'process: a turbo task passes only the variables its turbo.json `env` declares.',
|
|
34
|
+
].join('\n'));
|
|
35
|
+
this.name = 'EnvironmentError';
|
|
36
|
+
this.problems = problems;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Checks every requirement against `process.env` and throws one
|
|
41
|
+
* {@link EnvironmentError} listing every missing or too-short value, each with
|
|
42
|
+
* its `why`. Values are never printed — a too-short one is reported by length.
|
|
43
|
+
*
|
|
44
|
+
* Call it first in `payload.config.ts`, at module scope, with every plugin's
|
|
45
|
+
* list and the site's own variables:
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* assertEnvironment(
|
|
49
|
+
* approvalsEnv,
|
|
50
|
+
* emailEnv,
|
|
51
|
+
* formsEnv,
|
|
52
|
+
* { name: 'PAYLOAD_SECRET', minLength: 32, why: 'Signs Payload sessions.' },
|
|
53
|
+
* () => databaseConnectionString(),
|
|
54
|
+
* )
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* A name listed twice is reported once, against the strictest `minLength`.
|
|
58
|
+
*/
|
|
59
|
+
export function assertEnvironment(...checks) {
|
|
60
|
+
const requirements = new Map();
|
|
61
|
+
const problems = [];
|
|
62
|
+
for (const check of checks) {
|
|
63
|
+
if (typeof check === 'function') {
|
|
64
|
+
try {
|
|
65
|
+
check();
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
problems.push(error instanceof Error ? error.message : String(error));
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
for (const requirement of isList(check) ? check : [check]) {
|
|
73
|
+
const seen = requirements.get(requirement.name);
|
|
74
|
+
if (!seen) {
|
|
75
|
+
requirements.set(requirement.name, {
|
|
76
|
+
...(requirement.minLength !== undefined ? { minLength: requirement.minLength } : {}),
|
|
77
|
+
whys: [requirement.why],
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (requirement.minLength !== undefined) {
|
|
82
|
+
seen.minLength = Math.max(seen.minLength ?? 0, requirement.minLength);
|
|
83
|
+
}
|
|
84
|
+
if (!seen.whys.includes(requirement.why))
|
|
85
|
+
seen.whys.push(requirement.why);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Requirements first, in the order given; then the free-form checks.
|
|
89
|
+
const lines = [];
|
|
90
|
+
for (const [name, { minLength, whys }] of requirements) {
|
|
91
|
+
const requirement = {
|
|
92
|
+
name,
|
|
93
|
+
why: whys.join(' '),
|
|
94
|
+
...(minLength !== undefined ? { minLength } : {}),
|
|
95
|
+
};
|
|
96
|
+
const problem = checkEnvValue(requirement, process.env[name]);
|
|
97
|
+
if (problem)
|
|
98
|
+
lines.push(describe(requirement, problem));
|
|
99
|
+
}
|
|
100
|
+
lines.push(...problems);
|
|
101
|
+
if (lines.length > 0)
|
|
102
|
+
throw new EnvironmentError(lines);
|
|
103
|
+
}
|
|
104
|
+
function isList(check) {
|
|
105
|
+
return Array.isArray(check);
|
|
106
|
+
}
|
|
107
|
+
function describe(requirement, problem) {
|
|
108
|
+
const what = problem.kind === 'missing'
|
|
109
|
+
? `${requirement.name} is not set.`
|
|
110
|
+
: `${requirement.name} is ${problem.length} characters; the minimum is ${problem.minLength}.`;
|
|
111
|
+
return `${what} ${requirement.why}`;
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/env/index.ts"],"names":[],"mappings":"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"}
|