Skip to content

Commit 3299873

Browse files
fix(rest, runtime)!: the /meta action doors and MCP list_actions serve an action entry as the object door projects it (ADR-0106) (#31104)
security#1 · os-tesla · session_01L9qibvynRbCtMndW8EHNGc Clause-②: no (narrowing) — the `/meta` `action` reads (the list, the item read, `/layers` with its `?layers=true` spelling, and `/published`, on both transports) and MCP `list_actions` now answer a caller only the action entries the `/meta` object door's projection serves that caller, which ADR-0106 D1 ("a field the caller cannot read does not exist for that caller, on any plane") already promises, so no stated contract is narrowed; what narrows is the answer to a caller with a `readable: false` field. Nothing is widened: no answer member, error code or status is added; the new exports are the shared projection the doors ask. Metadata-protocol: none Skills-docs: no move — `skills/**` teaches no `/meta` action read; `content/docs/ai/connect-mcp.mdx` and `content/docs/deployment/environment-variables.mdx` state the projection. ## 维护者速读 - **改了什么**:`/meta` 的 `action` 读取(列表、单项、`/layers` 及 `?layers=true`、`/published`,REST 与 runtime dispatcher 两端)与 MCP `list_actions`,对每个独立动作条目按 `/meta` 对象门对该调用者的 ADR-0106 投影判定:参数绑定了调用者不可读字段的动作(按参数所指对象判定:有 `objectOverride` 取其目标,否则取动作的 `objectName`;含无所属对象的 global 动作)不再列出,单项读取的回答与不存在的名字逐字节相同;`list_actions` 对 `objectOverride` 参数的摘要改读目标对象的字段。 - **为什么改**:ADR-0106 D1 要求不可读字段在任何门都不存在;`/meta/object` 已隐去这类动作,但独立 `action` 条目的各个读取门与 MCP 的 global 动作仍未投影。 - **风险与回滚**:对带 `readable: false` 字段的调用者是收窄(标题带 `!`);动作所属对象的字段可见性无法评估时与对象门一样拒绝(`FIELD_VISIBILITY_UNRESOLVED` / 503)。无限制调用者、D4 豁免者(`manage_metadata`、system)、无 security 服务或关闭掩码的部署逐字节不变;执行门(MCP `run_action`、`POST /actions`)及其参数校验与拒绝文案不变。回滚即 revert 本 PR。 - **先看**:`packages/rest/src/meta-item-read-gate.ts`(`projectMetaActionForCaller` 及列表 7a、单项 4a、分层 3a 三步)、`packages/runtime/src/domains/mcp-object-schema-mask.test.ts`(新增的最后一个 describe)。 - **两分钟验证**:`pnpm --filter @objectstack/runtime exec vitest run src/domains/mcp-object-schema-mask.test.ts`,预期 `Tests 45 passed (45)`。 - **需要你做的**:无,席位落地。 ## What changes - `packages/rest/src/meta-item-read-gate.ts` — `projectMetaActionForCaller(sources, action, ownerName)`: one action entry as this caller is served it. The action is projected as its own object's only `actions` entry through `projectMetaObjectSchema` over the posture related to that document (the object door's `maskDeniedFieldReferences` → `actionParamReadsDenied`, `objectOverride` judged on the object it names), not a second rule. A `passthrough` posture serves the entry with no object read. An action with no own-object definition has only its `objectOverride` params judged, each on the caller's posture for the object it names; a posture that cannot be evaluated or is undetermined withholds it. - The list chain (step 7a) leaves a withheld entry out. The item chain (step 4a), the layered chain (step 3a, every present layer) and both `/published` doors answer it as a name with nothing behind it. `projectMetaActionItem` is the item-read entry the cached arm and the published doors ask. - `packages/rest/src/meta-item-read-routes.ts` — the cached item arm (the default door for `action`) asks the same projection, and judges `If-None-Match` after it, so a withheld entry is answered `404`, never `304`. - `packages/metadata-core/src/object-schema-fls.ts` — `actionObjectOverrideTargets` and `actionServedWithoutOwnObject`, the object door's own rule for an action with no own-object definition. - `packages/runtime/src/domains/mcp.ts` — `servedActionObject` delegates to `projectMetaActionForCaller`, so an object-less action is judged on the objects its `objectOverride` params name; `listActions` hands `summarizeAction` the served object of each override target. - `packages/runtime/src/action-execution.ts` — `summarizeAction`, `summarizeActionParams` and `resolveDeclaredActionParams` take an optional object lookup and read an `objectOverride` param's field on the object it names; a target the lookup does not resolve reads the own object's field, as before. `enforceActionParams` passes no lookup, so the run doors' validation and refusal text are unchanged. - `packages/rest/src/rest-server.ts`, `packages/runtime/src/domains/meta.ts` — each transport hands the item, layered and published chains its `object` masker (`resolveObjectMasker`). - Docs: `connect-mcp.mdx` and `environment-variables.mdx` state the projection. Changeset: a patch for `@objectstack/metadata-core`, `@objectstack/rest` and `@objectstack/runtime`, the BREAKING banner and the ADR-0087 `not-required` disposition. ## Tests A new top-level `describe` in `packages/runtime/src/domains/mcp-object-schema-mask.test.ts`: the real `SecurityPlugin` on a real `ObjectQL` over a real `SqlDriver`, the protocol over that engine, `RestServer`'s registered handlers (cached arm, and a deployment with the cache off) and the runtime dispatcher, plus the MCP bridge. - A member denied a bound field (an own-object `field`, and a global action's `objectOverride` field) is not listed the action, and its item, `/layers`, `?layers=true` and `/published` reads answer byte-identical to an unknown name, on every door. None of the field's label, help text or the param's label appears in the list. - The cached arm answers a member holding the staff's validator `404`, and the staff still revalidates `304`. - MCP `list_actions` withholds the global action; an `objectOverride` param is summarised with the target field's label and type. - Controls: a readable-field action, a field-less action and a readable global override are served to the member as to the staff; an author holding `manage_metadata` and the system context are answered byte-identical to the same caller with no security service, on every sub-door. - Unit pins: `resolveDeclaredActionParams` with and without the lookup (and `enforceActionParams` unchanged); `actionObjectOverrideTargets` and `actionServedWithoutOwnObject`. ## Acceptance notes - Ablating the projection (`projectMetaActionForCaller` returns `serve` first) turns the 16 refused legs red (list, item, `/layers`, `?layers=true`, `/published` per door, the `304` leg, MCP global, and the two #31075 legs) while the 29 controls stay green. Ablating the target-field lookup turns the two summary pins red. - The `/meta` doors read an action's own object from its spec `objectName`; `@objectstack/rest` cannot reach objectql's legacy `object` rung, so an entry carrying only that key is judged as object-less there. ## Verification All on head `c97eb416db` (origin/main merged in), under the verify lock: - `pnpm --filter @objectstack/metadata-core test` — Test Files 25 passed (25), Tests 966 passed (966); `typecheck` exit 0. - `pnpm --filter @objectstack/rest test` — Test Files 393 passed (393), Tests 7486 passed, 332 skipped (7818); `typecheck` exit 0. - `pnpm --filter @objectstack/runtime test` — Test Files 553 passed (553), Tests 8909 passed, 19 skipped (8928); `typecheck` exit 0. - `pnpm --filter @objectstack/runtime exec vitest run src/domains/mcp-object-schema-mask.test.ts` — Tests 45 passed (45). - Dogfood battery (`pnpm --filter '@objectstack/dogfood^...' build`, then `pnpm --filter @objectstack/dogfood test`) — Test Files 434 passed, 3 skipped (437), Tests 3596 passed, 41 skipped (3637). - `node scripts/pm/dispatch-gates.mjs --commands` — 93 derived gate commands, all exit 0 (`check:skill-examples` and `check:dual-build-cjs-loads` re-run after their build prerequisite); `--ran` reconciles 93 derived, 93 run, 0 unrun. --- _Generated by [Claude Code](https://claude.ai/code/session_01L9qibvynRbCtMndW8EHNGc)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0ba68b6 commit 3299873

15 files changed

Lines changed: 870 additions & 36 deletions
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/metadata-core': patch
3+
'@objectstack/rest': patch
4+
'@objectstack/runtime': patch
5+
---
6+
7+
fix(rest, runtime)!: the /meta action doors and MCP list_actions serve an action entry as the object door projects it (ADR-0106)
8+
9+
Clause-②: no (narrowing) — the `/meta` `action` reads (the list, the item read, `/layers` with its `?layers=true` spelling, and `/published`, on both transports) and MCP `list_actions` now answer a caller only the action entries the `/meta` object door's projection serves that caller, which ADR-0106 D1 ("a field the caller cannot read does not exist for that caller, on any plane") already promises, so no stated contract is narrowed; what narrows is the answer to a caller with a `readable: false` field. Nothing is widened: no answer member, error code or status is added; the new exports are the shared projection the doors ask.
10+
Metadata-protocol: none
11+
Skills-docs: no move — `skills/**` teaches no `/meta` action read; `content/docs/ai/connect-mcp.mdx` and `content/docs/deployment/environment-variables.mdx` state the projection.
12+
13+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema and no stored shape changes: the action doors answer a field-restricted caller the action entries the metadata object door already serves them. There is nothing for a migration to rewrite and no tombstone. -->
14+
15+
**BREAKING** for a field-restricted caller of the `/meta` `action` reads and of MCP `list_actions` over HTTP (`/api/v1/mcp`).
16+
17+
- **Now.** Each standalone `action` entry the `/meta` doors serve is projected through the object door's ADR-0106 posture and projection for this caller, as its own object's only action (`projectMetaActionForCaller` in `@objectstack/rest`, the one rule every action door asks). Each param is judged on the object it names: its `objectOverride` when set, else the action's `objectName`. An entry that projection withholds is left out of `GET /api/v1/meta/action`, and `GET /api/v1/meta/action/:name`, its `/layers` (and `?layers=true`) and its `/published` answer it exactly as they answer a name with nothing behind it. A layered read with any layer withheld is answered that way too. The cached item arm judges a conditional request after that projection, so a withheld entry is never answered `304`.
18+
- **Object-less actions.** An action with no own object (a `global` action) whose `objectOverride` param binds a field the caller cannot read on the object it names is withheld the same way, on the `/meta` doors and in MCP `list_actions`.
19+
- **Param summaries.** MCP `list_actions` summarises a param bound through `objectOverride` with the field of the object it names (its label and type), not the action's own object's field of the same name; a target that names no object the bridge holds is summarised as before.
20+
- **Refusal.** Where the caller's field visibility on an action's own object cannot be evaluated, or the projection would leave that object with no field, the read is refused as the object door refuses it: `FIELD_VISIBILITY_UNRESOLVED` / 503.
21+
- **Unchanged.** A caller who may read every field, a D4-exempt caller (`manage_metadata`, the system context) and a deployment with no `security` service or with the mask switched off are answered byte for byte as before; so is every action whose params bind only readable fields, or none. The run doors (MCP `run_action`, `POST /actions`) and their param validation and refusal text are unchanged.

‎content/docs/ai/connect-mcp.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,8 @@ drive this MCP server. Same word, different layer.
401401
scope name only the fields the caller can read, the same field set
402402
`GET /api/v1/meta/object/:name` serves that caller. `list_actions` follows
403403
the same projection: an action that object door withholds from the caller
404-
(one whose param binds a field the caller cannot read) is not listed, and a
404+
(one whose param binds a field the caller cannot read, judged on the object
405+
the param names, so an object-less action too) is not listed, and a
405406
listed action's params carry no label, help text or options of such a
406407
field. Sparse results or denied
407408
writes usually mean governance is *working*, not that the connection is

‎content/docs/deployment/environment-variables.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ that bypassed write hooks, `rebuildSearchCompanion` (from
339339
|:---|:---|:---|:---|
340340
| `OS_MARKETPLACE_CACHE` | enum | `on` | `off` disables the in-memory marketplace listing cache. |
341341
| `OS_MARKETPLACE_PUBLIC_BASE_URL` | url | — | Public base URL of the marketplace registry (proxied from this runtime when set). |
342-
| `OS_ALLOW_UNMASKED_OBJECT_METADATA` | boolean | `false` | Escape hatch for the metadata-plane field-level security mask ([ADR-0106](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0106-metadata-plane-fls-object-schema-masking.md) D8). By default every object schema served by `/meta` and `/metadata` is projected onto the fields the **calling user** may read, so a field they cannot read does not appear at all — not its name, label, type, picklist options, formula, `visibleWhen` predicate, `defaultValue`, or the `requiredPermissions` capability guarding it. Set to `1` to serve the full schema to every authenticated caller, as releases before this one did. This changes **disclosure only**: the data plane still masks values and refuses forbidden writes either way, and the console reads field affordances from `/auth/me/permissions`, so toggling it never changes UI correctness. The REST layer also honours a per-server `metadata.maskObjectFields: false`; this variable is the deployment-wide knob and covers the runtime `/metadata` dispatcher, which has no REST config to read. |
342+
| `OS_ALLOW_UNMASKED_OBJECT_METADATA` | boolean | `false` | Escape hatch for the metadata-plane field-level security mask ([ADR-0106](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0106-metadata-plane-fls-object-schema-masking.md) D8). By default every object schema served by `/meta` and `/metadata` is projected onto the fields the **calling user** may read, so a field they cannot read does not appear at all — not its name, label, type, picklist options, formula, `visibleWhen` predicate, `defaultValue`, or the `requiredPermissions` capability guarding it; an `action` entry whose param binds such a field is answered as a name that does not exist. Set to `1` to serve the full schema to every authenticated caller, as releases before this one did. This changes **disclosure only**: the data plane still masks values and refuses forbidden writes either way, and the console reads field affordances from `/auth/me/permissions`, so toggling it never changes UI correctness. The REST layer also honours a per-server `metadata.maskObjectFields: false`; this variable is the deployment-wide knob and covers the runtime `/metadata` dispatcher, which has no REST config to read. |
343343
| `OS_METADATA_WRITABLE` | csv | — (none) | Comma-separated metadata type names (e.g. `hook,job`) granted a runtime escape hatch that treats them as `allowEnvironmentOverlay: true` for items **no managed package ships**: it opens runtime creation of a type whose registry entry allows none. It never opens an organization-scoped write: since [ADR-0131](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0131-total-organization-ownership-no-null-organization-id.md) D6 the metadata protocol refuses every organization-scoped write with `403 NOT_OVERRIDABLE`. It **never opens an item a managed package ships**: managed content is sealed ([ADR-0131](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0131-total-organization-ownership-no-null-organization-id.md) D6), so an overlay or a removal of a shipped flow, object, field, permission set, position or any other type without an environment overlay is refused with `403 NOT_OVERRIDABLE` whether the hatch is set or not. Customize managed content through its type's own route instead: an environment overlay for `view`, `dashboard`, `report`, `translation` and `email_template`; disable, or clone under a new name, for a flow; clone for a permission set. An overlay row this hatch wrote before over a managed item is kept at rest but **not served**: every read takes the package's definition, and boot names each such row, per type (`[metadata_sealed_overlay_unserved]`), with the two remedies — re-express the change as a new item under a new name, or delete the row. A stored fork of a packaged permission set is the one exception: the deployment does not boot while it holds one (ADR-0048); `os migrate security-catalog-overlays` lists such rows and, with `--apply`, deletes them, which restores the package's definition. A stored row over a managed item of the other types that merge overlays at read (`position`, `page`, `app`, `dataset`, `book`, `tool`, `skill`) can still be removed, but removing it changes nothing that is served: no read serves it, so the package's definition is already served. See [ADR-0005](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0005-metadata-customization-overlay.md). |
344344

345345
---

‎packages/metadata-core/src/object-schema-fls.test.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import { describe, it, expect } from 'vitest';
99
import {
1010
ObjectSchemaMaskEvaluationError,
11+
actionObjectOverrideTargets,
12+
actionServedWithoutOwnObject,
1113
applyObjectSchemaMask,
1214
foldVisibilityFingerprintIntoEtag,
1315
isObjectSchemaMaskExempt,
@@ -428,3 +430,30 @@ describe('[#22250] the D4 exemption covers the definition, not the runtime view'
428430
expect(noService).toEqual({ kind: 'passthrough', reason: 'exempt' });
429431
});
430432
});
433+
434+
describe('[ADR-0106 D1] an action with no own-object definition — its objectOverride params judged on the object each names', () => {
435+
const RATE = { field: 'rate', objectOverride: 'vendor' };
436+
const ACTION = { name: 'quote', params: [RATE, { field: 'name', objectOverride: 'vendor' }, { field: 'code', objectOverride: 'region' }, { name: 'note' }] };
437+
438+
it('names every other object a param reads, once each, in declaration order', () => {
439+
expect(actionObjectOverrideTargets(ACTION)).toEqual(['vendor', 'region']);
440+
expect(actionObjectOverrideTargets(ACTION, 'vendor')).toEqual(['region']);
441+
expect(actionObjectOverrideTargets({ name: 'plain', params: [{ name: 'note' }] })).toEqual([]);
442+
});
443+
444+
it('serves the action only when every bound field is readable on the object it names', () => {
445+
const readable = new Map([['vendor', new Set(['rate', 'name'])], ['region', new Set(['code'])]]);
446+
expect(actionServedWithoutOwnObject(ACTION, undefined, readable)).toBe(true);
447+
const rateDenied = new Map([['vendor', new Set(['name'])], ['region', new Set(['code'])]]);
448+
expect(actionServedWithoutOwnObject(ACTION, undefined, rateDenied)).toBe(false);
449+
});
450+
451+
it('withholds the action when a named object\'s readable set is undetermined (fail closed)', () => {
452+
expect(actionServedWithoutOwnObject(ACTION, undefined, new Map([['vendor', new Set(['rate', 'name'])]]))).toBe(false);
453+
expect(actionServedWithoutOwnObject(ACTION, undefined, new Map([['vendor', new Set(['rate', 'name'])], ['region', undefined]]))).toBe(false);
454+
});
455+
456+
it('an action binding no other object is served as declared', () => {
457+
expect(actionServedWithoutOwnObject({ name: 'plain', params: [{ name: 'note' }] }, undefined, new Map())).toBe(true);
458+
});
459+
});

