drupal-mcp-connector 2.16.0 → 2.18.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-create-translation.md +3 -3
- package/.agents/commands/drupal-get-media.md +4 -3
- package/.agents/commands/drupal-list-translations.md +3 -3
- package/.agents/commands/drupal-update-media.md +5 -4
- package/CHANGELOG.md +52 -0
- package/README.md +5 -0
- package/package.json +4 -1
- package/src/index.js +16 -8
- package/src/lib/backends/backend-interface.js +3 -0
- package/src/lib/backends/graphql.js +1 -0
- package/src/lib/backends/jsonapi.js +1 -0
- package/src/lib/canonical.js +10 -5
- package/src/lib/dispatch.js +10 -0
- package/src/lib/draft-write.js +18 -429
- package/src/lib/module-tools.js +241 -0
- package/src/lib/node-draft-inventory.js +4 -46
- package/src/lib/operations.js +5 -0
- package/src/lib/patch-preflight.js +7 -15
- package/src/lib/sentinel-draft.js +530 -0
- package/src/lib/server-tools.js +50 -16
- package/src/lib/verify.js +7 -1
- package/src/tools/config.js +14 -4
- package/src/tools/media.js +61 -8
- package/src/tools/moderation.js +21 -21
- package/src/tools/nodes.js +1 -1
- package/src/tools/paragraphs.js +1 -1
- package/src/tools/reports-config.js +11 -1
- package/src/tools/reports-content.js +7 -6
- package/src/tools/revisions.js +24 -23
- package/src/tools/structure.js +1 -1
- package/src/tools/taxonomy.js +1 -1
- package/src/tools/translations.js +31 -21
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Create a translation as an unpublished non-default draft (governed write). Adds the target language beside the default language; it does not PATCH langcode on the canonical entity. When an English working draft already exists, both live and working revision IDs are sent (If-Match) so Sentinel will add the language on that draft (#282). English live title, body, status, alias, default revision, and paragraph ERR pins stay unchanged. An existing translation is a conflict, not an overwrite. The response includes `_revisions.live` / `_revisions.working` when known. Computed `metatag` is omitted on the draft body because JSON:API resolves it from the live default (#283); use field_metatags. Continue a node draft with drupal_update_node and langcode;
|
|
2
|
+
description: "Create a translation as an unpublished non-default draft (governed write). Adds the target language beside the default language; it does not PATCH langcode on the canonical entity. When an English working draft already exists, both live and working revision IDs are sent (If-Match) so Sentinel will add the language on that draft (#282). English live title, body, status, alias, default revision, and paragraph ERR pins stay unchanged. An existing translation is a conflict, not an overwrite. The response includes `_revisions.live` / `_revisions.working` when known. Computed `metatag` is omitted on the draft body because JSON:API resolves it from the live default (#283); use field_metatags. Continue a node draft with drupal_update_node and langcode; a paragraph with drupal_update_paragraph and langcode; media with drupal_update_media and langcode. Image alt is a relationship (same file UUID, meta.alt) — on media that is field_media_image, not a node image field. For paragraphs pass revisionId as the host pin. Requires Sentinel's draft-translation endpoint. Publication stays denied for content-tier callers."
|
|
3
3
|
argument-hint: "<type> <id> <langcode> [site] [entityType] [attributes] [relationships] [revisionId] [dryRun]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_create_translation`.
|
|
7
7
|
|
|
8
|
-
Create a translation as an unpublished non-default draft (governed write). Adds the target language beside the default language; it does not PATCH langcode on the canonical entity. When an English working draft already exists, both live and working revision IDs are sent (If-Match) so Sentinel will add the language on that draft (#282). English live title, body, status, alias, default revision, and paragraph ERR pins stay unchanged. An existing translation is a conflict, not an overwrite. The response includes `_revisions.live` / `_revisions.working` when known. Computed `metatag` is omitted on the draft body because JSON:API resolves it from the live default (#283); use field_metatags. Continue a node draft with drupal_update_node and langcode;
|
|
8
|
+
Create a translation as an unpublished non-default draft (governed write). Adds the target language beside the default language; it does not PATCH langcode on the canonical entity. When an English working draft already exists, both live and working revision IDs are sent (If-Match) so Sentinel will add the language on that draft (#282). English live title, body, status, alias, default revision, and paragraph ERR pins stay unchanged. An existing translation is a conflict, not an overwrite. The response includes `_revisions.live` / `_revisions.working` when known. Computed `metatag` is omitted on the draft body because JSON:API resolves it from the live default (#283); use field_metatags. Continue a node draft with drupal_update_node and langcode; a paragraph with drupal_update_paragraph and langcode; media with drupal_update_media and langcode. Image alt is a relationship (same file UUID, meta.alt) — on media that is field_media_image, not a node image field. For paragraphs pass revisionId as the host pin. Requires Sentinel's draft-translation endpoint. Publication stays denied for content-tier callers.
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
|
@@ -16,7 +16,7 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
16
16
|
|
|
17
17
|
**Optional:**
|
|
18
18
|
- `site` (string): Named site from connector config. Omit only on reads: multi-site configs fall back to defaultSite (often local/dev, not production). Writes require an explicit site when more than one site is configured. Every response includes `_target` { name, baseUrl, source } (`hint` when you passed site, `default` when you did not).
|
|
19
|
-
- `entityType` (string): Entity type machine name. Default: 'node'. Use 'paragraph' for paragraph field values.
|
|
19
|
+
- `entityType` (string): Entity type machine name. Default: 'node'. Use 'paragraph' for paragraph field values, 'media' for media (name, caption, image alt).
|
|
20
20
|
- `attributes` (object (pass as JSON)): Translated field values keyed by Drupal machine name
|
|
21
21
|
- `relationships` (object (pass as JSON)): JSON:API relationships. Use for image alt (same file UUID, meta.alt).
|
|
22
22
|
- `revisionId` (string): Paragraph revision id the host already pins. Required for Home-shaped non-default pins.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Fetch a single media entity by UUID and media type."
|
|
3
|
-
argument-hint: "<type> <id> [site]"
|
|
2
|
+
description: "Fetch a single media entity by UUID and media type. Image alt/title are on the file relationship (meta.alt / meta.title). Pass langcode to read an unpublished working translation via Sentinel."
|
|
3
|
+
argument-hint: "<type> <id> [site] [langcode]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_get_media`.
|
|
7
7
|
|
|
8
|
-
Fetch a single media entity by UUID and media type.
|
|
8
|
+
Fetch a single media entity by UUID and media type. Image alt/title are on the file relationship (meta.alt / meta.title). Pass langcode to read an unpublished working translation via Sentinel.
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
|
@@ -15,5 +15,6 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
15
15
|
|
|
16
16
|
**Optional:**
|
|
17
17
|
- `site` (string): Named site from connector config. Omit only on reads: multi-site configs fall back to defaultSite (often local/dev, not production). Writes require an explicit site when more than one site is configured. Every response includes `_target` { name, baseUrl, source } (`hint` when you passed site, `default` when you did not).
|
|
18
|
+
- `langcode` (string): Target language (e.g. 'es') to read the unpublished working translation instead of the default language.
|
|
18
19
|
|
|
19
20
|
If a required parameter is missing, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "List live and working translation langcodes for a Drupal node or
|
|
2
|
+
description: "List live and working translation langcodes for a Drupal node, paragraph, or media entity. Uses Sentinel's translation inventory when available (live default revision vs unpublished working draft), including core content_translation_outdated and source when the server sends them. Core JSON:API alone serves one language and cannot prove others are absent. Defaults to node."
|
|
3
3
|
argument-hint: "<type> <id> [site] [entityType]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_list_translations`.
|
|
7
7
|
|
|
8
|
-
List live and working translation langcodes for a Drupal node or
|
|
8
|
+
List live and working translation langcodes for a Drupal node, paragraph, or media entity. Uses Sentinel's translation inventory when available (live default revision vs unpublished working draft), including core content_translation_outdated and source when the server sends them. Core JSON:API alone serves one language and cannot prove others are absent. Defaults to node.
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
|
@@ -15,6 +15,6 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
15
15
|
|
|
16
16
|
**Optional:**
|
|
17
17
|
- `site` (string): Named site from connector config. Omit only on reads: multi-site configs fall back to defaultSite (often local/dev, not production). Writes require an explicit site when more than one site is configured. Every response includes `_target` { name, baseUrl, source } (`hint` when you passed site, `default` when you did not).
|
|
18
|
-
- `entityType` (string): Entity type machine name. Default: 'node'.
|
|
18
|
+
- `entityType` (string): Entity type machine name. Default: 'node'. Use 'paragraph' or 'media' when listing those.
|
|
19
19
|
|
|
20
20
|
If a required parameter is missing, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Update a media entity's name, status, or field values. Partial: omitted fields (status included) are left untouched."
|
|
3
|
-
argument-hint: "<type> <id> [site] [name] [status] [fields]"
|
|
2
|
+
description: "Update a media entity's name, status, or field values. Partial: omitted fields (status included) are left untouched. Pass langcode to continue an unpublished working translation via Sentinel; this does not PATCH canonical langcode and will not create a missing translation — use drupal_create_translation first."
|
|
3
|
+
argument-hint: "<type> <id> [site] [name] [status] [fields] [langcode]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_update_media`.
|
|
7
7
|
|
|
8
|
-
Update a media entity's name, status, or field values. Partial: omitted fields (status included) are left untouched.
|
|
8
|
+
Update a media entity's name, status, or field values. Partial: omitted fields (status included) are left untouched. Pass langcode to continue an unpublished working translation via Sentinel; this does not PATCH canonical langcode and will not create a missing translation — use drupal_create_translation first.
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
|
@@ -17,6 +17,7 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
17
17
|
- `site` (string): Named site from connector config. Omit only on reads: multi-site configs fall back to defaultSite (often local/dev, not production). Writes require an explicit site when more than one site is configured. Every response includes `_target` { name, baseUrl, source } (`hint` when you passed site, `default` when you did not).
|
|
18
18
|
- `name` (string)
|
|
19
19
|
- `status` (boolean (true/false)): Published flag. Only sent when provided; requires allowPublish when true.
|
|
20
|
-
- `fields` (object (pass as JSON)): Field values. Entity-reference values in JSON:API linkage shape ({ data: { type, id } }) are sent as relationships automatically.
|
|
20
|
+
- `fields` (object (pass as JSON)): Field values. Entity-reference values in JSON:API linkage shape ({ data: { type, id } }) are sent as relationships automatically. Image alt is meta.alt on field_media_image with the existing file UUID.
|
|
21
|
+
- `langcode` (string): Target language for an unpublished working translation (e.g. 'es'). Continues that translation via Sentinel; does not create a missing translation and does not PATCH canonical langcode.
|
|
21
22
|
|
|
22
23
|
If a required parameter is missing, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.18.0] - 2026-09-16
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Nullable module inputs (#320).** The generic registry accepts composed nullable
|
|
14
|
+
schemas while retaining runtime type, length, required and unknown-field checks.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **Module compatibility bindings (#317).** Existing config commands and reports
|
|
18
|
+
can use locally approved module tools through generic discovery and schema
|
|
19
|
+
validation. Configured bindings never fall back after a refusal. The live
|
|
20
|
+
verifier probes the configured source tool directly.
|
|
21
|
+
- **Module-owned tools (#317).** An opt-in registry discovers typed Drupal MCP
|
|
22
|
+
tools and routes them through explicit site, scope and operation policy. Module
|
|
23
|
+
actions need no dedicated JavaScript handler. Catalog revisions, schema checks,
|
|
24
|
+
bounded results and refusal without a valid source prevent silent fallbacks.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- Server-tool sessions are bounded and isolated by endpoint, credential and caller.
|
|
28
|
+
Module writes do not automatically retry after an authentication/session refusal.
|
|
29
|
+
- **Remaining writes use `prepareGuardedPatch`.** `langcode` always resolves
|
|
30
|
+
Sentinel inventory and draft-preflights, including unmoderated media.
|
|
31
|
+
`drupal_update_media` + `langcode`, unscoped `drupal_set_moderation_state`,
|
|
32
|
+
and `drupal_revert_revision` share the same preflight / `writeDraft` path
|
|
33
|
+
as node updates. `revisions.js` uses the shared `changedAheadOfRevision`.
|
|
34
|
+
- **One Sentinel HTTP client and fail-closed inventory policy.** Draft,
|
|
35
|
+
translation, and inventory requests share `src/lib/sentinel-draft.js`
|
|
36
|
+
(paths, If-Match, lang/state headers, missing-endpoint classifiers,
|
|
37
|
+
validated inventory). Missing 404/405 stays unavailable / no canonical
|
|
38
|
+
langcode PATCH. Permission, 5xx, and malformed inventory now throw from
|
|
39
|
+
`resolveNodeTranslationPair` instead of falling through to
|
|
40
|
+
`rel:working-copy`. JSON:API advertises `capabilities().sentinelDraft`;
|
|
41
|
+
GraphQL `rawQuery` does not count as Sentinel-capable.
|
|
42
|
+
|
|
43
|
+
## [2.17.0] - 2026-09-11
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
- **Governed media draft translation (#296).** `drupal_create_translation` and
|
|
47
|
+
`drupal_list_translations` accept `entityType: "media"`. Continue with
|
|
48
|
+
`drupal_update_media` + `langcode`; read with `drupal_get_media` + `langcode`.
|
|
49
|
+
Same Sentinel `/mcp-draft` live/working pair as nodes (d.o #3622412). Image
|
|
50
|
+
alt/title are kept on the file relationship (`meta.alt` / `meta.title`) so a
|
|
51
|
+
translator can read the source. Canonical media writes without `langcode` are
|
|
52
|
+
unchanged. Requires MCP Sentinel with media draft routes.
|
|
53
|
+
|
|
54
|
+
### Security
|
|
55
|
+
- Bump transitive `hono` 4.13.1 → 4.13.7 (via `@modelcontextprotocol/node`)
|
|
56
|
+
and override it to `^4.13.5`, clearing three moderate advisories:
|
|
57
|
+
GHSA-gqvv-2mrq-wpjv (`toSSG()` path traversal), GHSA-g6gw-c38x-mqfc
|
|
58
|
+
(`parseBody()` unbounded nesting), and GHSA-crvj-82cr-hjcx (query parser
|
|
59
|
+
after URL fragment). `npm audit` reports 0 vulnerabilities (#299).
|
|
60
|
+
|
|
10
61
|
## [2.16.0] - 2026-09-11
|
|
11
62
|
|
|
12
63
|
### Added
|
|
@@ -1620,6 +1671,7 @@ The connector is now **dual-protocol**: every tool runs against an abstract back
|
|
|
1620
1671
|
- User tools gained explicit PII-access assertions.
|
|
1621
1672
|
- Whole tree lint-clean (`npm run lint`) with object-injection sinks rewritten to safe lookups.
|
|
1622
1673
|
|
|
1674
|
+
[2.17.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.17.0
|
|
1623
1675
|
[2.16.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.16.0
|
|
1624
1676
|
[2.15.2]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.15.2
|
|
1625
1677
|
[2.15.1]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.15.1
|
package/README.md
CHANGED
|
@@ -31,6 +31,11 @@ The connector speaks **two Drupal backends interchangeably** — Drupal core's *
|
|
|
31
31
|
|
|
32
32
|
## Dual-Protocol Backends
|
|
33
33
|
|
|
34
|
+
Drupal modules can also supply their own typed MCP actions through an opt-in
|
|
35
|
+
[module-tool registry](docs/module-tools.md). The module owns business rules and
|
|
36
|
+
schemas; the connector discovers and forwards approved tools under Sentinel and
|
|
37
|
+
connector policy. Existing built-in tools remain available during migration.
|
|
38
|
+
|
|
34
39
|
Each site declares which backend(s) it exposes via the `api` key:
|
|
35
40
|
|
|
36
41
|
```json
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drupal-mcp-connector",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.18.0",
|
|
4
4
|
"description": "Drupal MCP Connector — multi-site MCP server for Drupal with JSON:API and GraphQL, governed writes, draft translations, content tools, audit reports, and an SSH Drush bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -68,6 +68,8 @@
|
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@modelcontextprotocol/node": "^2.0.0",
|
|
70
70
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
71
|
+
"ajv": "^8.20.0",
|
|
72
|
+
"ajv-formats": "^3.0.1",
|
|
71
73
|
"graphql": "^17.0.0",
|
|
72
74
|
"jose": "^6.2.9",
|
|
73
75
|
"node-fetch": "^3.3.2",
|
|
@@ -83,6 +85,7 @@
|
|
|
83
85
|
},
|
|
84
86
|
"overrides": {
|
|
85
87
|
"@hono/node-server": "^2.0.5",
|
|
88
|
+
"hono": "^4.13.5",
|
|
86
89
|
"postcss": "^8.5.18"
|
|
87
90
|
}
|
|
88
91
|
}
|
package/src/index.js
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
|
|
59
59
|
// Tools — aggregated (single source of truth, side-effect-free) and per-tool prompts
|
|
60
60
|
import { allDefinitions, allHandlers, definitionsByName } from "./tools/index.js";
|
|
61
|
+
import { createModuleToolRegistry, isModuleTool } from "./lib/module-tools.js";
|
|
61
62
|
import { buildToolPrompts, getToolPromptMessages } from "./lib/tool-prompts.js";
|
|
62
63
|
|
|
63
64
|
// Apply config/secrets.map (or the shipped example table) before any site
|
|
@@ -104,6 +105,18 @@ const RESOURCES = [
|
|
|
104
105
|
},
|
|
105
106
|
];
|
|
106
107
|
|
|
108
|
+
const moduleTools = createModuleToolRegistry();
|
|
109
|
+
|
|
110
|
+
async function discoverableTools() {
|
|
111
|
+
const sites = listResolvableSiteConfigs();
|
|
112
|
+
const identity = getRequestIdentity();
|
|
113
|
+
const governed = await filterDiscoverableTools(allDefinitions, sites);
|
|
114
|
+
return [
|
|
115
|
+
...filterToolsByPrincipal(governed, sites, identity),
|
|
116
|
+
...await moduleTools.list({ sites, identity }),
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
107
120
|
/**
|
|
108
121
|
* Resolve a resource URI to its JSON payload. URIs are matched in order; the
|
|
109
122
|
* templated forms (content-types, security-policy) capture the site name and
|
|
@@ -114,13 +127,6 @@ const RESOURCES = [
|
|
|
114
127
|
* @returns {Promise<object>} The resource data (later JSON-serialized).
|
|
115
128
|
* @throws {Error} If the URI matches no known resource.
|
|
116
129
|
*/
|
|
117
|
-
async function discoverableTools() {
|
|
118
|
-
const sites = listResolvableSiteConfigs();
|
|
119
|
-
const identity = getRequestIdentity();
|
|
120
|
-
const governed = await filterDiscoverableTools(allDefinitions, sites);
|
|
121
|
-
return filterToolsByPrincipal(governed, sites, identity);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
130
|
async function readResource(uri) {
|
|
125
131
|
const identity = getRequestIdentity();
|
|
126
132
|
const sites = listResolvableSiteConfigs();
|
|
@@ -295,7 +301,9 @@ const buildConnectorServer = createConnectorServerFactory({
|
|
|
295
301
|
tools: {
|
|
296
302
|
definitions: allDefinitions,
|
|
297
303
|
list: discoverableTools,
|
|
298
|
-
call:
|
|
304
|
+
call: (name, args, context) => isModuleTool(name)
|
|
305
|
+
? moduleTools.call(name, args, context)
|
|
306
|
+
: callTool(name, args, context),
|
|
299
307
|
},
|
|
300
308
|
resources: {
|
|
301
309
|
definitions: RESOURCES,
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
* @property {"full"|"enum"|"none"} sort Server-side sort support: arbitrary
|
|
24
24
|
* fields ("full"), a fixed enum of keys ("enum"), or none ("none").
|
|
25
25
|
* @property {boolean} revisions Adapter exposes entity revisions.
|
|
26
|
+
* @property {boolean} [sentinelDraft] Adapter can issue Sentinel's JSON:API
|
|
27
|
+
* `/mcp-draft` and `/mcp-translations` routes (`rawQuery({ path, options })`
|
|
28
|
+
* plus `resourcePath`). GraphQL `rawQuery({ query })` must be false.
|
|
26
29
|
* @property {((entityType: string, bundle: string) => string[])|null} fieldAvailability
|
|
27
30
|
* Optional resolver returning the known field names for a bundle, or null
|
|
28
31
|
* when the adapter cannot cheaply enumerate fields.
|
|
@@ -65,6 +65,7 @@ export class GraphqlBackend extends Backend {
|
|
|
65
65
|
return {
|
|
66
66
|
read: true, write: false, delete: false,
|
|
67
67
|
count: false, filter: false, sort: "enum", revisions: false,
|
|
68
|
+
sentinelDraft: false,
|
|
68
69
|
fieldAvailability: (entityType, bundle) => this._fieldNames(entityType, bundle),
|
|
69
70
|
};
|
|
70
71
|
}
|
package/src/lib/canonical.js
CHANGED
|
@@ -65,14 +65,19 @@ export function normalizeRelationship(ref) {
|
|
|
65
65
|
// JSON:API encodes type as "entityType--bundle"; split into the two parts.
|
|
66
66
|
const [entityType = null, bundle = null] = (ref.type || "").split("--");
|
|
67
67
|
const out = { id: ref.id, entityType, bundle };
|
|
68
|
-
// ERR identifiers carry the revision id in JSON:API `meta` (#192).
|
|
69
|
-
//
|
|
70
|
-
//
|
|
68
|
+
// ERR identifiers carry the revision id in JSON:API `meta` (#192). Image
|
|
69
|
+
// fields carry alt/title the same way (#296). Dropping either made a
|
|
70
|
+
// canonical re-read look like a plain {id, type}.
|
|
71
71
|
if (ref.meta && typeof ref.meta === "object") {
|
|
72
|
-
const
|
|
72
|
+
const entries = new Map(Object.entries(ref.meta));
|
|
73
|
+
const meta = {};
|
|
74
|
+
const vid = entries.get("target_revision_id");
|
|
73
75
|
if (vid !== undefined && vid !== null && vid !== "") {
|
|
74
|
-
|
|
76
|
+
meta.target_revision_id = vid;
|
|
75
77
|
}
|
|
78
|
+
if (typeof entries.get("alt") === "string") meta.alt = entries.get("alt");
|
|
79
|
+
if (typeof entries.get("title") === "string") meta.title = entries.get("title");
|
|
80
|
+
if (Object.keys(meta).length > 0) out.meta = meta;
|
|
76
81
|
}
|
|
77
82
|
return out;
|
|
78
83
|
}
|
package/src/lib/dispatch.js
CHANGED
|
@@ -18,6 +18,7 @@ import { inferOperation } from "./operations.js";
|
|
|
18
18
|
import { assertSourceGovernance, GovernanceError, GOVERNANCE_DIAGNOSTIC_TOOLS } from "./governance.js";
|
|
19
19
|
import {
|
|
20
20
|
assertPrincipalEntitlement, callerTargetHints, getRequestIdentity,
|
|
21
|
+
principalHasScope, resolveAuthoritativeTarget,
|
|
21
22
|
} from "./principal.js";
|
|
22
23
|
import { assertExplicitSiteForWrite, withResolvedTarget } from "./site-target.js";
|
|
23
24
|
import { buildDataFlowContext, consumeBudgetIfEnforced, runWithDataFlow } from "./data-flow.js";
|
|
@@ -62,6 +63,15 @@ export function resolveCallTarget(toolName, rawArgs, context = {}) {
|
|
|
62
63
|
|
|
63
64
|
const identity = context.identity !== undefined ? context.identity : getRequestIdentity();
|
|
64
65
|
if (identity) {
|
|
66
|
+
if (context.moduleTool) {
|
|
67
|
+
if (!principalHasScope(identity, context.moduleTool.scope)) {
|
|
68
|
+
throw new SecurityError("Not entitled to invoke this module tool.");
|
|
69
|
+
}
|
|
70
|
+
return resolveAuthoritativeTarget(rawArgs, identity,
|
|
71
|
+
context.sites ?? listResolvableSiteConfigs(), {
|
|
72
|
+
grants: context.grants, defaultSite: context.defaultSite,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
65
75
|
return assertPrincipalEntitlement({
|
|
66
76
|
toolName,
|
|
67
77
|
args: rawArgs,
|