Skip to content

Commit 7986d97

Browse files
os-trumpclaude
andauthored
Retire compound-name metadata addressing — un-mount the three :section arities and unify SDK URL spelling (#12176 stage 3) (#12378)
* feat(rest,runtime,client)!: retire compound-name metadata addressing Stage 3 of the #12176 maintainer-ruled retirement of slash-bearing metadata item names. Stage 1 (#12194) declared the item-name grammar and refuses every slash-bearing name at the publish door, so the compound arities addressed only names that can no longer be created — this removes them. packages/rest: - Un-mount the three compound arities: GET and PUT /meta/:type/:section/:name and GET /meta/:type/:section/:name/published. Drop their three route-ledger rows and the ordering pins that existed only because the three-segment compound route was a CATCH-ALL shadowing every literal sibling. packages/runtime: - Un-fold the dispatcher: the /published branch requires exactly three segments and the item branch exactly two, instead of re-joining every trailing segment into one slash-bearing key. - Decode the :name segment. This dispatcher splits the RAW path and nothing decodes for it, unlike the Hono routes in packages/rest — measured, not assumed. Without it a pre-grammar residue row would be addressable through REST and not through the dispatcher, breaking #12194's landed acceptance criterion that stored junk names stay listable and clearable. The sibling domains/packages.ts already decodes its own id segments this way. packages/client: - One URL spelling: encodeURIComponent on every /meta item address, closing the 10-unencoded / 4-encoded split. Encoding a name that satisfies #12194's grammar is a no-op, so this is byte-identical for every writable name; a residue name now reaches the single-segment door as %2F rather than forking the request onto a second door. - Correct the docblocks that promised unencoded compound pass-through, and the #11712 mode carve-out, which is closed at the source rather than here. Capability is re-expressed, not removed: %2F matches the single-segment pattern and Hono decodes the parameter back to the stored spelling, so residue rows still read, write and delete through the surviving doors. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H9StxQgG2DPA26XzZZqnJB * test(rest,runtime,client): rework the compound-arity pins for the removal Every pin that measured the retired compound-name arities is REWORKED, never silenced. Two shapes, chosen per file by what is actually still true: INVERTED — files whose subject was a shadowing or divergence hazard now pin the ABSENCE of the arity, because a re-mount is how the hazard returns and an order pin phrased against a retired route would go green while the retirement is undone: - meta-route-registration-order: the three-segment catch-all ordering pins become "no compound `:section` arity is mounted", plus a companion pin that the literals it used to shadow are all still mounted. - meta-item-layered-route, meta-plural-i18n, rest-server-meta-write-org-scope, meta-compound-save-{mode,force}-parity, meta-compound-save-and-reset- capability-gate, meta-501-envelope: same inversion, each keeping the reason the original pin existed (a re-mounted door arrives ungated, org-blind, and reading neither `?force` nor `?mode` until someone re-derives that work). RE-AIMED at the surviving door — coverage that was never about the spelling: - the full #11712 `?mode` and #11095 `?force` contracts, the #6877 repeated- parameter guard, the #7019 capability gate and the #8842 falsy-body hole now run against `/meta/:type/:name`. - meta-published-overlay's residue-row read drives the single-segment `/published` route with the decoded name — the pin that the retirement costs no capability. - client URL pins invert to `%2F`, each with a control proving a grammar-legal name is byte-identical on the wire. NEW post-removal pins, ADR-0112 code AND status on every one: - the compound path answers `404 ROUTE_NOT_FOUND` at the dispatcher; - the ENCODED spelling still reaches the item-name grammar's `400 INVALID_REQUEST`, so "the route is gone" and "the name is illegal" stay distinguishable; - `DELETE` on a compound path answers 404, not the 405 its two-segment address gets — the two refusals are different facts. Also: the meta domain's tail `{ handled: false }` was unreachable dead code until this card (0, 1 and — through the fold — every 2+ segment path were all covered). Un-folding makes it reachable, so it becomes a LOCATED `routeNotFound` rather than letting the adapter answer an anonymous 404 on the very shape this retirement newly produces. Same form `domains/ai.ts` and `domains/share-links.ts` already use. Changeset: minor on rest/runtime/client with the BREAKING route table and the FROM → TO spelling, per the launch-window convention. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H9StxQgG2DPA26XzZZqnJB * test(rest,dogfood): pay the six new TEST_DEBT errors and rework the dogfood compound-door rows (#12195) The pin rework left four dead constants and two off-type boot calls in the rest test layer — TEST_DEBT measured 161 against the frozen 155; all six are fixed, none ledgered. The dogfood anonymous-deny table drops the retired compound-save row (its registered-door leg asserts .not.toBe(404), which is what a retired route answers) and gains the retired-door case: the compound spelling must 404 for anonymous and member alike, since an auth floor only speaks for a door that exists. The authz-conformance matrix note re-tallies six -> five with the retirement named. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H9StxQgG2DPA26XzZZqnJB --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c4a27b7 commit 7986d97

29 files changed

Lines changed: 1042 additions & 1140 deletions
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
'@objectstack/rest': minor
3+
'@objectstack/runtime': minor
4+
'@objectstack/client': minor
5+
---
6+
7+
Retire compound-name metadata addressing (`/meta/:type/:section/:name`)
8+
9+
Stage 3 of the maintainer-ruled retirement of slash-bearing metadata item names.
10+
Stage 1 declared the item-name grammar and refuses every slash-bearing name at
11+
the publish door, so the routes removed here addressed only names that can no
12+
longer be created.
13+
14+
**BREAKING — three public REST routes stop answering:**
15+
16+
| stops answering | use instead |
17+
| :-- | :-- |
18+
| `GET /api/v1/meta/:type/:section/:name` | `GET /api/v1/meta/:type/:name` |
19+
| `PUT /api/v1/meta/:type/:section/:name` | `PUT /api/v1/meta/:type/:name` |
20+
| `GET /api/v1/meta/:type/:section/:name/published` | `GET /api/v1/meta/:type/:name/published` |
21+
22+
Each retired route folded its `:section` and `:name` segments back into one
23+
slash-bearing key (`views/all_leads`) that the protocol layer then treated as a
24+
single opaque string — the section half was never stored, filtered or
25+
enumerated. A request to a retired path now answers `404 ROUTE_NOT_FOUND`.
26+
27+
The `@objectstack/runtime` dispatcher stops folding in the same way: its
28+
`/meta` handler requires exactly two path segments for an item and three for
29+
`…/published`, instead of re-joining every trailing segment. A `/meta` path
30+
that matches no route now answers a located `404 ROUTE_NOT_FOUND` rather than
31+
falling through to the adapter's anonymous 404.
32+
33+
**FROM → TO for callers.** Address every item through the single-segment route
34+
and percent-encode the name:
35+
36+
```
37+
GET /api/v1/meta/lead/views/all_leads → GET /api/v1/meta/lead/views%2Fall_leads
38+
```
39+
40+
`@objectstack/client` now calls `encodeURIComponent` on every `/meta` item
41+
address, so SDK callers need no change: the SDK already sends the new spelling.
42+
Encoding is a **no-op** for every name the item-name grammar admits (lowercase
43+
snake_case segments, optionally dot-qualified), so the bytes on the wire are
44+
unchanged for every name that can be written today.
45+
46+
A pre-grammar **residue** row whose stored name contains a slash remains
47+
readable, writable and deletable: `%2F` matches the single-segment pattern and
48+
the parameter is decoded back to the stored spelling before the handler runs.
49+
Nothing that could be stored has become unaddressable.
50+
51+
Two SDK doc comments that promised "compound names pass through unencoded"
52+
(`meta.getPublished`, `meta.publishItem`) are corrected, and the
53+
`SaveMetaItemOptions.mode` carve-out — `{ mode: 'draft' }` was silently ignored
54+
at the compound door and published live — is closed at the source: there is one
55+
door, and it reads every member of the options bag.
56+
57+
<!-- adr-0087: not-required (already-registered metadata-item-name-grammar-enforced) the stage-1 semantic entry already names this exact surface — "the compound `:type/:section/:name` fold" — and carries the re-authoring prescription (dot-qualified, or flattened with an underscore). This stage removes the routes that fold; it adds no new authorable shape and no second migration prescription. -->

packages/client/src/client.test.ts

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -321,20 +321,46 @@ describe('ObjectStackClient', () => {
321321
);
322322
});
323323

