Skip to content

Commit 08610a9

Browse files
fix(rest)!: /meta list refuses a bracketed object key on views and a bracketed package key on the list and book tree (#29298)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9EdXbpw3tnxagrfuK2otRVU)_ Fixes #29199 Clause-②: no (narrowing) Metadata-protocol: none Skills-docs: none — no skill or doc page teaches a bracketed `object` or `package` key (`git grep` over `skills/`, `content/docs/` and `apps/docs/` finds none); the pages that name `?object=` and `?package=` teach the plain spelling, which is unchanged. ## 维护者速读 - **改了什么**:视图列表 `GET /meta/view` 收到括号写法的 `object`(`?object[]=NAME`、`?object[0]=NAME`)时答 400 `VALIDATION_ERROR` 并点名该键;`/meta` 列表与 book tree 收到括号写法的 `package`(`?package[]=ID`、`?package[0]=ID`)时同样答 400。以前这些键被忽略,返回全部视图或跨全部包的列表。 - **为什么改**:按 qs 风格序列化数组的客户端只想取一个对象的视图或一个包的内容,却拿回全部,还会当作它要的那一份来用;和 #29046 修好的 `?id[]=` 是同一类静默放宽。 - **风险与回滚**:只动 `packages/rest`(列表链一步 + 两个读门入口的同一判定,两条传输共用),`patch` 带 BREAKING 横幅;只影响发括号键的 API 调用方,`@objectstack/client` 与控制台都不发。所有写门本来就以未知参数拒收 `package[]`,本 PR 不改写行为。回滚即还原本 PR。 - **先看**:`packages/rest/src/meta-item-read-gate.ts`(`refuseBracketedListFilter`、`metaBracketedPackageRefusal` 与第 4 步的调用);`packages/rest/src/meta-collection-routes.ts` 列表门入口新增的几行;`.changeset/29199-bracketed-object-package-refused.md`。 - **两分钟验证**:`pnpm --filter @objectstack/rest exec vitest run src/meta-app-publish-gate.test.ts src/meta-query-arity-doors.test.ts`,预期全绿,其中 `BRACKETED view object[…]` 与 `BRACKETED package` 用例断言 400。 - **需要你做的**:无,席位落地。 ## What changes `?object[]=lead`, `?object[0]=lead`, `?package[]=ID` and `?package[0]=ID` reach the `/meta` doors under the key as spelled: every adapter hands query keys through unparsed (`readQuery` in `plugin-hono-server`, `readDispatchQuery` in `@objectstack/hono`). The view switcher reads `query.object` and the package binding reads `query.package`, so neither saw them, and the caller got every view, or the list across every package, with 200. - `packages/rest/src/meta-item-read-gate.ts` - #29046's `refuseBracketedAppIdFilter` becomes one helper keyed by parameter, `refuseBracketedListFilter(query, param, names, lists)`. Step 3 asks it for `id` on `app` and step 4 now asks it for `object` on `view`. The `id` answer is byte-identical; the sentence builder is now `listFilterRefusalMessage`, and the empty `?id=` / `?object=` messages are byte-identical too (their existing pins pass unchanged). - `object` (new): `Query parameter 'object[]' names no object; the 'object' filter takes exactly one object name, spelled ?object=NAME. The list was not served: a bracketed 'object' key is not a filter. Name one object, or leave the parameter out to list every view you can see.` - `metaBracketedPackageRefusal(door, query)` (new, module-level, not added to the package index): on the two arity doors that read `package` (the list and the book tree) a key that opens `package[` is refused as data, rule `bracketed-parameter`, in the same sentence frame. `metaQueryArityRefusal` asks it after the multiplicity rule, so the runtime dispatcher's list and tree get it with no runtime source change. - list: `Query parameter 'package[]' names no package; the 'package' filter takes exactly one package id, spelled ?package=ID. The list was not served: a bracketed 'package' key is not a filter. Name one package, or leave the parameter out to list across every package.` The tree says `The tree was not served` and `read the tree across every package`. - `packages/rest/src/meta-collection-routes.ts` (list) and `packages/rest/src/meta-item-read-routes.ts` (book tree): `RestServer`'s two doors ask the same decision right after `refuseRepeatedQueryParams`, before the binding is read, and write it in the nested envelope their arity gate writes. - A bracketed key beside the plain one is refused on all three (two spellings of one filter). **Measured: which doors does each seam serve (order item 2, 3).** | Door | `?object[]=` | `?package[]=` on main | This PR | |:--|:--|:--|:--| | `GET /meta/view`, `/meta/views` (list chain step 4) | 200, every view | 200, every package | 400 / 400 | | `GET /meta/:type`, any other type | not read (like `?object=`) | 200, every package | unchanged / 400 | | `GET /meta/book/:name/tree` | n/a | 200, every package | 400 | | `GET /meta/:type/:name`, `/layers` | n/a | 400 unknown parameter (closed set) | unchanged | | `PUT` / `DELETE /meta/:type/:name`, `POST …/publish` (write doors) | n/a | 400 unknown parameter (closed set), protocol never called | unchanged | | `GET /meta/diagnostics` | n/a | 400 unknown parameter (closed set) | unchanged | Step 4 is the only list step that reads `object`, keyed on the folded `view` (`views` is covered). The write doors were measured before any change (a scratch probe driving each `RestServer` route with `?package[]=`; the dispatcher's save and item read ask the same closed sets through `metaItemQueryRefusal`), so no write behaviour changes here. The order's "at the binding's one seam" could not be taken literally: `metaItemPackageBinding(raw)` receives the VALUE of `query.package`, which is `undefined` for a bracketed key, so it cannot see the key. The seam both transports ask before every arity-door read of `package` is the arity decision, which is where the refusal sits; `rest-server.ts` (the binding's home) is untouched. **Producer census (order item 5).** `@objectstack/client` builds `?package=` with `URLSearchParams.set` (both `meta.getItems` surfaces) and sends no `object` on the list; the console (`ui/**`, read only) has no qs library and no bracketed `object`/`package` builder; no docs or skill example spells either key bracketed. No real producer sends them. ## Tests - `packages/rest/src/meta-app-publish-gate.test.ts`: `it.each(['object[]', 'object[0]', 'object[name]'])` on `view` and `views` for three caller postures asserts 400, `code`, the full sentence and no list; a bracketed key beside a plain `object` is 400; controls: `?object=lead` switches, no `object` lists all, and a bracketed `object` key on `app` is not read (200). - `packages/rest/src/meta-query-arity-doors.test.ts`: the decision as data for `package[]`, `package[0]`, `package[id]` on the list and the tree (beside a plain `package` too); a repeated plain `package` still answers the multiplicity sentence first; not judged on `_drafts`, `state` or `packages[`; `RestServer`'s list and tree answer the nested 400 and `getMetaItems` is never called; controls: the plain `package` still scopes, none reads every package. - `packages/runtime/src/domains/dispatcher-query-arity.test.ts`: the dispatcher's list and tree refuse `package[]` / `package[0]` with the same code and sentence before any read, and its list chain refuses `object[]`. - `packages/runtime/src/domains/meta-list-projection-parity.test.ts`: probes `?object[]=lead`, `?object[0]=lead`, `?package[]=crm` on the list census (every type × caller, both transports), `?package[]=crm` on the item, tree and layers censuses; the "reference moves" test asserts `RestServer`'s 400 for `object[]` and `package[]`. - `packages/qa/dogfood/test/meta-read-doors-live.dogfood.test.ts` (booted showcase over Hono), appended `describe`: `/meta/view?object[]=` / `?object[0]=showcase_task` and `/meta/object?package[]=` / `?package[0]=com.example.showcase` answer 400 `VALIDATION_ERROR` naming the key, no items, as the admin and as a member; controls: `?object=showcase_task` is that object's views and fewer than all, `?package=com.example.showcase` is narrower than no package. No existing test was edited to pass; every hunk in a test file is an addition. <details><summary>Red first on main (45e9b2f)</summary> The pins were committed before the fix (919e71b) and run against main's source and dist: - rest: `Tests 13 failed | 58 passed (71)` — the three `BRACKETED view object[…]` cases (`expected 200 to be 400`) and the ten bracketed-`package` cases. Every control passed. - runtime: `Tests 6 failed | 1174 passed (1180)` — the five dispatcher cases and "the reference moves". The census rows stayed green, as expected: on main both transports gave the same unfiltered answer. - dogfood: the reds are read off the ablation legs below (the same code as main for each arm). </details> <details><summary>Ablation: each refusal, put back, reds only its own pins</summary> Through `scripts/ablation-replace.mjs` (wrap mode), `pnpm --filter @objectstack/rest build`, then `ablation-dist-preflight.mjs` before reading any run. 1. **object arm.** The step-4 call `if (metaType === 'view') refuseBracketedListFilter(query, 'object', 'object', 'view');` replaced by a marker statement. Anchor 1 → 0, blob `894bc70c` → `aae79d48`; marker present in 2 built files. rest `3 failed | 32 passed (35)` (the three `object[…]` cases); runtime `2 failed | 1178 passed (1180)` (the dispatcher `object[]` case and the reference test); dogfood `2 failed | 17 passed (19)` (`/meta/view?object[]=` and `?object[0]=`). 2. **package arm.** `metaBracketedPackageRefusal`'s door check replaced by an unconditional `return undefined`. Anchor 1 → 0, blob `894bc70c` → `2a7a9483`; marker present in 2 built files. rest `10 failed | 26 passed (36)` (the ten bracketed-`package` cases); runtime `5 failed | 1175 passed (1180)` (the four dispatcher list/tree cases and the reference test); dogfood `2 failed | 17 passed (19)` (`/meta/object?package[]=` and `?package[0]=`). Each restore: blob back to HEAD `894bc70c`, `git diff HEAD` empty, rebuild exit 0, marker absent from all 6 built files, whole tree clean. </details> ## Acceptance notes - **Premise.** Holds as the card states it, and wider: the bracketed `package` key also reads as "every package" on the list (every type) and on the book tree. - **File surface vs the claim.** The claim named `meta-item-read-gate.ts` and, if `package[]` red, the binding's home (`rest-server.ts`). The fix lives in `meta-item-read-gate.ts` plus three-line door hand-offs in `meta-collection-routes.ts` and `meta-item-read-routes.ts`; `rest-server.ts` is untouched (see "Measured" for why the binding cannot see the key). The runtime parity and dispatcher arity tests gained pins; no runtime source changed. - **Two dialects, as before.** The view `object[]` refusal is thrown inside the list chain after the per-caller gate, so `RestServer` writes the flat `{ error, code }` body #29046's `id[]` refusal writes. The `package[]` refusal is answered at the door entry, where `package` is read, so `RestServer` writes the nested `{ error: { code, message } }` its arity gate writes. Status and `code` are identical in both. - **Same class, not fixed here (report for the seat).** `GET /meta/_drafts` reads `?packageId=` and `?type=` with no closed set, so a bracketed `packageId[]` or `type[]` very likely reads as every package or every type; `?preview[]=draft` on the list reads as no preview (the published list) and `?include[]=` as no inclusion. Not measured here; they are outside the card's keys. ## Verification Every local reading below is at HEAD `1ee7e4f5` (the fix `34997b8f` plus a merge of `origin/main`), in a worktree on `claude/project-thread-9szbaq`. Families CI runs are cited from this PR's check-runs in the report, not re-run here. <details><summary>Package suites and typecheck (HEAD 1ee7e4f)</summary> - `pnpm --filter @objectstack/rest test`: `VERDICT command-exit 0` · `Test Files 367 passed (367)` · `Tests 6980 passed | 332 skipped (7312)` - `pnpm --filter @objectstack/rest typecheck`: exit 0 - `pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/domains/dispatcher-query-arity.test.ts src/domains/meta-list-projection-parity.test.ts`: exit 0 · `Tests 1180 passed (1180)` - `pnpm --filter @objectstack/runtime typecheck`: exit 0; `pnpm --filter @objectstack/dogfood typecheck`: exit 0 - lint: root eslint with `--no-inline-config` over `packages/rest`, `packages/runtime`, `packages/qa/dogfood`: exit 0, no findings </details> <details><summary>Full dogfood battery (the narrowing owes it)</summary> `pnpm --filter @objectstack/dogfood test` under the verify lock, after the `@objectstack/dogfood^...` closure build on this head: `VERDICT command-exit 0` · `Test Files 389 passed | 3 skipped (392)` · `Tests 3220 passed | 28 skipped (3248)`. </details> <details><summary>Derived gate set</summary> `node scripts/pm/dispatch-gates.mjs --commands` (from the merge-base, at `1ee7e4f5`): 64 commands, all exit 0. `--ran` reconciliation: `64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED`. Three first-pass exit 3s were prerequisites, not reds, each re-run green: `check-plugin-teardown-shape.mjs --self-test` (its pinned fixture commit fetched on this shallow clone), `check:dual-build-cjs-loads` (7 packages outside the dogfood closure had no `dist/`; built, then `115 published require entry point(s) across 70 package(s)` load) and `check:type-check-debt` (its closure build queued behind the dogfood battery on the shared lock; re-run under the lock, `0 raw tsc error(s)`). </details> - **Not run locally:** `test:repo` (the vitest `repo` projects, CI `Test Core`). The one `repo`-project file this diff edits, `meta-list-projection-parity.test.ts`, was run directly (above). - `origin/main` has moved 22 commits since the merge; none touches a file this PR edits (`git diff --name-only` over the range), so the queue's re-run against current main is the remaining check. --- _Generated by [Claude Code](https://claude.ai/code/session_01ADHkmPfJsJeWNxeaZkvyEM)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3ec2647 commit 08610a9

9 files changed

Lines changed: 342 additions & 31 deletions
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/rest': patch
3+
---
4+
5+
fix(rest)!: a bracketed `object` key on the view list (`?object[]=NAME`, `?object[0]=NAME`) and a bracketed `package` key on the `/meta` list and book tree (`?package[]=ID`, `?package[0]=ID`) are refused 400 naming the key instead of being ignored and answered with every view or every package
6+
7+
Clause-②: no (narrowing) — `GET /meta/view` / `GET /meta/views` with any query key that opens `object[`, and `GET /meta/:type` / `GET /meta/book/:name/tree` with any query key that opens `package[` (on `RestServer` and on the runtime dispatcher, which share the list chain and the arity decision), are refused with `400 VALIDATION_ERROR`; they answered 200 with every view, or across every package, before. No package export, answer member, error code or status is added; the shared refusal record's `rule` tag names one more reason (`bracketed-parameter`), which no transport writes to the wire.
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped: a list or tree request that spells its one-object filter or its one-package scope as an array key is refused instead of served unfiltered. There is nothing for a migration to rewrite and no tombstone. -->
11+
12+
**BREAKING** accept-set narrowing on the `/meta` list and book tree reads (`@objectstack/rest`).
13+
14+
- **Before.** `?object[]=lead` and `?object[0]=lead` (the qs-style array spellings) reached the view list under the key as spelled, which the view switcher never read, so a caller who asked for one object's views got every view it may see. `?package[]=ID` and `?package[0]=ID` reached the list and the book tree the same way, which read no package, so a caller who named one package was answered across every package.
15+
- **Now.** The list chain refuses a key that opens `object[` on the view list, after the per-caller visibility gate, as it refuses a bracketed `id` on the app list: `400 VALIDATION_ERROR`, message `Query parameter 'object[]' names no object; the 'object' filter takes exactly one object name, spelled ?object=NAME. …`, and no list in the body. The list and the book tree refuse a key that opens `package[` at their entry, before any read: `400 VALIDATION_ERROR`, message `Query parameter 'package[]' names no package; the 'package' filter takes exactly one package id, spelled ?package=ID. …`. A bracketed key beside the plain one is refused too.
16+
- **Unchanged.** `?object=NAME` switches to that object's views; `?package=ID` scopes to that package (`?package=all` and no `package` read every package); an empty `?object=` and a repeated `object` or `package` keep their 400s; a bracketed `object` key on any type but `view` is still ignored, as `?object=` is there. Every by-name `/meta` door (the item read, `/layers`, the save, the reset, `/publish`) and the diagnostics sweep already refused a bracketed `package` key as an unknown name, and still do.
17+
- **Who sees it.** An API client that serialises arrays qs-style. `@objectstack/client` builds `?package=` with `URLSearchParams` (no brackets), and the console sends neither key bracketed.
18+
19+
**The remedy.** FROM `?object[]=NAME` (or `?object[0]=NAME`) TO `?object=NAME`; FROM `?package[]=ID` (or `?package[0]=ID`) TO `?package=ID`; leave the key out to list every view, or every package.

‎packages/qa/dogfood/test/meta-read-doors-live.dogfood.test.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,4 +218,50 @@ describe('[#26572] the v18 /meta read doors, over HTTP on a booted showcase', ()
218218
expect(memberApps, 'a member never sees setup').not.toContain('setup');
219219
});
220220
});
221+
222+
// The same class on the view switcher's `object` and on the list's
223+
// `package` scope: a bracketed key (`?object[]=`, `?package[]=`) reached the
224+
// door under the key as spelled, so the filter never read it, and a caller
225+
// who asked for one object's views (or one package's items) got every view
226+
// (or every package's items). Each is refused: 400 VALIDATION_ERROR naming
227+
// the key, no list.
228+
describe('GET /meta/view with a bracketed `object` key, and a list with a bracketed `package` key, are refused', () => {
229+
const itemsOf = (body: any): any[] => (Array.isArray(body) ? body : body?.items) ?? [];
230+
const OBJ = 'showcase_task';
231+
232+
for (const [path, key] of [
233+
['/meta/view', 'object[]'], ['/meta/view', 'object[0]'],
234+
['/meta/object', 'package[]'], ['/meta/object', 'package[0]'],
235+
] as const) {
236+
it(`${path}?${key}=…: 400 VALIDATION_ERROR naming the key, for the admin and for a member`, async () => {
237+
const value = key.startsWith('object') ? OBJ : PKG;
238+
const names = key.startsWith('object') ? 'object' : 'package';
239+
for (const [who, token] of [['admin', admin], ['member', liaison]] as const) {
240+
const res = await stack.apiAs(token, 'GET', `${path}?${key}=${encodeURIComponent(value)}`);
241+
const body: any = await res.json();
242+
expect({ who, status: res.status, code: codeOf(body) }, JSON.stringify(body).slice(0, 400))
243+
.toEqual({ who, status: 400, code: 'VALIDATION_ERROR' });
244+
expect(messageOf(body)).toMatch(new RegExp(`^Query parameter '${key.replace(/[[\]]/g, '\\$&')}' names no ${names}`));
245+
expect(itemsOf(body)).toEqual([]);
246+
}
247+
});
248+
}
249+
250+
it('CONTROLS — ?object=NAME is that object\'s views and ?package=ID that package\'s items, each narrower than no filter', async () => {
251+
const all = itemsOf(await (await stack.apiAs(admin, 'GET', '/meta/view')).json());
252+
const switched = await stack.apiAs(admin, 'GET', `/meta/view?object=${OBJ}`);
253+
expect(switched.status).toBe(200);
254+
const views = itemsOf(await switched.json());
255+
expect(views.length, 'the showcase ships views of showcase_task').toBeGreaterThan(0);
256+
expect(views.every((v: any) => v?.object === OBJ), JSON.stringify(views.map((v: any) => v?.object))).toBe(true);
257+
expect(views.length).toBeLessThan(all.length);
258+
259+
const everyPackage = itemsOf(await (await stack.apiAs(admin, 'GET', '/meta/object')).json());
260+
const scoped = await stack.apiAs(admin, 'GET', `/meta/object?package=${encodeURIComponent(PKG)}`);
261+
expect(scoped.status).toBe(200);
262+
const objects = itemsOf(await scoped.json());
263+
expect(objects.length, 'the showcase package declares objects').toBeGreaterThan(0);
264+
expect(objects.length).toBeLessThan(everyPackage.length);
265+
});
266+
});
221267
});

