drupal-mcp-connector 2.19.0 → 2.20.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/.agents/commands/drupal-config-set.md +2 -2
- package/.agents/commands/drupal-create-node.md +2 -2
- package/.agents/commands/drupal-create-translation.md +1 -1
- package/.agents/commands/drupal-delete-node.md +1 -1
- package/.agents/commands/drupal-describe-fields.md +2 -2
- package/.agents/commands/drupal-drush-config-import.md +2 -2
- package/.agents/commands/drupal-drush-module-disable.md +2 -2
- package/.agents/commands/drupal-drush-module-list.md +1 -1
- package/.agents/commands/drupal-drush-user-list.md +1 -1
- package/.agents/commands/drupal-drush-watchdog.md +1 -1
- package/.agents/commands/drupal-entity-create.md +2 -2
- package/.agents/commands/drupal-entity-delete.md +1 -1
- package/.agents/commands/drupal-entity-update.md +4 -4
- package/.agents/commands/drupal-report-field-completeness.md +2 -2
- package/.agents/commands/drupal-report-missing-field.md +2 -2
- package/.agents/commands/drupal-report-seo-meta-coverage.md +2 -2
- package/.agents/commands/drupal-report-status-report.md +1 -1
- package/.agents/commands/drupal-update-node.md +4 -4
- package/CHANGELOG.md +254 -0
- package/README.md +10 -3
- package/bin/drupal-mcp-verify.js +4 -3
- package/config/config.example.json +52 -2
- package/package.json +1 -1
- package/scripts/generate-commands.js +40 -5
- package/scripts/install-commands.js +148 -11
- package/src/index.js +9 -12
- package/src/lib/backends/graphql-schema.js +9 -1
- package/src/lib/backends/graphql.js +4 -2
- package/src/lib/backends/index.js +9 -3
- package/src/lib/backends/jsonapi.js +2 -0
- package/src/lib/dispatch.js +6 -6
- package/src/lib/drupal-fetch.js +97 -26
- package/src/lib/dry-run-checks.js +78 -0
- package/src/lib/error-body.js +448 -0
- package/src/lib/error-status.js +38 -0
- package/src/lib/errors.js +0 -11
- package/src/lib/evidence.js +0 -6
- package/src/lib/governance.js +2 -8
- package/src/lib/link-checker.js +3 -3
- package/src/lib/mcp-server.js +7 -1
- package/src/lib/metatag-audit.js +2 -1
- package/src/lib/module-tools.js +23 -2
- package/src/lib/operations.js +2 -2
- package/src/lib/patch-preflight.js +23 -5
- package/src/lib/policy-enforcement.js +4 -4
- package/src/lib/principal.js +3 -3
- package/src/lib/relay/edge.js +2 -2
- package/src/lib/reports-support.js +75 -0
- package/src/lib/security.js +234 -18
- package/src/lib/sentinel-draft.js +3 -2
- package/src/lib/server-tools.js +188 -27
- package/src/lib/tool-prompts.js +139 -8
- package/src/lib/usage.js +0 -9
- package/src/lib/verify.js +164 -61
- package/src/tools/config.js +70 -5
- package/src/tools/drush.js +191 -17
- package/src/tools/entities.js +18 -6
- package/src/tools/fields.js +40 -4
- package/src/tools/graphql.js +10 -5
- package/src/tools/nodes.js +16 -6
- package/src/tools/paragraphs.js +1 -1
- package/src/tools/reports-config.js +3 -3
- package/src/tools/reports-content.js +34 -26
- package/src/tools/reports-extra.js +46 -38
- package/src/tools/reports.js +50 -25
- package/src/tools/scheduler.js +1 -1
- package/src/tools/structure.js +12 -2
- package/src/tools/translations.js +5 -1
- package/src/lib/draft-write.js +0 -19
- package/src/lib/node-draft-inventory.js +0 -5
package/src/lib/dispatch.js
CHANGED
|
@@ -15,10 +15,10 @@ import { resolveSecurityConfig, assertNotReadOnly,
|
|
|
15
15
|
import { toolError, toolResult } from "./errors.js";
|
|
16
16
|
import { BackendCapabilityError, BackendResolutionError } from "./backends/errors.js";
|
|
17
17
|
import { inferOperation } from "./operations.js";
|
|
18
|
-
import { assertSourceGovernance, GovernanceError
|
|
18
|
+
import { assertSourceGovernance, GovernanceError } from "./governance.js";
|
|
19
19
|
import {
|
|
20
|
-
assertPrincipalEntitlement, callerTargetHints,
|
|
21
|
-
principalHasScope, resolveAuthoritativeTarget,
|
|
20
|
+
assertPrincipalEntitlement, callerTargetHints, DIAGNOSTIC_TOOLS,
|
|
21
|
+
getRequestIdentity, principalHasScope, resolveAuthoritativeTarget,
|
|
22
22
|
} from "./principal.js";
|
|
23
23
|
import { assertExplicitSiteForWrite, withResolvedTarget } from "./site-target.js";
|
|
24
24
|
import { buildDataFlowContext, consumeBudgetIfEnforced, runWithDataFlow } from "./data-flow.js";
|
|
@@ -55,7 +55,7 @@ export function listResolvableSiteConfigs() {
|
|
|
55
55
|
* @returns {?{site: object, source: string, name: string}}
|
|
56
56
|
* @throws {SecurityError}
|
|
57
57
|
*/
|
|
58
|
-
|
|
58
|
+
function resolveCallTarget(toolName, rawArgs, context = {}) {
|
|
59
59
|
if (toolName === "drupal_list_sites") return null;
|
|
60
60
|
if (toolName === "drupal_governance_status" && callerTargetHints(rawArgs).length === 0) {
|
|
61
61
|
return null;
|
|
@@ -157,7 +157,7 @@ export async function securityMiddleware(toolName, args, handler, context = {})
|
|
|
157
157
|
const assertCallAllowed = async () => {
|
|
158
158
|
// Source-governance gate (#176). The diagnostic tools stay callable while
|
|
159
159
|
// governance fails — they are how an operator learns which condition failed.
|
|
160
|
-
if (!
|
|
160
|
+
if (!DIAGNOSTIC_TOOLS.has(toolName)) {
|
|
161
161
|
await assertSourceGovernance(site);
|
|
162
162
|
}
|
|
163
163
|
|
|
@@ -188,7 +188,7 @@ export async function securityMiddleware(toolName, args, handler, context = {})
|
|
|
188
188
|
await assertCallAllowed();
|
|
189
189
|
// Charge only after governance and policy gates pass, so an outage or a
|
|
190
190
|
// local deny cannot exhaust the window. Diagnostics do not consume.
|
|
191
|
-
if (!
|
|
191
|
+
if (!DIAGNOSTIC_TOOLS.has(toolName)) {
|
|
192
192
|
consumeBudgetIfEnforced("chained_action");
|
|
193
193
|
}
|
|
194
194
|
return handler(nextArgs);
|
package/src/lib/drupal-fetch.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Authenticated HTTP wrappers for Drupal JSON:API and file uploads.
|
|
3
|
+
* Northbound node-fetch calls share a 30s abort timeout (`DRUPAL_FETCH_TIMEOUT_MS`).
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
import fetch from "node-fetch";
|
|
@@ -19,9 +20,32 @@ import {
|
|
|
19
20
|
sanitizeUploadFilename,
|
|
20
21
|
validateMachineName,
|
|
21
22
|
} from "./validate.js";
|
|
23
|
+
import { describeErrorBody, cleanGraphqlErrors, ERROR_DOCUMENT_MAX_CHARS } from "./error-body.js";
|
|
22
24
|
|
|
23
25
|
const JSON_API_CONTENT_TYPE = "application/vnd.api+json";
|
|
24
26
|
|
|
27
|
+
/** Default northbound Drupal HTTP timeout in milliseconds. Matches `sshDrush`. */
|
|
28
|
+
export const DRUPAL_FETCH_TIMEOUT_MS = 30_000;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* node-fetch with the shared abort timeout. A caller `signal` wins.
|
|
32
|
+
* @param {string} url Fully-qualified request URL.
|
|
33
|
+
* @param {object} [options] node-fetch options (method, body, headers, signal).
|
|
34
|
+
* @returns {Promise<object>} node-fetch Response.
|
|
35
|
+
* @throws {Error} when the default timeout fires (`AbortError` remapped).
|
|
36
|
+
*/
|
|
37
|
+
async function timedDrupalFetch(url, options = {}) {
|
|
38
|
+
const signal = options.signal ?? AbortSignal.timeout(DRUPAL_FETCH_TIMEOUT_MS);
|
|
39
|
+
try {
|
|
40
|
+
return await fetch(url, { ...options, signal });
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (err?.name === "AbortError" && !options.signal) {
|
|
43
|
+
throw new Error(`Drupal request timed out after ${DRUPAL_FETCH_TIMEOUT_MS / 1000}s`);
|
|
44
|
+
}
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
25
49
|
/**
|
|
26
50
|
* Read a 2xx body once. Prefers text() so byte accounting can see the payload;
|
|
27
51
|
* falls back to json() for test doubles that only implement that.
|
|
@@ -41,6 +65,45 @@ async function readOkBody(res) {
|
|
|
41
65
|
return { json: await res.json(), text: "" };
|
|
42
66
|
}
|
|
43
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Build the error for a non-2xx response.
|
|
70
|
+
*
|
|
71
|
+
* The body is untrusted: it may be an HTML error page from Drupal, PHP or a
|
|
72
|
+
* proxy, or carry a server path, another user's filename or a backtrace. Only a
|
|
73
|
+
* cleaned, bounded detail is surfaced (see `describeErrorBody`); the prefix,
|
|
74
|
+
* which holds the HTTP status, is always kept because callers match on it
|
|
75
|
+
* (#343, #345).
|
|
76
|
+
* @param {object} res node-fetch Response with `ok === false`.
|
|
77
|
+
* @param {string} prefix Message start, e.g. "Drupal 404 on GET /jsonapi/x".
|
|
78
|
+
* @param {object} [options] Passed to `describeErrorBody`.
|
|
79
|
+
* @returns {Promise<Error>} A source budget denial, or the described failure
|
|
80
|
+
* with the HTTP status on its `status` property. Callers that branch on the
|
|
81
|
+
* status read it with `httpStatusOf()`, never from the message text (#355).
|
|
82
|
+
*/
|
|
83
|
+
async function failedResponseError(res, prefix, options) {
|
|
84
|
+
let body;
|
|
85
|
+
try {
|
|
86
|
+
body = await res.text();
|
|
87
|
+
} catch {
|
|
88
|
+
return withStatus(new Error(`${prefix} (response body could not be read)`), res.status);
|
|
89
|
+
}
|
|
90
|
+
const mapped = sourceBudgetDenial(body);
|
|
91
|
+
if (mapped) return mapped;
|
|
92
|
+
const detail = describeErrorBody(body, res.headers?.get?.("content-type") ?? null, options);
|
|
93
|
+
return withStatus(new Error(detail ? `${prefix}: ${detail}` : `${prefix} (empty response body)`), res.status);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Set the HTTP status on an error.
|
|
98
|
+
* @param {Error} error Error to mark.
|
|
99
|
+
* @param {number} status HTTP status of the response.
|
|
100
|
+
* @returns {Error} The same error.
|
|
101
|
+
*/
|
|
102
|
+
function withStatus(error, status) {
|
|
103
|
+
error.status = status;
|
|
104
|
+
return error;
|
|
105
|
+
}
|
|
106
|
+
|
|
44
107
|
/**
|
|
45
108
|
* Standard JSON:API request against a site.
|
|
46
109
|
*
|
|
@@ -50,7 +113,10 @@ async function readOkBody(res) {
|
|
|
50
113
|
* @param {string} path Path appended to site.baseUrl (e.g. "/jsonapi/node/article").
|
|
51
114
|
* @param {object} [options] node-fetch options (method, body, extra headers).
|
|
52
115
|
* @returns {Promise<object|null>} Parsed JSON body, or null for a 204 No Content.
|
|
53
|
-
* @throws {Error} on any non-2xx response
|
|
116
|
+
* @throws {Error} on any non-2xx response. The message is
|
|
117
|
+
* `Drupal <status> on <method> <path>: <detail>`, where the detail is the
|
|
118
|
+
* cleaned, bounded `errors[].detail` text (see `describeErrorBody`). The raw
|
|
119
|
+
* body and any HTML page are never included.
|
|
54
120
|
*/
|
|
55
121
|
export async function drupalFetch(site, path, options = {}) {
|
|
56
122
|
const url = `${site.baseUrl}${path}`;
|
|
@@ -61,7 +127,7 @@ export async function drupalFetch(site, path, options = {}) {
|
|
|
61
127
|
consumeBudgetIfEnforced("request", 1, { retry: isRetry || paid });
|
|
62
128
|
if (collection) consumeBudgetIfEnforced("page", 1, { retry: isRetry || paid });
|
|
63
129
|
paid = true;
|
|
64
|
-
return
|
|
130
|
+
return timedDrupalFetch(url, {
|
|
65
131
|
...options,
|
|
66
132
|
headers: {
|
|
67
133
|
"Content-Type": JSON_API_CONTENT_TYPE,
|
|
@@ -83,18 +149,11 @@ export async function drupalFetch(site, path, options = {}) {
|
|
|
83
149
|
}
|
|
84
150
|
|
|
85
151
|
if (!res.ok) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const parsed = JSON.parse(body);
|
|
92
|
-
// Drupal JSON:API surfaces errors in errors[].detail
|
|
93
|
-
if (parsed.errors?.length) {
|
|
94
|
-
detail = parsed.errors.map((e) => e.detail || e.title).join("; ");
|
|
95
|
-
}
|
|
96
|
-
} catch { /* use raw body */ }
|
|
97
|
-
throw new Error(`Drupal ${res.status} on ${options.method || "GET"} ${path}: ${detail}`);
|
|
152
|
+
throw await failedResponseError(
|
|
153
|
+
res,
|
|
154
|
+
`Drupal ${res.status} on ${options.method || "GET"} ${path}`,
|
|
155
|
+
{ maxChars: ERROR_DOCUMENT_MAX_CHARS },
|
|
156
|
+
);
|
|
98
157
|
}
|
|
99
158
|
|
|
100
159
|
if (res.status === 204) return null; // No Content (e.g. DELETE success)
|
|
@@ -107,15 +166,22 @@ export async function drupalFetch(site, path, options = {}) {
|
|
|
107
166
|
* GraphQL request — posts a JSON body to the site's GraphQL endpoint.
|
|
108
167
|
* @param {object} site Resolved site config (provides baseUrl + auth).
|
|
109
168
|
* @param {object} body GraphQL request body, e.g. { query, variables }.
|
|
110
|
-
* @returns {Promise<object>} Parsed GraphQL JSON response.
|
|
169
|
+
* @returns {Promise<object>} Parsed GraphQL JSON response. `data` is returned
|
|
170
|
+
* as received. GraphQL reports a failed query as HTTP 200 with an `errors`
|
|
171
|
+
* array; that array is replaced by its cleaned, bounded form (see
|
|
172
|
+
* `cleanGraphqlErrors`): `message`, `path`, `locations` and the machine
|
|
173
|
+
* values of `extensions` only (#356).
|
|
111
174
|
* @throws {Error} on any non-2xx response (clears the OAuth token cache on 401).
|
|
175
|
+
* The message is `GraphQL request failed <status>: <detail>`, where the detail
|
|
176
|
+
* is the cleaned, bounded `errors[].message` text (see `describeErrorBody`).
|
|
177
|
+
* The raw body and any HTML page are never included.
|
|
112
178
|
*/
|
|
113
179
|
export async function drupalGraphqlFetch(site, body) {
|
|
114
180
|
const endpoint = site.graphqlEndpoint || "/graphql";
|
|
115
181
|
const url = `${site.baseUrl}${endpoint}`;
|
|
116
182
|
|
|
117
183
|
consumeBudgetIfEnforced("request");
|
|
118
|
-
const res = await
|
|
184
|
+
const res = await timedDrupalFetch(url, {
|
|
119
185
|
method: "POST",
|
|
120
186
|
headers: {
|
|
121
187
|
"Content-Type": "application/json",
|
|
@@ -131,14 +197,20 @@ export async function drupalGraphqlFetch(site, body) {
|
|
|
131
197
|
// OAuth sites: a 401 likely means the token expired server-side. Clear the
|
|
132
198
|
// cached token so the next request re-acquires, then surface the error.
|
|
133
199
|
if (res.status === 401 && site.oauth) clearToken(site);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
200
|
+
throw await failedResponseError(
|
|
201
|
+
res,
|
|
202
|
+
`GraphQL request failed ${res.status}`,
|
|
203
|
+
{ maxChars: ERROR_DOCUMENT_MAX_CHARS },
|
|
204
|
+
);
|
|
138
205
|
}
|
|
139
206
|
|
|
140
207
|
const { json, text } = await readOkBody(res);
|
|
141
208
|
accountNorthboundBody(json, text);
|
|
209
|
+
if (json && typeof json === "object" && !Array.isArray(json) && "errors" in json) {
|
|
210
|
+
json.errors = cleanGraphqlErrors(json.errors);
|
|
211
|
+
// An empty list is not an error; consumers test `json.errors` for truth.
|
|
212
|
+
if (!json.errors.length) delete json.errors;
|
|
213
|
+
}
|
|
142
214
|
return json;
|
|
143
215
|
}
|
|
144
216
|
|
|
@@ -159,7 +231,9 @@ export async function drupalGraphqlFetch(site, body) {
|
|
|
159
231
|
* @param {string} fieldName File field on the bundle (e.g. "field_media_image").
|
|
160
232
|
* @param {string} filePath Local path to the file to upload.
|
|
161
233
|
* @returns {Promise<object>} Parsed JSON:API File entity response.
|
|
162
|
-
* @throws {Error} on any non-2xx response.
|
|
234
|
+
* @throws {Error} on any non-2xx response. The message keeps the HTTP status and a
|
|
235
|
+
* cleaned, bounded detail from the body (see `describeErrorBody`); the raw body
|
|
236
|
+
* and any HTML page are never included.
|
|
163
237
|
*/
|
|
164
238
|
export async function drupalUploadFile(site, entityType, bundle, fieldName, filePath) {
|
|
165
239
|
// #137: machine-name segments + path allowlist before any FS or network I/O.
|
|
@@ -173,7 +247,7 @@ export async function drupalUploadFile(site, entityType, bundle, fieldName, file
|
|
|
173
247
|
const url = `${site.baseUrl}/jsonapi/${encodeURIComponent(entityType)}/${encodeURIComponent(bundle)}/${encodeURIComponent(fieldName)}`;
|
|
174
248
|
|
|
175
249
|
consumeBudgetIfEnforced("request");
|
|
176
|
-
const res = await
|
|
250
|
+
const res = await timedDrupalFetch(url, {
|
|
177
251
|
method: "POST",
|
|
178
252
|
headers: {
|
|
179
253
|
"Content-Type": "application/octet-stream",
|
|
@@ -190,10 +264,7 @@ export async function drupalUploadFile(site, entityType, bundle, fieldName, file
|
|
|
190
264
|
});
|
|
191
265
|
|
|
192
266
|
if (!res.ok) {
|
|
193
|
-
|
|
194
|
-
const mapped = sourceBudgetDenial(body);
|
|
195
|
-
if (mapped) throw mapped;
|
|
196
|
-
throw new Error(`File upload failed ${res.status}: ${body}`);
|
|
267
|
+
throw await failedResponseError(res, `File upload failed ${res.status}`);
|
|
197
268
|
}
|
|
198
269
|
|
|
199
270
|
return res.json();
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a `dryRun` preview actually checked (#336).
|
|
3
|
+
*
|
|
4
|
+
* A preview that returns without a refusal reads as "this write will work".
|
|
5
|
+
* That is only true for the checks that ran, and most previews run few of
|
|
6
|
+
* them. Every dryRun result carries a `checks` block naming each check as
|
|
7
|
+
* `checked` or `not_checked`, plus a `caveat` sentence when anything was left
|
|
8
|
+
* out. `checked` always means "ran and passed": a failed check throws, so it
|
|
9
|
+
* never reaches a preview.
|
|
10
|
+
*
|
|
11
|
+
* Three server-side preflights exist:
|
|
12
|
+
*
|
|
13
|
+
* - `sentinel_draft` — Sentinel's non-saving draft endpoint. It receives the
|
|
14
|
+
* real attributes and relationships, applies them through core field
|
|
15
|
+
* access, validates the entity, and returns before saving. It predicts a
|
|
16
|
+
* field-access or validation refusal.
|
|
17
|
+
* - `core_patch_guard` — a core JSON:API PATCH with no fields and a
|
|
18
|
+
* non-matching `data.id`. The route checks entity update access and core
|
|
19
|
+
* checks the working-copy guard, then rejects the id. Core evaluates field
|
|
20
|
+
* access and validation only after the id check, so this probe says
|
|
21
|
+
* nothing about the submitted fields.
|
|
22
|
+
* - `none` — Drupal evaluated nothing about the write. The connector may
|
|
23
|
+
* still have read from Drupal (the existing entity, field definitions).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** No server-side preflight ran. */
|
|
27
|
+
export const PREFLIGHT_NONE = "none";
|
|
28
|
+
|
|
29
|
+
/** Core's PATCH working-copy guard was probed with an empty, id-mismatched body. */
|
|
30
|
+
export const PREFLIGHT_CORE_GUARD = "core_patch_guard";
|
|
31
|
+
|
|
32
|
+
/** Sentinel's non-saving draft endpoint evaluated the real payload. */
|
|
33
|
+
export const PREFLIGHT_SENTINEL_DRAFT = "sentinel_draft";
|
|
34
|
+
|
|
35
|
+
const CHECKED = "checked";
|
|
36
|
+
const NOT_CHECKED = "not_checked";
|
|
37
|
+
|
|
38
|
+
const CAVEAT_CORE_GUARD =
|
|
39
|
+
"Field access and entity validation were NOT checked. The server-side probe carried no fields: " +
|
|
40
|
+
"it checked entity update access and core's working-copy guard only. " +
|
|
41
|
+
"The real write can still fail with a field-access 403 or a validation 422.";
|
|
42
|
+
|
|
43
|
+
const CAVEAT_NONE_WRITE =
|
|
44
|
+
"Drupal did not evaluate this write. Entity access, field access and entity validation were NOT checked. " +
|
|
45
|
+
"This preview shows the payload the connector would send after its own policy checks; " +
|
|
46
|
+
"the real write can still fail with a 403 or a validation 422.";
|
|
47
|
+
|
|
48
|
+
const CAVEAT_NONE_DELETE =
|
|
49
|
+
"Drupal did not evaluate this delete. Drupal's delete access for this entity was NOT checked; " +
|
|
50
|
+
"only the connector's own policy was. The real delete can still fail with a 403 or 404.";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Describe what a dryRun preview checked.
|
|
54
|
+
*
|
|
55
|
+
* Fails closed: an unknown or missing `preflight` is reported as `none`, so a
|
|
56
|
+
* caller that forgets to pass it cannot claim a check that did not run.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} args
|
|
59
|
+
* @param {"create"|"update"|"delete"|string} args.operation Previewed operation.
|
|
60
|
+
* @param {?string} [args.preflight] One of the PREFLIGHT_* constants.
|
|
61
|
+
* @returns {{checks: {serverPreflight: string, connectorPolicy: string, entityAccess: string, revisionGuard: string, fieldAccess: string, entityValidation: string}, caveat?: string}}
|
|
62
|
+
*/
|
|
63
|
+
export function dryRunChecks({ operation, preflight } = {}) {
|
|
64
|
+
const sentinel = preflight === PREFLIGHT_SENTINEL_DRAFT;
|
|
65
|
+
const core = preflight === PREFLIGHT_CORE_GUARD;
|
|
66
|
+
const contacted = sentinel || core;
|
|
67
|
+
const checks = {
|
|
68
|
+
serverPreflight: sentinel ? PREFLIGHT_SENTINEL_DRAFT : core ? PREFLIGHT_CORE_GUARD : PREFLIGHT_NONE,
|
|
69
|
+
connectorPolicy: CHECKED,
|
|
70
|
+
entityAccess: contacted ? CHECKED : NOT_CHECKED,
|
|
71
|
+
revisionGuard: contacted ? CHECKED : NOT_CHECKED,
|
|
72
|
+
fieldAccess: sentinel ? CHECKED : NOT_CHECKED,
|
|
73
|
+
entityValidation: sentinel ? CHECKED : NOT_CHECKED,
|
|
74
|
+
};
|
|
75
|
+
if (sentinel) return { checks };
|
|
76
|
+
if (core) return { checks, caveat: CAVEAT_CORE_GUARD };
|
|
77
|
+
return { checks, caveat: operation === "delete" ? CAVEAT_NONE_DELETE : CAVEAT_NONE_WRITE };
|
|
78
|
+
}
|