Skip to content

Commit 64e07f3

Browse files
committed
docs(spec,skills): 文档与 skill 追平端点执行器(#5040 E9)
执行器已落(#5040 E1–E8),整面硬拒已收窄为五道逐端点 publish 门,但三层散文 仍在讲反话。对升级者(往往只有这段文字的 AI 维护者)那不是「过期」,是把人 指离一个已经能用的能力的**指令**。 spec 内两处处方文本(本单唯一会随包发布的一半): - `App.apis` 墓碑原写「stack 级 defineStack({ apis }) 在 v17 也不可执行 (#4936)……等执行器」。一条重定向式墓碑必须对它指向的地方说真话,而这条 在同一句里把目的地称作死的 —— 顺理成章的下一步就是继续用 handler 代码写 路由。现改为:该面自 protocol 17 起真实服务,点名五道门,并带上抬到 stack 级时必须做对的两件事(carve-out 与显式 `manifest.namespace`,以及 `authRequired` 缺省 true / D6 的已装配 rateLimit 配对义务)。移除那一半 原样保留 —— `App.apis` 从未被读过,仍然是删;#4936 也仍在,历史正是这条 重定向存在的理由。 - `defineStack({ server })` 模块头原写 #5040「wires endpoint-level rateLimit —— still unwired today」。已接线。同时补上服务端预算作者真正需要的关系: 端点桶键在独立命名空间,两份预算各计各的,不共享计数器。 两者经 gen:docs 传播到 content/docs/references/ 的两页,生成物只动这两页。 手写文档:http-protocol.mdx 把「该面无执行器」的 callout 换成真正的 Declarative Endpoints 一节(匹配 → 策略链 → 委派到内建路由同款流水线;五道 门;401 / 429+Retry-After / 成功答案才有的 Cache-Control: private;以及最易 搞错的恒等语义 —— 未匹配路径与**已声明路径上的方法不匹配**都保持传输层裸 404 逐字节不变,因为这条缝是 Hono notFound 而不是注册路由,没有方法集可以 报 405)。quick-reference.mdx 补速查条目。 objectstack-api skill 不再把 ApiEndpointSchema 描述成四臂 type 联合,改为按 现状教学:何时 `apis:` 胜过 `contributes.routes`(以及何时不 —— 真需要 handler 代码时)、carve-out、把五道门当作「跑 objectstack validate」而不是 背诵的条文、D6 的 `enabled === true` 判据、映射键的最小语义。指向 `declarative-apis-endpoints-live` 升级条目而不复述第二套规则。 以上每条都对着已构建的 spec 实测而非相信:文档示例能发布、省略 `authRequired` 的形状解析为 true、`authRequired: false` 旁只写窗口配额的 rateLimit 被带处方拒绝。墓碑新文本以两侧断言钉住(必须说新话 **且** 不得再 说已退休的那句,`#4936` 仍在),空串无法蒙混过关。 Fixes #5238 Part of #5040 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd
1 parent 0161c7f commit 64e07f3

10 files changed

Lines changed: 429 additions & 30 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
docs(spec,skills): the docs and the `objectstack-api` skill catch up with the endpoint executor (#5238)
6+
7+
The executor landed (#5040 E1–E8) and the blanket refusal of a non-empty `apis:`
8+
became five per-endpoint publish gates — but three prose surfaces still told the
9+
reader the opposite. That is worse than stale: each one is an *instruction* an
10+
upgrading author (very often an AI maintainer with nothing but that text) would
11+
follow away from a capability that now works.
12+
13+
**Two prescription texts in `packages/spec` — the shipped half of this change.**
14+
15+
- `App.apis`'s `retiredKey()` tombstone said the stack-level `defineStack({ apis })`
16+
it redirects to "is ALSO not executable in v17 (#4936) … until the endpoint
17+
executor ships". A tombstone that redirects somewhere has to be true about the
18+
place it points at; this one sent the author to a surface it called dead in the
19+
same sentence, so the natural next move was to keep serving the route from
20+
handler code. It now says the surface EXECUTES from protocol 17, names the five
21+
gates, and carries the two things to get right when moving a declaration up a
22+
level: the `/api/v1/apps/<namespace>/<subpath>` carve-out with an explicit
23+
`manifest.namespace` (ADR-0121 D1/D2), and `authRequired` defaulting to `true`
24+
with ADR-0121 D6's armed-`rateLimit` pairing on an explicit `false`. The removal
25+
half is untouched — `App.apis` was never read and is still gone — and #4936 is
26+
still named, because the history is why the redirect exists at all.
27+
- The `defineStack({ server })` module header said #5040 "wires endpoint-level
28+
`rateLimit` — still unwired today". It is wired. The header now also states the
29+
relationship an author of the server-level budget actually needs: endpoint
30+
buckets are keyed in their own namespace, so the two budgets meter
31+
independently rather than sharing a counter.
32+
33+
Both feed `content/docs/references/` through `gen:docs`; those two pages move with
34+
them and nothing else does.
35+
36+
**Hand-written docs.** `protocol/kernel/http-protocol.mdx` traded its "that surface
37+
has no executor" callout for a real **Declarative Endpoints** section: the serving
38+
chain (match → policy chain → delegation to the same pipelines the built-in routes
39+
use), the five gates, the policy answers (401 / 429 + `Retry-After` /
40+
`Cache-Control: private, max-age=<ttl>` on successes only), and the identity that is
41+
easy to get wrong — an unmatched path, and a **method mismatch on a declared path**,
42+
both keep the transport's bare `404` byte for byte, because the seam is Hono's
43+
`notFound` and not a registered route, so there is no method set to report a `405`
44+
over. `getting-started/quick-reference.mdx` gains the compact lookup entry.
45+
46+
**The `objectstack-api` skill** stopped describing `ApiEndpointSchema` as a
47+
four-arm `type` union with a `target` and gained a section that teaches the current
48+
capability: when `apis:` beats `contributes.routes` (and when it does not — real
49+
handler code), the carve-out, the gates as *things to run `objectstack validate`
50+
for* rather than texts to memorise, D6 with its `enabled === true` predicate spelled
51+
out, and the mapping keys' minimal semantics (projection by dot path; no
52+
`transform`; no `inputMapping` on a bodyless operation). It points at the
53+
`declarative-apis-endpoints-live` upgrade entry rather than restating the review,
54+
so there is one source of truth for it.
55+
56+
Every claim above was measured against the built spec rather than believed: the
57+
doc example publishes, the `authRequired`-omitted shape resolves to `true`, and the
58+
`rateLimit` written without `enabled: true` beside `authRequired: false` is refused
59+
with its prescription. The tombstone's new text is pinned two-sided (it must say
60+
the new thing AND must not say the retired one, with `#4936` still present) so a
61+
regression cannot pass by emptying the message.

content/docs/getting-started/quick-reference.mdx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,38 @@ import type { Field, ServiceObject, QueryAST } from '@objectstack/spec/data';
284284
import type { View } from '@objectstack/spec/ui';
285285
```
286286

287+
### Declarative Endpoints (`apis:`)
288+
289+
Declared on the **stack** (`defineStack({ apis })`), live from protocol 17 — each entry
290+
is gated at publish and, once it passes, serves real traffic. Full contract:
291+
[HTTP API → Declarative Endpoints](/docs/protocol/kernel/http-protocol).
292+
293+
| Rule | Value |
294+
|:---|:---|
295+
| **Path shape** | `/api/v1/apps/<manifest.namespace>/<subpath>` — only the subpath is yours (ADR-0121 D1) |
296+
| **Namespace** | must be declared **explicitly** as `manifest.namespace`; never derived from `manifest.id` (D2) |
297+
| **Types that execute** | `object_operation` (needs `objectParams.object` + `.operation`) and `flow` (needs `target`). `script` / `proxy` are rejected at publish |
298+
| **`authRequired`** | defaults to `true`**omitting it is safe**. An explicit `false` is the only thing that opens anonymous access |
299+
| **`authRequired: false`** | REQUIRES an armed budget, `rateLimit: { enabled: true, windowMs, maxRequests }` (ADR-0121 D6) — `enabled` itself defaults to `false`, so a budget without it meters nothing |
300+
| **`cacheTtl`** | seconds, GET-only, applied to successful answers only (`Cache-Control: private, max-age=<ttl>`) |
301+
302+
{/* os:check */}
303+
```typescript
304+
import type { ApiEndpointInput } from '@objectstack/spec/api';
305+
306+
// With `manifest: { namespace: 'acme', … }` on the same stack.
307+
export const leadFeed: ApiEndpointInput = {
308+
name: 'acme_lead_feed',
309+
path: '/api/v1/apps/acme/leads', // /api/v1/apps/<namespace>/<subpath>
310+
method: 'GET',
311+
type: 'object_operation',
312+
target: 'acme_lead',
313+
objectParams: { object: 'acme_lead', operation: 'find' },
314+
// `authRequired` omitted → defaults to true (a session is required).
315+
cacheTtl: 30,
316+
};
317+
```
318+
287319
## Search Tips
288320

289321
- **By Feature**: "How do I model relationships?" → [Field Protocol](/docs/references/data/field)`lookup` or `master_detail` types

content/docs/protocol/kernel/http-protocol.mdx

Lines changed: 130 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,17 +1036,140 @@ is the declared budget multiplied by the number of nodes.
10361036
<Callout type="warn">
10371037
**Not implemented, deliberately named rather than implied.** ObjectStack does **not**
10381038
emit `X-RateLimit-Limit` / `-Remaining` / `-Reset` headers on successful responses —
1039-
only `Retry-After` on a 429. And the per-endpoint `rateLimit` key on
1040-
`ApiEndpointSchema` is **not wired to anything**; declaring it changes nothing today.
1041-
It travels with the declarative `apis:` surface as a whole: since
1042-
[#4936](https://github.com/objectstack-ai/objectstack/issues/4936) that surface has no
1043-
executor and a **non-empty `apis:` is rejected at publish/validate**, so the key cannot
1044-
be reached at all. Wiring it is part of the endpoint executor, tracked by
1045-
[#5040](https://github.com/objectstack-ai/objectstack/issues/5040).
1039+
only `Retry-After` on a 429. That is still true of every budget on the platform,
1040+
including the per-endpoint one below.
10461041
(The second spelling this callout used to name, `ApiEndpointRegistrationSchema`, was
10471042
retired outright in [#4939](https://github.com/objectstack-ai/objectstack/issues/4939).)
10481043
</Callout>
10491044

1045+
The **per-endpoint** `rateLimit` on `ApiEndpointSchema` is a second, independent budget.
1046+
Between [#4936](https://github.com/objectstack-ai/objectstack/issues/4936) and protocol
1047+
17 it was unreachable — that surface had no executor and a non-empty `apis:` was
1048+
rejected outright — but the executor shipped with
1049+
[#5040](https://github.com/objectstack-ai/objectstack/issues/5040) and the key is now
1050+
enforced. Endpoint buckets are keyed in their own namespace, so an endpoint budget and
1051+
the `server.security.rateLimit` budget above meter separately rather than sharing a
1052+
counter. See the next section.
1053+
1054+
## Declarative Endpoints (`apis:`)
1055+
1056+
A stack can declare an HTTP endpoint as **metadata** instead of writing a handler:
1057+
1058+
<Callout type="warn">
1059+
**Declared endpoints are LIVE from protocol 17.** An `apis:` block written against an
1060+
older major changes meaning without changing a byte — what used to be inert
1061+
documentation becomes an execution entry point. Before upgrading, work through the
1062+
`declarative-apis-endpoints-live` entry of the protocol upgrade guide; it is a security
1063+
review, not a rename.
1064+
</Callout>
1065+
1066+
{/* os:check */}
1067+
```typescript
1068+
import { defineStack } from '@objectstack/spec';
1069+
1070+
export default defineStack({
1071+
manifest: {
1072+
id: 'acme-crm',
1073+
name: 'Acme CRM',
1074+
version: '1.0.0',
1075+
type: 'app',
1076+
// REQUIRED to declare `apis:` — the URL carve-out is derived from it, and
1077+
// there is deliberately no fallback that derives it from `manifest.id`.
1078+
namespace: 'acme',
1079+
},
1080+
apis: [
1081+
{
1082+
name: 'acme_lead_feed',
1083+
// `/api/v1/apps/<manifest.namespace>/<subpath>` — only the subpath is yours.
1084+
path: '/api/v1/apps/acme/leads',
1085+
method: 'GET',
1086+
summary: 'Lead feed',
1087+
type: 'object_operation',
1088+
target: 'acme_lead',
1089+
objectParams: { object: 'acme_lead', operation: 'find' },
1090+
// Defaults to `true`. Omitting it is safe; see the policy table below.
1091+
authRequired: true,
1092+
// Seconds. GET-only, and only ever on a successful answer.
1093+
cacheTtl: 30,
1094+
},
1095+
],
1096+
});
1097+
```
1098+
1099+
### How a request is served
1100+
1101+
Declared endpoints are **not registered routes**. They run in the dispatcher's
1102+
unmatched-request seam, which is what makes it structurally impossible for a
1103+
declaration to shadow a built-in route:
1104+
1105+
1. **Match** — the request path must be under `<prefix>/apps/`, and `METHOD` + path
1106+
(one trailing slash trimmed) must hit exactly one declaration.
1107+
2. **Policy chain**`rateLimit``authRequired``cacheTtl`, in that order. Metering
1108+
runs *before* the auth gate on purpose: the traffic that most needs a budget
1109+
(credential stuffing, scraping) is exactly the traffic that ends in a 401, so a denied
1110+
request still spends a token.
1111+
3. **Delegation** — a request that passed is executed by the **same pipelines the
1112+
built-in routes use**, under the caller's own execution context, so RLS/FLS and the
1113+
exposure gate apply identically. A declared endpoint is a stable URL plus a policy
1114+
layer over an existing pipeline, never a second execution dialect.
1115+
1116+
| Endpoint declares | Answer |
1117+
|:---|:---|
1118+
| `type: 'object_operation'` | delegated to the same `callData` binding that serves `/api/v1/data/{object}` — byte-identical `data` |
1119+
| `type: 'flow'` | delegated to the same automation pipeline as `POST /api/v1/automation/{name}/trigger` |
1120+
| `authRequired: true` (or omitted) + anonymous caller | `401` `UNAUTHENTICATED`, the same envelope every seam answers |
1121+
| `rateLimit` armed and exhausted | `429` + `Retry-After`, never with a cache directive |
1122+
| `cacheTtl: 30` on a successful GET | `Cache-Control: private, max-age=30``private` is a security rule, not tuning: any response can be RLS-trimmed |
1123+
| `cacheTtl: 0` | `Cache-Control: no-store` |
1124+
| an error answer (401/429/5xx) | never carries `Cache-Control`, and `outputMapping` is never applied to it |
1125+
1126+
### What an unmatched request answers
1127+
1128+
The endpoint seam **writes nothing** when it does not match, so it changes no existing
1129+
answer. Both of these are the transport's own bare 404, byte for byte:
1130+
1131+
```http
1132+
GET /api/v1/apps/acme/no-such-endpoint → 404 {"error":"Not found"}
1133+
GET /api/v1/no-such-route → 404 {"error":"Not found"}
1134+
```
1135+
1136+
A **method mismatch on a declared path is also a 404**, not `405` + `Allow`:
1137+
1138+
```http
1139+
POST /api/v1/apps/acme/leads → 404 {"error":"Not found"}
1140+
```
1141+
1142+
That is a consequence of the seam, not an inconsistency — nothing registered a route for
1143+
that path, so there is no method set to report. A registered route still answers `405`
1144+
when it exists and the verb does not fit — that contract is unchanged.
1145+
1146+
### The five publish gates
1147+
1148+
A declaration this runtime cannot serve is **rejected at publish**, naming the endpoint,
1149+
the key and the fix — never parsed into silence. `objectstack validate` (or `os build`)
1150+
runs the same gates your publish path does:
1151+
1152+
| Gate | Rejects |
1153+
|:---|:---|
1154+
| **Namespace** (ADR-0121 D1/D2) | a `path` outside `/api/v1/apps/<manifest.namespace>/<subpath>`, or a stack declaring `apis:` with no explicit `manifest.namespace` |
1155+
| **Supported target** | `type: 'script'` / `'proxy'` (neither executes in 17.x), an `object_operation` missing `objectParams.object` or `.operation`, a `flow` naming no `target` |
1156+
| **Mapping** | a mapping `transform` (there is no transformation registry), an unusable dot path (empty segment, `__proto__`), two entries writing the same target path, or `inputMapping` on a `find` / `get` / `delete` operation that never reads a body |
1157+
| **Policy** | `authRequired: false` without `rateLimit.enabled: true` (ADR-0121 D6), an unusable armed budget, a negative `cacheTtl`, or `cacheTtl` on a non-GET method |
1158+
| **Uniqueness** | two endpoints in one stack claiming the same `METHOD` + path |
1159+
1160+
<Callout type="warn">
1161+
**`authRequired: false` is the one answer that cannot be taken back.** It defaults to
1162+
`true`, so omitting it is safe; an explicit `false` is the only thing that opens an
1163+
unauthenticated execution entry point, and ADR-0121 D6 pairs it with an **armed** budget
1164+
`rateLimit.enabled` itself defaults to `false`, so writing only `windowMs` /
1165+
`maxRequests` declares a budget that meters nothing. The gate checks
1166+
`enabled === true`, not the key's presence.
1167+
</Callout>
1168+
1169+
`inputMapping` / `outputMapping` **move and rename fields by dot path, and nothing more**
1170+
`inputMapping` projects the request body before delegation (so it can never buy a
1171+
caller past the policy chain), `outputMapping` projects a successful response body only.
1172+
10501173
## Best Practices
10511174

10521175
### Use Field Selection

content/docs/references/system/stack-server.mdx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,25 @@ baked into the artifact.
6565

6666
Related: #4910 (this seam), #4937 (the limiter that documented an execution
6767

68-
chain it never had), #4936 (the declarative `apis:` surface: vocabulary kept,
68+
chain it never had), #4936 (the declarative `apis:` surface as it stood while
6969

70-
a non-empty array rejected until an executor exists) and #5040 (that
70+
nothing executed it: vocabulary kept, a non-empty array rejected outright) and
7171

72-
executor, which wires endpoint-level `rateLimit` — still unwired today),
72+
#5040 — the executor that ended that state. From protocol 17 a declared
7373

74-
ADR-0069 D2 (shared counters), ADR-0049 (enforce or remove).
74+
endpoint is LIVE behind five per-endpoint publish gates, and its own
75+
76+
`rateLimit` is enforced by the endpoint policy chain against a bucket keyed in
77+
78+
a separate namespace, so an endpoint budget and the server-level budget
79+
80+
declared here meter INDEPENDENTLY rather than sharing a counter. The upgrade
81+
82+
checklist for that flip is the `declarative-apis-endpoints-live` entry of the
83+
84+
protocol upgrade guide. ADR-0069 D2 (shared counters), ADR-0049 (enforce or
85+
86+
remove).
7587

7688
<Callout type="info">
7789
**Source:** `packages/spec/src/system/stack-server.zod.ts`

content/docs/references/ui/app.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ const result = ActionNavItemSchema.parse(data);
8686
| **homePageId** | `any` | optional | [REMOVED] `app.homePageId` was removed in @objectstack/spec 17.0.0 (#4667, #4709, ADR-0049). objectui's console did read it before v17 (`resolveLandingRoute`), so this key had a consumer — it was retired because the capability is better expressed on the navigation item itself than as an ID cross-reference that silently falls back when it dangles. An app's landing page IS its first navigation item (by `order`), and the root landing follows `isDefault` routing. Delete the key; to change where an app opens, reorder `navigation` so the intended entry is first, and set `isDefault` on the app that should own the root landing. Run `os migrate meta --from 16` to rewrite existing sources automatically. |
8787
| **requiredPermissions** | `string[]` | optional | Permissions required to access this app |
8888
| **objects** | `any` | optional | [REMOVED] `App.objects` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read; the spec itself labelled it "config file convenience"). Objects belong to the stack (`defineStack({ objects })`); an app reaches them through its navigation items. Delete the key. |
89-
| **apis** | `any` | optional | [REMOVED] `App.apis` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read). Delete the key. Note the stack-level `defineStack({ apis })` this prescription used to redirect to is ALSO not executable in v17 (#4936): the vocabulary is kept but a non-empty array is rejected there too, until the endpoint executor ships (tracked by https://github.com/objectstack-ai/objectstack/issues/5040). Serve the route in code meanwhile — a plugin manifest `contributes.routes` entry or an `http.server` route. |
89+
| **apis** | `any` | optional | [REMOVED] `App.apis` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read). Delete the key and declare the endpoint one level up, on the STACK: `defineStack({ apis })`. That surface EXECUTES from protocol 17 (#5040). Between #4936 and the executor landing it was refused wholesale — nothing mounted a declared path, so every key including `authRequired` parsed and gated nothing — and that blanket refusal is now narrowed to five per-endpoint publish gates (namespace, supported target, mapping, policy, uniqueness): an endpoint that passes them is mounted and serves traffic as soon as the stack is published. Two things to get right when you move it: the path must sit inside your own carve-out, `/api/v1/apps/<manifest.namespace>/<subpath>` with an explicit `manifest.namespace` (ADR-0121 D1/D2), and `authRequired` defaults to `true` — an explicit `false` is the only thing that opens anonymous access, and ADR-0121 D6 then requires an armed `rateLimit: { enabled: true, windowMs, maxRequests }`. Read the `declarative-apis-endpoints-live` entry of the protocol upgrade guide first; it is a security review, not a rename. A route that genuinely needs handler CODE still belongs in a plugin manifest `contributes.routes` entry. |
9090
| **sharing** | `any` | optional | [REMOVED] `App.sharing` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit / ADR-0049 enforce-or-remove) — no public-app route ever read it, so it declared sharing that did not exist. Public access is granted per FORM VIEW (`FormView.sharing`, the public-data-collection surface). Delete the key. |
9191
| **embed** | `any` | optional | [REMOVED] `App.embed` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit / ADR-0049) — no iframe route ever read it. Embedding is a per-form-view surface (`FormView.sharing`), not an app-level switch. Delete the key. |
9292
| **mobileNavigation** | `any` | optional | [REMOVED] `App.mobileNavigation` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — fully unimplemented; no renderer, including packages/mobile, ever read it). Delete the key; the block returns if/when a real mobile navigation ships. |

packages/spec/src/system/stack-server.zod.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,16 @@
3939
* baked into the artifact.
4040
*
4141
* Related: #4910 (this seam), #4937 (the limiter that documented an execution
42-
* chain it never had), #4936 (the declarative `apis:` surface: vocabulary kept,
43-
* a non-empty array rejected until an executor exists) and #5040 (that
44-
* executor, which wires endpoint-level `rateLimit` — still unwired today),
45-
* ADR-0069 D2 (shared counters), ADR-0049 (enforce or remove).
42+
* chain it never had), #4936 (the declarative `apis:` surface as it stood while
43+
* nothing executed it: vocabulary kept, a non-empty array rejected outright) and
44+
* #5040 — the executor that ended that state. From protocol 17 a declared
45+
* endpoint is LIVE behind five per-endpoint publish gates, and its own
46+
* `rateLimit` is enforced by the endpoint policy chain against a bucket keyed in
47+
* a separate namespace, so an endpoint budget and the server-level budget
48+
* declared here meter INDEPENDENTLY rather than sharing a counter. The upgrade
49+
* checklist for that flip is the `declarative-apis-endpoints-live` entry of the
50+
* protocol upgrade guide. ADR-0069 D2 (shared counters), ADR-0049 (enforce or
51+
* remove).
4652
*/
4753

4854
import { z } from 'zod';

0 commit comments

Comments
 (0)