324-
it('meta.getItem/saveItem pass compound names through unencoded (reaches /meta/:type/:section/:name)', async () => {
324+
it('[#12195] meta.getItem/saveItem ENCODE the name — one spelling, one door', async () => {
325325
const { client, fetchMock } = createMockClient({ success: true, data: { name: 'views/all_leads' } });
326+
// ⚠️ This pin is INVERTED from what it was, and the inversion is the
327+
// card. It used to require the slash to survive UNENCODED, so the
328+
// request would reach the compound handler
329+
// `/meta/:type/:section/:name` instead of collapsing onto the
330+
// two-segment route. #12176 retired compound-name addressing: that
331+
// handler is gone, so `%2F` is now the correct and only spelling.
332+
//
333+
// Encoding is a no-op for every name #12194's grammar admits (snake
334+
// case, optionally dot-qualified), so this changes nothing a legal
335+
// caller sends. What it changes is a pre-grammar residue name: it now
336+
// reaches the surviving door with its slash intact as `%2F`, which Hono
337+
// decodes back to `views/all_leads` — the capability that used to
338+
// require a second route.
326339
await client.meta.getItem('object', 'views/all_leads');
327-
// The slash must survive: %2F would collapse the request onto the
328-
// 3-segment /meta/:type/:name route and miss the compound handler.
329340
expect(String(fetchMock.mock.calls[0][0])).toBe(
330-
'http://localhost:3000/api/v1/meta/object/views/all_leads',
341+
'http://localhost:3000/api/v1/meta/object/views%2Fall_leads',
331342
);
332343
await client.meta.saveItem('object', 'views/all_leads', { label: 'All leads' });
333344
expect(String(fetchMock.mock.calls[1][0])).toBe(
334-
'http://localhost:3000/api/v1/meta/object/views/all_leads',
345+
'http://localhost:3000/api/v1/meta/object/views%2Fall_leads',
335346
);
336347
expect(fetchMock.mock.calls[1][1].method).toBe('PUT');
337348
});
349+
350+
it('[#12195] a LEGAL name is byte-identical before and after the encoding change', async () => {
351+
// The other half: unifying on `encodeURIComponent` must not have moved
352+
// the wire for any name a caller can actually write. Dotted and flat
353+
// snake_case both pass through untouched.
354+
const { client, fetchMock } = createMockClient({ success: true, data: {} });
355+
await client.meta.getItem('object', 'crm_lead');
356+
await client.meta.getItem('view', 'crm_lead.pipeline');
357+
expect(String(fetchMock.mock.calls[0][0])).toBe(
358+
'http://localhost:3000/api/v1/meta/object/crm_lead',
359+
);
360+
expect(String(fetchMock.mock.calls[1][0])).toBe(
361+
'http://localhost:3000/api/v1/meta/view/crm_lead.pipeline',
362+
);
363+
});
338364
});
339365