‎packages/rest/src/meta-app-publish-gate.test.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,52 @@ describe('#7566 — `GET /meta/app?id=` narrows the list instead of being droppe
374374
expect(namesFrom(res.body)).toEqual(['all_leads']);
375375
});
376376

377+
// The same class one step down the chain: `?object[]=lead` or
378+
// `?object[0]=lead` reached the view switcher under the key as spelled,
379+
// so the filter, which reads `query.object`, never saw it and the caller
380+
// who asked for one object's views got every view it may see. Refused with
381+
// the bracketed `id`'s answer, naming the key as sent; the dispatcher's
382+
// twin is pinned in `@objectstack/runtime`'s list parity census.
383+
it.each(['object[]', 'object[0]', 'object[name]'])('a BRACKETED view `%s` key is refused with 400 naming it — never every view', async (key) => {
384+
const views = async ({ type: t }: any) =>
385+
String(t ?? '').startsWith('view')
386+
? [{ name: 'all_leads', viewKind: 'list', object: 'lead' }, { name: 'acct_view', viewKind: 'list', object: 'account' }]
387+
: [];
388+
for (const perms of [['studio.access'], ['manage_users'], []]) {
389+
for (const type of ['view', 'views']) {
390+
const { rest, protocol } = setup(perms);
391+
protocol.getMetaItems = vi.fn(views);
392+
const res = await getList(rest, type, { [key]: 'lead' });
393+
394+
expect(res.statusCode, `${type}, caller holding [${perms}]`).toBe(400);
395+
expect(res.body?.code).toBe('VALIDATION_ERROR');
396+
expect(String(res.body?.error)).toBe(
397+
`Query parameter '${key}' names no object; the 'object' filter takes exactly one object name, `
398+
+ `spelled ?object=NAME. The list was not served: a bracketed 'object' key is not a filter. `
399+
+ 'Name one object, or leave the parameter out to list every view you can see.',
400+
);
401+
expect(res.body?.items).toBeUndefined();
402+
expect(Array.isArray(res.body)).toBe(false);
403+
}
404+
}
405+
// A bracketed key beside a plain one is two spellings of one filter.
406+
const { rest, protocol } = setup(['manage_users']);
407+
protocol.getMetaItems = vi.fn(views);
408+
const both = await getList(rest, 'view', { object: 'lead', [key]: 'account' });
409+
expect(both.statusCode).toBe(400);
410+
expect(both.body?.code).toBe('VALIDATION_ERROR');
411+
// Controls: the plain spelling still switches, an absent one lists all.
412+
expect(namesFrom((await getList(rest, 'view', { object: 'lead' })).body)).toEqual(['all_leads']);
413+
expect(namesFrom((await getList(rest, 'view')).body).sort()).toEqual(['acct_view', 'all_leads']);
414+
});
415+
416+
it('a bracketed `object` key on another list type is not read, as no other `object` spelling is (control)', async () => {
417+
const res = await getList(setup(['manage_users']).rest, 'app', { 'object[]': 'lead' });
418+
419+
expect(res.statusCode).toBe(200);
420+
expect(namesFrom(res.body).sort()).toEqual(['account', 'crm']);
421+
});
422+
377423
it('a WHITESPACE id keeps its answer — a value no app is named, so an empty list (#28548 control)', async () => {
378424
const { rest } = setup(['manage_users']);
379425
const res = await getList(rest, 'app', { id: ' ' });

‎packages/rest/src/meta-collection-routes.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,16 @@ export function registerMetaCollectionRoutes(this: MetaCollectionRouteHost, scop
648648
// [#23214] The list lives beside the shared decision, so the
649649
// runtime dispatcher's twin refuses the same names.
650650
if (refuseRepeatedQueryParams(req, res, metaReadGate.META_QUERY_ARITY_DOORS.list)) return;
651+
// A bracketed `package` key (`?package[]=`) is read by no
652+
// binding below, so it scoped nothing: refused here, by the
653+
// decision the dispatcher's twin asks too.
654+
const bracketedPackage = metaReadGate.metaBracketedPackageRefusal('list', req.query);
655+
if (bracketedPackage) {
656+
res.status(bracketedPackage.status).json({
657+
error: { code: bracketedPackage.code, message: bracketedPackage.message },
658+
});
659+
return;
660+
}
651661
// [#22188] Read through {@link metaItemPackageBinding}:
652662
// `all`, this list's "show everything" scope, names no package.
653663
const packageId = metaItemPackageBinding(req.query?.package);

0 commit comments

Comments
 (0)