@forumone/throughline-core 0.9.1 → 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 +77 -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/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/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,274 @@
|
|
|
1
|
+
import { defaultLogger } from '../logger/index.js';
|
|
2
|
+
/*
|
|
3
|
+
What an error looks like when it leaves the application, and how it leaves.
|
|
4
|
+
|
|
5
|
+
Provider-agnostic on purpose. Which error tracker a site pays for is the site's
|
|
6
|
+
decision, so this defines the *shape* of a report and posts it to whatever URL
|
|
7
|
+
it is given — a log drain, an alerting endpoint, a Slack incoming webhook, a
|
|
8
|
+
small proxy in front of Sentry. Swapping in an SDK later replaces the reporter
|
|
9
|
+
and nothing that builds reports.
|
|
10
|
+
|
|
11
|
+
Three rules hold everywhere in this file:
|
|
12
|
+
|
|
13
|
+
- **Nothing here throws.** A reporter runs because something already went
|
|
14
|
+
wrong. One that throws turns a handled error into an unhandled one, and in an
|
|
15
|
+
Inngest `onFailure` handler it puts the run back in the retry queue for a
|
|
16
|
+
reason that has nothing to do with the original fault.
|
|
17
|
+
- **Request headers are an allowlist.** Next's `onRequestError` hands over the
|
|
18
|
+
request's headers entire, and those include `cookie` — on an admin's request,
|
|
19
|
+
a live Payload session. A report that copied headers wholesale would post a
|
|
20
|
+
credential to a third party. A handful of headers that are never secret are
|
|
21
|
+
copied, and a short list that always is a credential can never be copied,
|
|
22
|
+
whatever a caller passes.
|
|
23
|
+
- **Every report has a `text`.** Slack's incoming webhooks refuse a body
|
|
24
|
+
without one, and a delivery failure is swallowed here by design — so a Slack
|
|
25
|
+
URL without it would read as configured and deliver nothing. Other receivers
|
|
26
|
+
ignore the extra key.
|
|
27
|
+
*/
|
|
28
|
+
/** Request headers copied into a report unless the caller names its own list. */
|
|
29
|
+
export const DEFAULT_REPORTED_HEADERS = [
|
|
30
|
+
'x-request-id',
|
|
31
|
+
'x-vercel-id',
|
|
32
|
+
'user-agent',
|
|
33
|
+
'referer',
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* Headers that are never copied, even when a caller lists them. Each one is,
|
|
37
|
+
* or routinely carries, a credential. `x-forwarded-for` is here too: the whole
|
|
38
|
+
* address is personal data, and a report carries only its prefix.
|
|
39
|
+
*/
|
|
40
|
+
export const NEVER_REPORTED_HEADERS = [
|
|
41
|
+
'authorization',
|
|
42
|
+
'proxy-authorization',
|
|
43
|
+
'cookie',
|
|
44
|
+
'set-cookie',
|
|
45
|
+
'x-api-key',
|
|
46
|
+
'x-forwarded-for',
|
|
47
|
+
];
|
|
48
|
+
/** The environment variable the default reporter reads. */
|
|
49
|
+
export const ERROR_WEBHOOK_URL_ENV = 'ERROR_WEBHOOK_URL';
|
|
50
|
+
const DEFAULT_TIMEOUT_MS = 3_000;
|
|
51
|
+
/**
|
|
52
|
+
* The deployment a report came from, read from the environment.
|
|
53
|
+
*
|
|
54
|
+
* `VERCEL_ENV` before `NODE_ENV`, because on Vercel a preview deployment is
|
|
55
|
+
* `NODE_ENV=production` too, and "production or somebody's branch" is the
|
|
56
|
+
* first question about an alert. Off Vercel, `NODE_ENV` is what there is.
|
|
57
|
+
*/
|
|
58
|
+
export function currentDeployment(env = process.env) {
|
|
59
|
+
const environment = nonEmpty(env['VERCEL_ENV']) ?? nonEmpty(env['NODE_ENV']);
|
|
60
|
+
const commit = nonEmpty(env['VERCEL_GIT_COMMIT_SHA']);
|
|
61
|
+
return {
|
|
62
|
+
...(environment ? { environment } : {}),
|
|
63
|
+
...(commit ? { commit: commit.slice(0, 12) } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* An IPv4 or IPv6 address reduced to a prefix: enough to tell one host's burst
|
|
68
|
+
* from many readers hitting the same fault, and not enough to identify anyone.
|
|
69
|
+
* `x-forwarded-for` may carry a list; only the first entry is the client.
|
|
70
|
+
*/
|
|
71
|
+
export function clientPrefix(forwardedFor) {
|
|
72
|
+
if (!forwardedFor)
|
|
73
|
+
return undefined;
|
|
74
|
+
const first = forwardedFor.split(',')[0]?.trim();
|
|
75
|
+
if (!first)
|
|
76
|
+
return undefined;
|
|
77
|
+
if (first.includes(':')) {
|
|
78
|
+
const groups = first.split(':').filter(Boolean).slice(0, 4);
|
|
79
|
+
return groups.length > 0 ? `${groups.join(':')}::/64` : undefined;
|
|
80
|
+
}
|
|
81
|
+
const octets = first.split('.');
|
|
82
|
+
return octets.length === 4 ? `${octets[0]}.${octets[1]}.0.0/16` : undefined;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build a report for an error captured during a request. Pure, so the
|
|
86
|
+
* redaction is testable without a server.
|
|
87
|
+
*
|
|
88
|
+
* `error` is `unknown` because Next types it that way and means it: the value
|
|
89
|
+
* reaching `onRequestError` may have been processed by React rather than being
|
|
90
|
+
* the instance that was thrown, which is why `digest` is read separately.
|
|
91
|
+
*/
|
|
92
|
+
export function buildRequestErrorReport(error, request, context = {}, options = {}) {
|
|
93
|
+
const prefix = clientPrefix(headerValue(request.headers, 'x-forwarded-for'));
|
|
94
|
+
const headers = {};
|
|
95
|
+
for (const name of allowedHeaders(options.headers)) {
|
|
96
|
+
const value = headerValue(request.headers, name);
|
|
97
|
+
if (value)
|
|
98
|
+
headers[name] = value;
|
|
99
|
+
}
|
|
100
|
+
const routeContext = {};
|
|
101
|
+
for (const key of ['routePath', 'routeType', 'renderSource', 'revalidateReason']) {
|
|
102
|
+
const value = context[key];
|
|
103
|
+
if (typeof value === 'string')
|
|
104
|
+
routeContext[key] = value;
|
|
105
|
+
}
|
|
106
|
+
const digest = typeof error === 'object' && error !== null && 'digest' in error
|
|
107
|
+
? String(error.digest)
|
|
108
|
+
: undefined;
|
|
109
|
+
return {
|
|
110
|
+
kind: 'request',
|
|
111
|
+
at: (options.now ?? new Date()).toISOString(),
|
|
112
|
+
...describeError(error),
|
|
113
|
+
...(digest ? { digest } : {}),
|
|
114
|
+
request: {
|
|
115
|
+
path: request.path,
|
|
116
|
+
method: request.method,
|
|
117
|
+
headers,
|
|
118
|
+
...(prefix ? { clientPrefix: prefix } : {}),
|
|
119
|
+
},
|
|
120
|
+
context: routeContext,
|
|
121
|
+
deployment: options.deployment ?? currentDeployment(),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Build a report for a job that ran out of retries.
|
|
126
|
+
*
|
|
127
|
+
* The stack is included: this goes to a webhook the team owns, where a stack
|
|
128
|
+
* is the most useful field in it. The job-failures collection, which admins
|
|
129
|
+
* read through the CMS, stores the message and not the stack.
|
|
130
|
+
*/
|
|
131
|
+
export function buildJobFailureReport(input, options = {}) {
|
|
132
|
+
return {
|
|
133
|
+
kind: 'job',
|
|
134
|
+
at: (options.now ?? new Date()).toISOString(),
|
|
135
|
+
functionId: nonEmpty(input.functionId) ?? 'unknown',
|
|
136
|
+
...(nonEmpty(input.runId) ? { runId: input.runId } : {}),
|
|
137
|
+
...(nonEmpty(input.triggerEvent) ? { triggerEvent: input.triggerEvent } : {}),
|
|
138
|
+
...describeError(input.error),
|
|
139
|
+
deployment: options.deployment ?? currentDeployment(),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** Build a report for a healthcheck run with failing checks. */
|
|
143
|
+
export function buildHealthcheckFailureReport(failures, options = {}) {
|
|
144
|
+
return {
|
|
145
|
+
kind: 'healthcheck',
|
|
146
|
+
at: (options.now ?? new Date()).toISOString(),
|
|
147
|
+
functionId: options.functionId ?? 'healthcheck',
|
|
148
|
+
failed: failures.map((failure) => ({
|
|
149
|
+
name: failure.name,
|
|
150
|
+
...(failure.details ? { details: failure.details } : {}),
|
|
151
|
+
})),
|
|
152
|
+
deployment: options.deployment ?? currentDeployment(),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* One line describing a report, for a receiver that renders rather than
|
|
157
|
+
* indexes. Reads the report structurally, so a site's own report shapes get a
|
|
158
|
+
* sensible line too.
|
|
159
|
+
*/
|
|
160
|
+
export function summariseReport(report) {
|
|
161
|
+
const row = (typeof report === 'object' && report !== null ? report : {});
|
|
162
|
+
const deployment = (row['deployment'] ?? {});
|
|
163
|
+
const environment = nonEmpty(deployment['environment']) ?? 'unknown environment';
|
|
164
|
+
const kind = nonEmpty(row['kind']) ?? 'error';
|
|
165
|
+
const failed = Array.isArray(row['failed'])
|
|
166
|
+
? row['failed'].map((f) => String(f.name)).join(', ')
|
|
167
|
+
: undefined;
|
|
168
|
+
const headline = nonEmpty(row['message']) ?? (failed ? `failing: ${failed}` : undefined) ?? 'no message';
|
|
169
|
+
const request = (row['request'] ?? {});
|
|
170
|
+
const path = nonEmpty(request['path']);
|
|
171
|
+
const where = nonEmpty(row['functionId']) ??
|
|
172
|
+
(path ? `${nonEmpty(request['method']) ?? 'GET'} ${path}` : undefined);
|
|
173
|
+
return `[${environment}] ${kind}: ${headline}${where ? ` — ${where}` : ''}`;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Whether reports go anywhere, as a sentence for a boot-time log line. A
|
|
177
|
+
* sentence rather than a boolean because "off because this is a laptop" and
|
|
178
|
+
* "off because somebody forgot in production" look identical otherwise.
|
|
179
|
+
*/
|
|
180
|
+
export function describeErrorReporting(url = readWebhookUrl()) {
|
|
181
|
+
if (!url || url.trim() === '') {
|
|
182
|
+
return `error reporting: OFF (${ERROR_WEBHOOK_URL_ENV} unset) — errors reach the log only`;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
return `error reporting: on, to ${new URL(url).host}`;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return `error reporting: MISCONFIGURED (${ERROR_WEBHOOK_URL_ENV} is not a URL) — errors reach the log only`;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* A reporter that POSTs each report as JSON, with a `text` summary added.
|
|
193
|
+
*
|
|
194
|
+
* A delivery that fails — a refused connection, a timeout, a non-2xx — is
|
|
195
|
+
* logged as a warning and otherwise swallowed. It is never rethrown: see the
|
|
196
|
+
* note at the top of this file.
|
|
197
|
+
*/
|
|
198
|
+
export function createErrorReporter(options = {}) {
|
|
199
|
+
const logger = options.logger ?? defaultLogger;
|
|
200
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
201
|
+
return async function deliver(report) {
|
|
202
|
+
try {
|
|
203
|
+
const url = (options.url ?? readWebhookUrl())?.trim();
|
|
204
|
+
if (!url)
|
|
205
|
+
return;
|
|
206
|
+
const text = summariseReport(report);
|
|
207
|
+
// A non-object report is still delivered, wrapped, rather than dropped.
|
|
208
|
+
const body = typeof report === 'object' && report !== null
|
|
209
|
+
? { text, ...report }
|
|
210
|
+
: { text, report };
|
|
211
|
+
const doFetch = options.fetch ?? fetch;
|
|
212
|
+
const response = await doFetch(url, {
|
|
213
|
+
method: 'POST',
|
|
214
|
+
headers: { ...options.webhookHeaders, 'content-type': 'application/json' },
|
|
215
|
+
body: JSON.stringify(body),
|
|
216
|
+
// A serverless function may be frozen once its response is sent; a
|
|
217
|
+
// report that has not left by then never does.
|
|
218
|
+
keepalive: true,
|
|
219
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
220
|
+
});
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
logger.warn('Error report was not accepted', { status: response.status, summary: text });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch (deliveryError) {
|
|
226
|
+
try {
|
|
227
|
+
logger.warn('Error report delivery failed', { error: String(deliveryError) });
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// A logger that throws is the last thing that can go wrong here.
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/** The default reporter: `ERROR_WEBHOOK_URL`, a 3s timeout, the console logger. */
|
|
236
|
+
export const reportError = createErrorReporter();
|
|
237
|
+
function readWebhookUrl() {
|
|
238
|
+
return typeof process === 'undefined' ? undefined : process.env[ERROR_WEBHOOK_URL_ENV];
|
|
239
|
+
}
|
|
240
|
+
function allowedHeaders(requested) {
|
|
241
|
+
const never = new Set(NEVER_REPORTED_HEADERS);
|
|
242
|
+
return (requested ?? DEFAULT_REPORTED_HEADERS)
|
|
243
|
+
.map((name) => name.toLowerCase())
|
|
244
|
+
.filter((name) => !never.has(name));
|
|
245
|
+
}
|
|
246
|
+
function headerValue(headers, name) {
|
|
247
|
+
let raw;
|
|
248
|
+
if (typeof headers.get === 'function') {
|
|
249
|
+
raw = headers.get(name);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
const record = headers;
|
|
253
|
+
raw = record[name] ?? Object.entries(record).find(([key]) => key.toLowerCase() === name)?.[1];
|
|
254
|
+
}
|
|
255
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
256
|
+
return nonEmpty(value);
|
|
257
|
+
}
|
|
258
|
+
function describeError(error) {
|
|
259
|
+
if (error instanceof Error) {
|
|
260
|
+
return {
|
|
261
|
+
message: error.message || String(error),
|
|
262
|
+
...(error.name ? { name: error.name } : {}),
|
|
263
|
+
...(error.stack ? { stack: error.stack } : {}),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (typeof error === 'object' && error !== null && 'message' in error) {
|
|
267
|
+
return { message: String(error.message) };
|
|
268
|
+
}
|
|
269
|
+
return { message: String(error) };
|
|
270
|
+
}
|
|
271
|
+
function nonEmpty(value) {
|
|
272
|
+
return typeof value === 'string' && value.trim() !== '' ? value : undefined;
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/observability/report.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Payload } from 'payload';
|
|
2
|
+
import type { Logger } from '@forumone/throughline-plugin-contract';
|
|
3
|
+
import { type HealthcheckFailureReport, type JobFailureReport } from './report.js';
|
|
4
|
+
export interface JobFailureWriterOptions {
|
|
5
|
+
payload: Payload;
|
|
6
|
+
collectionSlug?: string | undefined;
|
|
7
|
+
logger?: Logger | undefined;
|
|
8
|
+
}
|
|
9
|
+
/** Records one failure. Never throws, never rejects. */
|
|
10
|
+
export type JobFailureWriter = (report: JobFailureReport | HealthcheckFailureReport) => Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Returns a writer that turns a failure report into a `job-failures` row.
|
|
13
|
+
*
|
|
14
|
+
* It never throws: it runs inside a failure handler, and a throw there is
|
|
15
|
+
* retried by the job runner — a retry storm about the recorder while the
|
|
16
|
+
* original failure goes unrecorded. A write that fails is logged at `error`
|
|
17
|
+
* **with the failure's own summary and message**, so the one thing this
|
|
18
|
+
* writer exists to keep is never lost silently, only demoted to the log.
|
|
19
|
+
*
|
|
20
|
+
* The stack is not stored. Rows are readable through the admin, and a stack
|
|
21
|
+
* names file paths; the webhook report carries it instead.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createJobFailureWriter(options: JobFailureWriterOptions): JobFailureWriter;
|
|
24
|
+
//# sourceMappingURL=writer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../../src/observability/writer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uCAAuC,CAAA;AAGnE,OAAO,EAAmB,KAAK,wBAAwB,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAEnG,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC5B;AAED,wDAAwD;AACxD,MAAM,MAAM,gBAAgB,GAAG,CAC7B,MAAM,EAAE,gBAAgB,GAAG,wBAAwB,KAChD,OAAO,CAAC,IAAI,CAAC,CAAA;AAElB;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,uBAAuB,GAAG,gBAAgB,CAsBzF"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { defaultLogger } from '../logger/index.js';
|
|
2
|
+
import { DEFAULT_JOB_FAILURES_SLUG } from './collection.js';
|
|
3
|
+
import { summariseReport } from './report.js';
|
|
4
|
+
/**
|
|
5
|
+
* Returns a writer that turns a failure report into a `job-failures` row.
|
|
6
|
+
*
|
|
7
|
+
* It never throws: it runs inside a failure handler, and a throw there is
|
|
8
|
+
* retried by the job runner — a retry storm about the recorder while the
|
|
9
|
+
* original failure goes unrecorded. A write that fails is logged at `error`
|
|
10
|
+
* **with the failure's own summary and message**, so the one thing this
|
|
11
|
+
* writer exists to keep is never lost silently, only demoted to the log.
|
|
12
|
+
*
|
|
13
|
+
* The stack is not stored. Rows are readable through the admin, and a stack
|
|
14
|
+
* names file paths; the webhook report carries it instead.
|
|
15
|
+
*/
|
|
16
|
+
export function createJobFailureWriter(options) {
|
|
17
|
+
const { payload, collectionSlug = DEFAULT_JOB_FAILURES_SLUG } = options;
|
|
18
|
+
const logger = options.logger ?? defaultLogger;
|
|
19
|
+
return async function recordJobFailure(report) {
|
|
20
|
+
let data;
|
|
21
|
+
try {
|
|
22
|
+
data = toRow(report);
|
|
23
|
+
await payload.create({ collection: collectionSlug, data });
|
|
24
|
+
}
|
|
25
|
+
catch (writeError) {
|
|
26
|
+
try {
|
|
27
|
+
logger.error('Job failure write failed', {
|
|
28
|
+
error: String(writeError),
|
|
29
|
+
collection: collectionSlug,
|
|
30
|
+
summary: data?.['summary'] ?? safeSummary(report),
|
|
31
|
+
message: data?.['message'],
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Nothing left to tell.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function toRow(report) {
|
|
41
|
+
const base = {
|
|
42
|
+
createdAt: report.at,
|
|
43
|
+
kind: report.kind,
|
|
44
|
+
source: report.functionId,
|
|
45
|
+
summary: summariseReport(report).slice(0, 500),
|
|
46
|
+
...(report.deployment.environment ? { environment: report.deployment.environment } : {}),
|
|
47
|
+
...(report.deployment.commit ? { commit: report.deployment.commit } : {}),
|
|
48
|
+
};
|
|
49
|
+
if (report.kind === 'healthcheck') {
|
|
50
|
+
return {
|
|
51
|
+
...base,
|
|
52
|
+
message: `${report.failed.length} check(s) failed: ${report.failed.map((f) => f.name).join(', ')}`,
|
|
53
|
+
details: report.failed,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
...base,
|
|
58
|
+
message: report.message,
|
|
59
|
+
...(report.name ? { errorName: report.name } : {}),
|
|
60
|
+
...(report.runId ? { runId: report.runId } : {}),
|
|
61
|
+
...(report.triggerEvent ? { triggerEvent: report.triggerEvent } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function safeSummary(report) {
|
|
65
|
+
try {
|
|
66
|
+
return summariseReport(report);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return '(unsummarisable failure report)';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writer.js","sourceRoot":"","sources":["../../src/observability/writer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { Access } from 'payload';
|
|
2
|
+
/** Where each collection sits. Site data: the buckets are the site's decision. */
|
|
3
|
+
export interface AnonymousAccessBuckets {
|
|
4
|
+
/**
|
|
5
|
+
* Collections a published page reads with nobody signed in, each with where
|
|
6
|
+
* it is read. Populated relationships count: Payload applies the related
|
|
7
|
+
* collection's own rule when it resolves one at depth, so an upload
|
|
8
|
+
* collection is here whenever a page shows an image.
|
|
9
|
+
*/
|
|
10
|
+
renderPath: Readonly<Record<string, string>>;
|
|
11
|
+
/**
|
|
12
|
+
* Collections the internet must not read. A reason per entry is optional,
|
|
13
|
+
* and worth writing for anything that is not obvious.
|
|
14
|
+
*/
|
|
15
|
+
private: Readonly<Record<string, string>> | readonly string[];
|
|
16
|
+
/**
|
|
17
|
+
* The same two buckets for globals. Omit it when the site has none; when it
|
|
18
|
+
* is given, every global must be in it.
|
|
19
|
+
*/
|
|
20
|
+
globals?: {
|
|
21
|
+
renderPath?: Readonly<Record<string, string>>;
|
|
22
|
+
private?: Readonly<Record<string, string>> | readonly string[];
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** The slice of a collection or global config this reads. */
|
|
26
|
+
export interface AccessCheckedEntity {
|
|
27
|
+
slug: string;
|
|
28
|
+
access?: {
|
|
29
|
+
read?: unknown;
|
|
30
|
+
} | undefined;
|
|
31
|
+
versions?: unknown;
|
|
32
|
+
}
|
|
33
|
+
/** The slice of a Payload config this reads — a `SanitizedConfig` satisfies it. */
|
|
34
|
+
export interface AccessCheckedConfig {
|
|
35
|
+
collections?: readonly AccessCheckedEntity[] | undefined;
|
|
36
|
+
globals?: readonly AccessCheckedEntity[] | undefined;
|
|
37
|
+
}
|
|
38
|
+
export type AccessFindingKind =
|
|
39
|
+
/** In the config and in no bucket. */
|
|
40
|
+
'unclassified'
|
|
41
|
+
/** In both buckets, or listed twice. */
|
|
42
|
+
| 'classified-twice'
|
|
43
|
+
/** In a bucket and not in the config: renamed, or removed. */
|
|
44
|
+
| 'not-in-config'
|
|
45
|
+
/** On the render path and refusing an anonymous read. */
|
|
46
|
+
| 'render-path-refused'
|
|
47
|
+
/** On the render path with drafts on, and answering a bare `true`. */
|
|
48
|
+
| 'drafts-unfiltered'
|
|
49
|
+
/** Private, and answering an anonymous read with `true` or a query. */
|
|
50
|
+
| 'private-readable';
|
|
51
|
+
export interface AccessFinding {
|
|
52
|
+
kind: AccessFindingKind;
|
|
53
|
+
/** `collection` or `global`. */
|
|
54
|
+
entity: 'collection' | 'global';
|
|
55
|
+
slug: string;
|
|
56
|
+
message: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The anonymous request an access rule is handed.
|
|
60
|
+
*
|
|
61
|
+
* `req.payload` is a stub that throws on use rather than a mock that answers:
|
|
62
|
+
* a read rule that looks something up to decide is one this cannot evaluate
|
|
63
|
+
* without a database, and it should say so here rather than pass on a guess.
|
|
64
|
+
*/
|
|
65
|
+
export declare function anonymousAccessArgs(): Parameters<Access>[0];
|
|
66
|
+
/**
|
|
67
|
+
* What a read rule answers an anonymous request: `true`, `false`, a `where`
|
|
68
|
+
* query, or `undefined` when there is no rule at all.
|
|
69
|
+
*/
|
|
70
|
+
export declare function anonymousRead(entity: AccessCheckedEntity): Promise<unknown>;
|
|
71
|
+
/** Whether an entity keeps drafts, so an anonymous read must be narrowed to published. */
|
|
72
|
+
export declare function hasDrafts(entity: AccessCheckedEntity): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Why a render-path entity fails, or `undefined` when it passes.
|
|
75
|
+
*
|
|
76
|
+
* `undefined` is a failure here and not in `privateProblem`, deliberately. A
|
|
77
|
+
* collection with no `read` rule gets Payload's default, which requires a user
|
|
78
|
+
* — so leaving one off is the same outage as writing an editors-only rule, and
|
|
79
|
+
* reads in review as though no decision were needed.
|
|
80
|
+
*/
|
|
81
|
+
export declare function renderPathProblem(entity: AccessCheckedEntity): Promise<{
|
|
82
|
+
kind: AccessFindingKind;
|
|
83
|
+
message: string;
|
|
84
|
+
} | undefined>;
|
|
85
|
+
/**
|
|
86
|
+
* Why a private entity fails, or `undefined` when it passes.
|
|
87
|
+
*
|
|
88
|
+
* `undefined` passes: Payload's default refuses an anonymous read, so a plugin
|
|
89
|
+
* collection with no rule of its own is already closed. What must not happen is
|
|
90
|
+
* `true`, or a query — a query is a successful read of some subset.
|
|
91
|
+
*/
|
|
92
|
+
export declare function privateProblem(entity: AccessCheckedEntity): Promise<{
|
|
93
|
+
kind: AccessFindingKind;
|
|
94
|
+
message: string;
|
|
95
|
+
} | undefined>;
|
|
96
|
+
/** Slugs of a bucket, whichever shape it was written in. */
|
|
97
|
+
export declare function bucketSlugs(bucket: Readonly<Record<string, string>> | readonly string[] | undefined): string[];
|
|
98
|
+
/**
|
|
99
|
+
* Every entity in exactly one bucket, and every bucket entry in the config.
|
|
100
|
+
*/
|
|
101
|
+
export declare function classificationFindings(entity: 'collection' | 'global', inConfig: readonly AccessCheckedEntity[], renderPath: readonly string[], closed: readonly string[]): AccessFinding[];
|
|
102
|
+
/** A Payload config, or the promise `buildConfig` returns. */
|
|
103
|
+
export type ConfigInput = AccessCheckedConfig | Promise<AccessCheckedConfig>;
|
|
104
|
+
/**
|
|
105
|
+
* Every finding for a config and its buckets, empty when it is sound.
|
|
106
|
+
*
|
|
107
|
+
* The pure form of `describeAnonymousAccess`, for a script or a test that
|
|
108
|
+
* wants the list rather than one test per collection.
|
|
109
|
+
*/
|
|
110
|
+
export declare function checkAnonymousAccess(config: ConfigInput, buckets: AnonymousAccessBuckets): Promise<AccessFinding[]>;
|
|
111
|
+
//# sourceMappingURL=anonymousAccess.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anonymousAccess.d.ts","sourceRoot":"","sources":["../../src/testing/anonymousAccess.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AA2BrC,kFAAkF;AAClF,MAAM,WAAW,sBAAsB;IACrC;;;;;OAKG;IACH,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC5C;;;OAGG;IACH,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,MAAM,EAAE,CAAA;IAC7D;;;OAGG;IACH,OAAO,CAAC,EAAE;QACR,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;QAC7C,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,MAAM,EAAE,CAAA;KAC/D,CAAA;CACF;AAED,6DAA6D;AAC7D,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,mFAAmF;AACnF,MAAM,WAAW,mBAAmB;IAClC,WAAW,CAAC,EAAE,SAAS,mBAAmB,EAAE,GAAG,SAAS,CAAA;IACxD,OAAO,CAAC,EAAE,SAAS,mBAAmB,EAAE,GAAG,SAAS,CAAA;CACrD;AAED,MAAM,MAAM,iBAAiB;AAC3B,sCAAsC;AACpC,cAAc;AAChB,wCAAwC;GACtC,kBAAkB;AACpB,8DAA8D;GAC5D,eAAe;AACjB,yDAAyD;GACvD,qBAAqB;AACvB,sEAAsE;GACpE,mBAAmB;AACrB,uEAAuE;GACrE,kBAAkB,CAAA;AAEtB,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,iBAAiB,CAAA;IACvB,gCAAgC;IAChC,MAAM,EAAE,YAAY,GAAG,QAAQ,CAAA;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAmB3D;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC,CAIjF;AAED,0FAA0F;AAC1F,wBAAgB,SAAS,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAG9D;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAC,CA0BnE;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAC,CAYnE;AAED,4DAA4D;AAC5D,wBAAgB,WAAW,CACzB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,GACvE,MAAM,EAAE,CAGV;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,YAAY,GAAG,QAAQ,EAC/B,QAAQ,EAAE,SAAS,mBAAmB,EAAE,EACxC,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,MAAM,EAAE,SAAS,MAAM,EAAE,GACxB,aAAa,EAAE,CA2CjB;AAED,8DAA8D;AAC9D,MAAM,MAAM,WAAW,GAAG,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAA;AAE5E;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,aAAa,EAAE,CAAC,CA2C1B"}
|