340366
describe('Reports namespace (#3587 gap closure)', () => {
@@ -2662,14 +2688,15 @@ describe('[#11391] meta.saveItem query string (unscoped client)', () => {
26622688
);
26632689
});
26642690

2665-
it('a compound name keeps its unencoded slash AND gets the query string', async () => {
2691+
it('[#12195] a slash-bearing name is ENCODED and still gets the query string', async () => {
26662692
const { client, fetchMock } = createMockClient({ success: true });
26672693
await client.meta.saveItem('object', 'views/all_leads', { label: 'All leads' }, { force: true });
2668-
// The slash must still survive (%2F would collapse this onto the
2669-
// 3-segment route and miss `PUT /meta/:type/:section/:name`), and the
2670-
// compound door reads `?force` too since #11095.
2694+
// Inverted by #12195: the slash used to be required to survive raw so
2695+
// the request reached `PUT /meta/:type/:section/:name`, which had read
2696+
// `?force` since #11095. That door is retired; `%2F` reaches the
2697+
// surviving door, which has always read `?force`.
26712698
expect(String(fetchMock.mock.calls[0][0])).toBe(
2672-
'http://localhost:3000/api/v1/meta/object/views/all_leads?force=true',
2699+
'http://localhost:3000/api/v1/meta/object/views%2Fall_leads?force=true',
26732700
);
26742701
});
26752702
});
@@ -2878,16 +2905,15 @@ describe('[#11713] meta.saveItem sends the If-Match header (unscoped client)', (
28782905
expect(JSON.parse(init.body)).toEqual({ name: 'customer' });
28792906
});
28802907

2881-
it('OCC-guards a COMPOUND name too — unlike `mode`, this reaches both doors', async () => {
2908+
it('[#12195] OCC-guards a slash-bearing name too, at the one surviving door', async () => {
28822909
const { client, fetchMock } = createMockClient({ success: true });
28832910
await client.meta.saveItem('object', 'views/all_leads', { label: 'All leads' }, { ifMatch: OCC_TOKEN });
2884-
// The compound-name door `PUT /meta/:type/:section/:name` reads
2885-
// `if-match` and strips ETag quotes exactly as the single-segment door
2886-
// does — measured in rest-server.ts. `mode` is the member that does NOT
2887-
// reach it; this one does, so the slash must survive AND the pin must
2888-
// ride along.
2911+
// This case used to say "unlike `mode`, this reaches BOTH doors" — the
2912+
// compound door read `if-match` while never reading `mode`. There is
2913+
// one door now, so every member of the options bag reaches it and the
2914+
// per-member carve-out is gone. The name is encoded like every other.
28892915
expect(String(fetchMock.mock.calls[0][0])).toBe(
2890-
'http://localhost:3000/api/v1/meta/object/views/all_leads',
2916+
'http://localhost:3000/api/v1/meta/object/views%2Fall_leads',
28912917
);
28922918
expect(headersOfCall(fetchMock)['If-Match']).toBe(OCC_TOKEN);
28932919
});

packages/client/src/index.ts

Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -601,11 +601,11 @@ export interface SaveMetaItemOptions {
601601
* empty spelling would pin the write against the empty string and refuse
602602
* every save with a 409 the caller never asked for.
603603
*
604-
* ✅ REACHES BOTH DOORS — unlike `mode` below. The compound-name twin
605-
* `PUT /meta/:type/:section/:name` reads `if-match` and strips ETag-style
606-
* quotes exactly as the single-segment door does, so
607-
* `saveItem('object', 'views/all_leads', item, { ifMatch })` is
608-
* OCC-guarded like any other save.
604+
* [#12195] There is ONE door now. The compound-name twin
605+
* `PUT /meta/:type/:section/:name` — which this note used to pair with —
606+
* is retired, and every name reaches `PUT /meta/:type/:name`
607+
* percent-encoded, so `if-match` behaviour no longer varies by how the
608+
* name is spelled.
609609
*
610610
* Same member name, same header, same truthy guard as the sibling
611611
* first-party `@object-ui/data-objectstack` `MetadataClient.save`, whose
@@ -645,21 +645,21 @@ export interface SaveMetaItemOptions {
645645
* on the wire that the server ignores. Same shape the first-party
646646
* `@object-ui/data-objectstack` `MetadataClient.save` already uses.
647647
*
648-
* ⚠️ COMPOUND NAMES DO NOT STAGE. `mode` reaches only the single-segment
649-
* `PUT /meta/:type/:name`. Its compound-name twin
650-
* `PUT /meta/:type/:section/:name` — the door a `name` containing a slash
651-
* lands on, e.g. `saveItem('object', 'views/all_leads', item)` — never
652-
* reads this parameter, so `{ mode: 'draft' }` there is IGNORED and the
653-
* write is PUBLISHED LIVE, answered 200. It is not refused; there is no
654-
* signal at the call site. Filed as objectstack#11712 and deliberately not
655-
* repaired from this side: threading it is the route's decision, and a
656-
* client-side guess would be a second place the two doors disagree.
648+
* [#12195] REACHES EVERY SAVE — the carve-out this note used to carry is
649+
* GONE, and it is worth recording why rather than deleting it silently.
657650
*
658-
* ⛔ Do not "fix" this by rejecting compound names here. `force` and
659-
* `packageId` DO reach both doors (measured: the compound handler reads
660-
* and threads `?force` since objectstack#11095 and `?package` alongside
661-
* it), so refusing the whole bag on a compound name would break the two
662-
* parameters that work in order to warn about the one that does not.
651+
* `mode` used to reach only the single-segment `PUT /meta/:type/:name`.
652+
* A `name` containing a slash landed on the compound-name twin
653+
* `PUT /meta/:type/:section/:name`, which never read this parameter — so
654+
* `{ mode: 'draft' }` there was IGNORED and the write was PUBLISHED LIVE,
655+
* answered 200, with no signal at the call site (objectstack#11712).
656+
*
657+
* Two changes closed it at the source rather than from this side. Stage 1
658+
* (#12194) made a slash-bearing name unwritable at all, and this stage
659+
* retired the twin and unified this file on `encodeURIComponent`, so every
660+
* save now arrives at the one door that reads `mode`. A name that would
661+
* once have forked to the silent-publish door is now refused `400
662+
* INVALID_REQUEST` by the grammar — loud, at the door, before any write.
663663
*/
664664
mode?: 'draft' | 'publish';
665665
}
@@ -911,7 +911,7 @@ export class ObjectStackClient {
911911
const params = new URLSearchParams();
912912
if (options?.packageId) params.set('package', options.packageId);
913913
const qs = params.toString();
914-
const url = `${this.baseUrl}${route}/${type}/${name}${qs ? `?${qs}` : ''}`;
914+
const url = `${this.baseUrl}${route}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs ? `?${qs}` : ''}`;
915915
const res = await this.fetch(url);
916916
return this.unwrapResponse<GetMetaItemResponse>(res);
917917
},
@@ -953,10 +953,16 @@ export class ObjectStackClient {
953953
// omits the `headers` key altogether, so a save without `ifMatch`
954954
// hands `fetch` the same `init` it always did.
955955
const headers = metaSaveHeaders(options);
956-
// `type`/`name` stay UNENCODED — a compound name's slash must survive
957-
// so the request reaches `PUT /meta/:type/:section/:name` instead of
958-
// collapsing onto the 3-segment route (pinned in client.test.ts).
959-
const res = await this.fetch(`${this.baseUrl}${route}/${type}/${name}${query}`, {
956+
// [#12195] ENCODED, like every other `/meta` item address in this file.
957+
// This site used to leave `type`/`name` RAW so a compound name's slash
958+
// would survive into a separate path segment and reach
959+
// `PUT /meta/:type/:section/:name`. That door is retired, and encoding
960+
// is now the single spelling: a legal name (#12194's grammar — snake
961+
// case, optionally dot-qualified) contains nothing `encodeURIComponent`
962+
// alters, so this is byte-identical for every name that can be written,
963+
// and a pre-grammar residue name reaches the single-segment door with
964+
// its slash intact as `%2F` instead of forking the request.
965+
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${query}`, {
960966
method: 'PUT',
961967
body: JSON.stringify(item),
962968
...(headers ? { headers } : {}),
@@ -1090,13 +1096,19 @@ export class ObjectStackClient {
10901096
*/
10911097

10921098
/**
1093-
* ADR-0033: the published version of a metadata item. Compound names are
1094-
* passed through unencoded (e.g. `getPublished('lead', 'views/all_leads')`),
1095-
* matching how `getItem` addresses sub-resources.
1099+
* ADR-0033: the published version of a metadata item.
1100+
*
1101+
* [#12195] The name is percent-encoded, like every other `/meta` item
1102+
* address in this file. This docblock used to promise the opposite — that
1103+
* a compound name passed through UNENCODED, `getPublished('lead',
1104+
* 'views/all_leads')`, so its slash would reach the compound arity
1105+
* `GET /meta/:type/:section/:name/published`. That arity is retired and a
1106+
* slash-bearing name is refused at the publish door (#12194), so there is
1107+
* one spelling and one door.
10961108
*/
10971109
getPublished: async (type: string, name: string) => {
10981110
const route = this.getRoute('metadata');
1099-
const res = await this.fetch(`${this.baseUrl}${route}/${type}/${name}/published`);
1111+
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/published`);
11001112
return this.unwrapResponse<any>(res);
11011113
},
11021114

@@ -1177,7 +1189,7 @@ export class ObjectStackClient {
11771189
*/
11781190
getReferences: async (type: string, name: string) => {
11791191
const route = this.getRoute('metadata');
1180-
const res = await this.fetch(`${this.baseUrl}${route}/${type}/${name}/references`);
1192+
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/references`);
11811193
return this.unwrapResponse<any>(res);
11821194
},
11831195

@@ -1200,15 +1212,16 @@ export class ObjectStackClient {
12001212
getAudit: async (type: string, name: string, opts?: { limit?: number }) => {
12011213
const route = this.getRoute('metadata');
12021214
const qs = opts?.limit !== undefined ? `?limit=${opts.limit}` : '';
1203-
const res = await this.fetch(`${this.baseUrl}${route}/${type}/${name}/audit${qs}`);
1215+
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/audit${qs}`);
12041216
return this.unwrapResponse<any>(res);
12051217
},
12061218

12071219
/**
12081220
* ADR-0033: promote a single item's pending draft overlay to live —
12091221
* the per-item flow beside `packages.publishDrafts`' package-scoped one.
1210-
* 404 [no_draft] when there is nothing to publish. Compound names pass
1211-
* through unencoded, like `getItem`.
1222+
* 404 [no_draft] when there is nothing to publish. [#12195] The name is
1223+
* percent-encoded, like `getItem` — this line used to promise unencoded
1224+
* pass-through for compound names, whose arity is now retired.
12121225
*
12131226
* The resolved `version` is the ADR-0008 optimistic-concurrency token, the
12141227
* same carrier `saveItem` returns and with the same job: pass it back as
@@ -1229,7 +1242,7 @@ export class ObjectStackClient {
12291242
opts?: { message?: string },
12301243
): Promise<PublishMetaItemResponse> => {
12311244
const route = this.getRoute('metadata');
1232-
const res = await this.fetch(`${this.baseUrl}${route}/${type}/${name}/publish`, {
1245+
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/publish`, {
12331246
method: 'POST',
12341247
body: JSON.stringify(opts?.message ? { message: opts.message } : {}),
12351248
});
@@ -1241,7 +1254,7 @@ export class ObjectStackClient {
12411254
*/
12421255
rollbackItem: async (type: string, name: string, toVersion: number, opts?: { message?: string }) => {
12431256
const route = this.getRoute('metadata');
1244-
const res = await this.fetch(`${this.baseUrl}${route}/${type}/${name}/rollback`, {
1257+
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/rollback`, {
12451258
method: 'POST',
12461259
body: JSON.stringify({ toVersion, ...(opts?.message ? { message: opts.message } : {}) }),
12471260
});
@@ -1258,7 +1271,7 @@ export class ObjectStackClient {
12581271
if (opts?.from !== undefined) params.set('from', String(opts.from));
12591272
if (opts?.to !== undefined) params.set('to', String(opts.to));
12601273
const qs = params.toString();
1261-
const res = await this.fetch(`${this.baseUrl}${route}/${type}/${name}/diff${qs ? `?${qs}` : ''}`);
1274+
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/diff${qs ? `?${qs}` : ''}`);
12621275
return this.unwrapResponse<any>(res);
12631276
}
12641277
};
@@ -5581,7 +5594,7 @@ export class ScopedProjectClient {
55815594
const params = new URLSearchParams();
55825595
if (options?.packageId) params.set('package', options.packageId);
55835596
const qs = params.toString();
5584-
const res = await this.parent._fetch(this.url(`/meta/${type}/${name}${qs ? `?${qs}` : ''}`));
5597+
const res = await this.parent._fetch(this.url(`/meta/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs ? `?${qs}` : ''}`));
55855598
return this.parent._unwrap<GetMetaItemResponse>(res);
55865599
},
55875600
/**
@@ -5608,7 +5621,7 @@ export class ScopedProjectClient {
56085621
// Header half of the same bag, through the same one builder the twin
56095622
// calls — see {@link metaSaveHeaders}.
56105623
const headers = metaSaveHeaders(options);
5611-
const res = await this.parent._fetch(this.url(`/meta/${type}/${name}${query}`), {
5624+
const res = await this.parent._fetch(this.url(`/meta/${encodeURIComponent(type)}/${encodeURIComponent(name)}${query}`), {
56125625
method: 'PUT',
56135626
body: JSON.stringify(item),
56145627
...(headers ? { headers } : {}),

0 commit comments

Comments
 (0)