@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +192 -33
  3. package/bin/payload-cli.mjs +443 -0
  4. package/bin/throughline-payload.mjs +6 -0
  5. package/dist/audit/collection.d.ts +3 -0
  6. package/dist/audit/collection.d.ts.map +1 -1
  7. package/dist/audit/collection.js +2 -0
  8. package/dist/audit/collection.js.map +1 -1
  9. package/dist/env/index.d.ts +54 -0
  10. package/dist/env/index.d.ts.map +1 -0
  11. package/dist/env/index.js +113 -0
  12. package/dist/env/index.js.map +1 -0
  13. package/dist/index.d.ts +6 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +3 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/mcp/api-key-access.d.ts +50 -0
  18. package/dist/mcp/api-key-access.d.ts.map +1 -0
  19. package/dist/mcp/api-key-access.js +94 -0
  20. package/dist/mcp/api-key-access.js.map +1 -0
  21. package/dist/mcp/envelope.d.ts +6 -0
  22. package/dist/mcp/envelope.d.ts.map +1 -1
  23. package/dist/mcp/envelope.js +6 -0
  24. package/dist/mcp/envelope.js.map +1 -1
  25. package/dist/mcp/index.d.ts +1 -0
  26. package/dist/mcp/index.d.ts.map +1 -1
  27. package/dist/mcp/index.js +1 -0
  28. package/dist/mcp/index.js.map +1 -1
  29. package/dist/mcp/payload-mcp.d.ts +1 -15
  30. package/dist/mcp/payload-mcp.d.ts.map +1 -1
  31. package/dist/mcp/payload-mcp.js +40 -1
  32. package/dist/mcp/payload-mcp.js.map +1 -1
  33. package/dist/observability/collection.d.ts +13 -0
  34. package/dist/observability/collection.d.ts.map +1 -0
  35. package/dist/observability/collection.js +76 -0
  36. package/dist/observability/collection.js.map +1 -0
  37. package/dist/observability/index.d.ts +9 -0
  38. package/dist/observability/index.d.ts.map +1 -0
  39. package/dist/observability/index.js +5 -0
  40. package/dist/observability/index.js.map +1 -0
  41. package/dist/observability/plugin.d.ts +21 -0
  42. package/dist/observability/plugin.d.ts.map +1 -0
  43. package/dist/observability/plugin.js +51 -0
  44. package/dist/observability/plugin.js.map +1 -0
  45. package/dist/observability/report.d.ts +175 -0
  46. package/dist/observability/report.d.ts.map +1 -0
  47. package/dist/observability/report.js +274 -0
  48. package/dist/observability/report.js.map +1 -0
  49. package/dist/observability/writer.d.ts +24 -0
  50. package/dist/observability/writer.d.ts.map +1 -0
  51. package/dist/observability/writer.js +72 -0
  52. package/dist/observability/writer.js.map +1 -0
  53. package/dist/testing/anonymousAccess.d.ts +111 -0
  54. package/dist/testing/anonymousAccess.d.ts.map +1 -0
  55. package/dist/testing/anonymousAccess.js +185 -0
  56. package/dist/testing/anonymousAccess.js.map +1 -0
  57. package/dist/testing/describeAnonymousAccess.d.ts +26 -0
  58. package/dist/testing/describeAnonymousAccess.d.ts.map +1 -0
  59. package/dist/testing/describeAnonymousAccess.js +76 -0
  60. package/dist/testing/describeAnonymousAccess.js.map +1 -0
  61. package/dist/testing/index.d.ts +11 -0
  62. package/dist/testing/index.d.ts.map +1 -0
  63. package/dist/testing/index.js +9 -0
  64. package/dist/testing/index.js.map +1 -0
  65. 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
+ }
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ // The Payload CLI, run in its own process group under a wall clock, with a
3
+ // sweep for runs a killed shell left behind. See ./payload-cli.mjs.
4
+ import { main } from './payload-cli.mjs'
5
+
6
+ await main()
@@ -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;AAGvD,MAAM,WAAW,sBAAsB;IACrC,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,eAAO,MAAM,kBAAkB,iBAAiB,CAAA;AAOhD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,sBAA2B,GAAG,gBAAgB,CA+F5F"}
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"}
@@ -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":"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"}
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":"AAwBA;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAC3B,WAA2B,EAC3B,KAAyB;IAEzB,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAA;IAC1E,MAAM,EAAE,SAAS,EAAE,GAAG,WAAW,CAAA;IACjC,IAAI,SAAS,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC;QACxD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,CAAA;IAC/D,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAWD,mFAAmF;AACnF,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAChC,QAAQ,CAAmB;IAEpC,YAAY,QAA2B;QACrC,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAA;QAC7B,KAAK,CACH;YACE,0BAA0B,KAAK,wBAAwB,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG;gBAC9E,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,mDAAmD;gBAChF,qCAAqC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,OAAO;YAChF,EAAE;YACF,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,EAAE,CAAC;YACxC,EAAE;YACF,8EAA8E;gBAC5E,gFAAgF;gBAChF,gFAAgF;SACnF,CAAC,IAAI,CAAC,IAAI,CAAC,CACb,CAAA;QACD,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAA;QAC9B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;IAC1B,CAAC;CACF;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAG,MAAmC;IACtE,MAAM,YAAY,GAAG,IAAI,GAAG,EAAkD,CAAA;IAC9E,MAAM,QAAQ,GAAa,EAAE,CAAA;IAE7B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,OAAO,KAAK,KAAK,UAAU,EAAE,CAAC;YAChC,IAAI,CAAC;gBACH,KAAK,EAAE,CAAA;YACT,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,QAAQ,CAAC,IAAI,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;YACvE,CAAC;YACD,SAAQ;QACV,CAAC;QACD,KAAK,MAAM,WAAW,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1D,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;YAC/C,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE;oBACjC,GAAG,CAAC,WAAW,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,WAAW,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBACpF,IAAI,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC;iBACxB,CAAC,CAAA;gBACF,SAAQ;YACV,CAAC;YACD,IAAI,WAAW,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;gBACxC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,EAAE,WAAW,CAAC,SAAS,CAAC,CAAA;YACvE,CAAC;YACD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC;gBAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;QAC3E,CAAC;IACH,CAAC;IAED,qEAAqE;IACrE,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,IAAI,YAAY,EAAE,CAAC;QACvD,MAAM,WAAW,GAAmB;YAClC,IAAI;YACJ,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;YACnB,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAClD,CAAA;QACD,MAAM,OAAO,GAAG,aAAa,CAAC,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA;QAC7D,IAAI,OAAO;YAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAA;IACzD,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAA;IAEvB,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,IAAI,gBAAgB,CAAC,KAAK,CAAC,CAAA;AACzD,CAAC;AAED,SAAS,MAAM,CAAC,KAAuB;IACrC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;AAC7B,CAAC;AAED,SAAS,QAAQ,CAAC,WAA2B,EAAE,OAAwB;IACrE,MAAM,IAAI,GACR,OAAO,CAAC,IAAI,KAAK,SAAS;QACxB,CAAC,CAAC,GAAG,WAAW,CAAC,IAAI,cAAc;QACnC,CAAC,CAAC,GAAG,WAAW,CAAC,IAAI,OAAO,OAAO,CAAC,MAAM,+BAA+B,OAAO,CAAC,SAAS,GAAG,CAAA;IACjG,OAAO,GAAG,IAAI,IAAI,WAAW,CAAC,GAAG,EAAE,CAAA;AACrC,CAAC"}