‎packages/metadata-core/src/object-schema-fls.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,39 @@ export function applyObjectSchemaMask<T>(document: T, posture: ObjectSchemaMaskP
696696
};
697697
}
698698

699+
/**
700+
* [ADR-0106 D1] The objects an action entry's params name through
701+
* `objectOverride` — every object other than `ownerName` — once each, in
702+
* declaration order. These are the objects {@link actionServedWithoutOwnObject}
703+
* judges against.
704+
*/
705+
export function actionObjectOverrideTargets(action: unknown, ownerName?: string): string[] {
706+
const document = ownerName === undefined ? { actions: [action] } : { name: ownerName, actions: [action] };
707+
return [...new Set(objectOverrideReads(document).map((read) => read.object))];
708+
}
709+
710+
/**
711+
* [ADR-0106 D1] Does the `/meta` object door's projection keep this action,
712+
* for an action with no own-object definition to project: an object-less
713+
* (`global`) action, or one whose `objectName` names no object?
714+
*
715+
* Such an action carries no field of its own object, so nothing of it is
716+
* denied there; each param bound through `objectOverride` is judged on the
717+
* object it names, against `related` (the caller's readable fields on that
718+
* object; a missing or `undefined` entry withholds the action, fail closed).
719+
* It is the object door's own rule — {@link maskDeniedFieldReferences} on the
720+
* action as a document's only `actions` entry — not a second one.
721+
*/
722+
export function actionServedWithoutOwnObject(
723+
action: unknown,
724+
ownerName: string | undefined,
725+
related: ReadonlyMap<string, ReadonlySet<string> | undefined>,
726+
): boolean {
727+
const document = ownerName === undefined ? { actions: [action] } : { name: ownerName, actions: [action] };
728+
const served = maskDeniedFieldReferences(document, new Set(), related);
729+
return Array.isArray(served.actions) && served.actions.length === 1;
730+
}
731+
699732
/**
700733
* A stable hash of the caller's **denied** field set for one object (ADR-0106
701734
* D3) — the ETag dimension that keeps `304` semantics correct per permission

‎packages/rest/src/index.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ export {
179179
metaRequestLocale,
180180
metaTypeReadRefusal,
181181
metaTypeWriteRefusal,
182+
projectMetaActionForCaller,
183+
projectMetaActionItem,
182184
projectMetaObjectSchema,
183185
refuseUnknownMetaListType,
184186
STORED_VERSION_DOOR_POLICY,
@@ -187,6 +189,8 @@ export {
187189
wantsMetaItemLayers,
188190
} from './meta-item-read-gate.js';
189191
export type {
192+
MetaActionProjection,
193+
MetaActionProjectionSources,
190194
MetaBookTreeAnswer,
191195
MetaBookTreeSources,
192196
MetaItemAnswer,

‎packages/rest/src/meta-item-history-routes.ts‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ import type { AuditMetaItemRequest, HistoryMetaItemRequest, PublishMetaItemReque
170170
import { canonicalMetaUrlType } from '@objectstack/spec/shared';
171171
import { handleRouteError, sendFieldVisibilityFault, thrownAnswerIsBareNotFound } from './error-response.js';
172172
import type { MetaRouteScope, TransportScopedMetaRequest } from './meta-collection-routes.js';
173-
import { META_ITEM_MEMBER_PARAM, META_ITEM_PUBLISHED_PARAMS, metaMemberSelector } from './meta-item-read-gate.js';
173+
import { META_ITEM_MEMBER_PARAM, META_ITEM_PUBLISHED_PARAMS, metaMemberSelector, projectMetaActionItem } from './meta-item-read-gate.js';
174174
import { refuseUnknownQueryParams } from './query-allowlist.js';
175175
import { refuseRepeatedQueryParams } from './query-multiplicity.js';
176176
import {
@@ -1094,6 +1094,8 @@ export interface MetaItemPublishedRouteHost {
10941094
readonly objectQLProvider: RestServer['objectQLProvider'];
10951095
resolveRequestEnvironmentId: RestServer['resolveRequestEnvironmentId'];
10961096
resolveMetadataService: RestServer['resolveMetadataService'];
1097+
/** [ADR-0106 D1] The `object` list read the `action` projection reads its objects from. */
1098+
metaListSource: RestServer['metaListSource'];
10971099
}
10981100

10991101
/**
@@ -1246,6 +1248,25 @@ export function registerMetaItemPublishedRoute(this: MetaItemPublishedRouteHost,
12461248
} else if (publishedMaskPosture.kind === 'undetermined') {
12471249
res.header('Cache-Control', 'private, no-store');
12481250
}
1251+
// [ADR-0106 D1] An `action` is served as the object
1252+
// door's projection serves it to this caller, as the
1253+
// plain read serves it; a withheld one is this door's
1254+
// answer for a name with nothing published behind it.
1255+
if (publishedMetaType === 'action') {
1256+
const projected = await projectMetaActionItem({
1257+
...this.metaListSource(gateProtocol, environmentId),
1258+
resolveObjectMasker: () => this.resolveObjectMasker(environmentId, req, 'object'),
1259+
}, served);
1260+
if (projected.kind === 'withhold') {
1261+
res.status(404).json({ error: { code: 'NOT_FOUND', message: 'Not found' } });
1262+
return;
1263+
}
1264+
if (projected.kind === 'fault') {
1265+
sendFieldVisibilityFault(res, projected.object);
1266+
return;
1267+
}
1268+
if (projected.cacheControl) res.header('Cache-Control', projected.cacheControl);
1269+
}
12491270
res.json(served);
12501271
};
12511272
// [#20156] The overlay found is SERVED outside the

0 commit comments

Comments
 (0)