Skip to content

Commit 7df2f32

Browse files
committed
docs(protocol): http-protocol 的 API Discovery 拆成两段式 —— REST 形状与 dispatcher 形状分开 (#4817)
该页此前声称 `/.well-known/objectstack` 与 `/api/v1/discovery` 「都直接返回同一份 discovery 文档」,并给出一份混合示例(`name` / `version: "2.1.0"` / `environment` / `locale`)。这份示例其实是 dispatcher `getDiscoveryInfo()` 的形状,而挂了 `@objectstack/rest` 的常规组合下 `/api/v1/discovery` 返回的是 `{ version, apiName, routes, services, capabilities }` + rest-server 追加的 `scoping`,没有 `name` / `environment` / `locale`,`version` 还被 `api.version` 覆盖成 `"v1"`。 改为与 `content/docs/api/index.mdx`(#4816 已核实)一致的两段式: - `GET /api/v1`(与 `/api/v1/discovery`)一节给 REST 形状; - `GET /.well-known/objectstack` 一节给 dispatcher 形状(`{ "data": ... }` 包裹, 含 `name` / `environment` / `features` / `locale`); - 「两条路径同文档」限定为 REST-less 组合,并点名 ADR-0076 D11 单一 owner 规则。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ
1 parent 158a3d8 commit 7df2f32

2 files changed

Lines changed: 125 additions & 24 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
docs(protocol): `protocol/kernel/http-protocol` 的 API Discovery 一节拆成两段式 —— `@objectstack/rest` 服务的 `/api/v1`(与 `/api/v1/discovery`)与 dispatcher 服务的 `/.well-known/objectstack` 各给一份真实响应形状,不再共用一份混合示例。Docs-only;releases nothing.

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

Lines changed: 121 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,51 +19,148 @@ The **HTTP API** defines how ObjectStack maps data operations to RESTful HTTP en
1919

2020
## API Discovery
2121

22-
### Discovery Endpoint
22+
Before making any API calls, clients should request a discovery endpoint to learn about
23+
available services. **Two endpoints answer that question, and in a stack that mounts
24+
`@objectstack/rest` they do not return the same shape** — they are built by different
25+
packages. Read the one that matches your composition; do not mix their fields.
2326

24-
Before making any API calls, clients should request the discovery endpoint to learn about available services:
27+
### `GET /api/v1` (and `GET /api/v1/discovery`)
2528

26-
**Request:**
27-
```http
28-
GET /.well-known/objectstack HTTP/1.1
29-
Host: api.acme.com
30-
```
29+
Returns the full discovery manifest. `@objectstack/rest` registers **one handler at both
30+
paths** — the API base path and `<basePath>/discovery` — so the two are the same document,
31+
not a redirect and not two shapes. In a REST-less composition the runtime dispatcher
32+
registers `<basePath>/discovery` as the fallback owner instead, and then serves its own
33+
`/.well-known/objectstack` payload there (see below); when `@objectstack/rest` is mounted
34+
the dispatcher cedes the route to it, so a single owner answers it (ADR-0076 D11).
3135

32-
`/.well-known/objectstack` and the versioned `/api/v1/discovery` route both return the
33-
discovery document directly — there is no HTTP redirect between them:
36+
**Request:**
3437
```http
3538
GET /api/v1/discovery HTTP/1.1
39+
Host: api.acme.com
3640
```
3741

3842
**Response:**
3943
```json
4044
{
41-
"name": "Acme CRM Production",
42-
"version": "2.1.0",
43-
"environment": "production",
45+
"version": "v1",
46+
"apiName": "ObjectStack API",
4447
"routes": {
4548
"data": "/api/v1/data",
46-
"metadata": "/api/v1/meta",
47-
"packages": "/api/v1/packages",
48-
"auth": "/api/v1/auth",
49-
"ui": "/api/v1/ui",
50-
"storage": "/api/v1/storage"
49+
"metadata": "/api/v1/meta"
5150
},
5251
"services": {
53-
"data": { "enabled": true, "status": "available", "route": "/api/v1/data", "provider": "objectql" },
54-
"metadata": { "enabled": true, "status": "available", "route": "/api/v1/meta", "provider": "objectql" },
55-
"auth": { "enabled": true, "status": "available", "route": "/api/v1/auth", "provider": "@objectstack/plugin-auth" },
52+
"metadata": { "enabled": true, "status": "available", "handlerReady": true, "route": "/api/v1/meta", "provider": "objectql" },
53+
"data": { "enabled": true, "status": "available", "handlerReady": true, "route": "/api/v1/data", "provider": "objectql" },
5654
"search": { "enabled": false, "status": "unavailable", "message": "No implementation ships for the 'search' slot — register a service under it to enable" },
5755
"ai": { "enabled": false, "status": "unavailable", "message": "Provided by @objectstack/service-ai in ObjectStack Cloud/Enterprise — no implementation ships in the open framework" }
5856
},
59-
"locale": {
60-
"default": "en-US",
61-
"supported": ["en-US", "zh-CN", "es-ES", "fr-FR"],
62-
"timezone": "America/Los_Angeles"
57+
"capabilities": {
58+
"cron": { "enabled": false },
59+
"automation": { "enabled": false },
60+
"search": { "enabled": false },
61+
"transactionalBatch": { "enabled": true, "description": "Atomic cross-object batch endpoint (POST {basePath}/batch)…" }
62+
},
63+
"scoping": {
64+
"enabled": false,
65+
"resolution": "auto",
66+
"scoped": false
67+
}
68+
}
69+
```
70+
71+
Three things about this body are worth stating explicitly, because they are what the
72+
`/.well-known/objectstack` document below does *not* share:
73+
74+
- **`version` is the configured API version, not a product version.** The handler
75+
overwrites the protocol's value with `api.version` — the same string that forms the
76+
path segment (`"v1"`). It is never a semantic version like `2.1.0`.
77+
- **There is no `name`, `environment` or `locale` here.** Those are dispatcher fields
78+
(see below). A client that initialises i18n from `locale` must read
79+
`/.well-known/objectstack`, not this response.
80+
- **`scoping` is added by the REST server**, so clients can detect dual-mode routing;
81+
`environmentId` is present only on the environment-scoped mount
82+
(`/api/v1/environments/:environmentId/...`).
83+
84+
Disabled/uninstalled route keys are omitted from `routes` entirely rather than set to
85+
`null`; check `services` to tell "not installed" apart from "installed but not yet mounted
86+
here." `capabilities` is a flat map of platform feature flags (`comments`, `automation`,
87+
`cron`, `search`, `export`, `chunkedUpload`, `transactionalBatch`), each derived from what
88+
is actually registered — never hardcoded. See
89+
[API → Discovery](/docs/api#discovery) for the field-by-field reference.
90+
91+
### `GET /.well-known/objectstack`
92+
93+
Served by the runtime dispatcher (`@objectstack/runtime`), not `@objectstack/rest` — its
94+
body is wrapped as `{ "data": { ... } }` and includes fields (`name`, `environment`,
95+
`features`, `locale`) that the `@objectstack/rest`-served `/api/v1` response above does
96+
not. This path is unconditionally dispatcher-owned: no other plugin registers it, so it
97+
answers with this shape whether or not REST is mounted. The client SDK's `connect()` tries
98+
`/api/v1/discovery` first and falls back to this endpoint, unwrapping either `body.data` or
99+
the bare `body`.
100+
101+
**Request:**
102+
```http
103+
GET /.well-known/objectstack HTTP/1.1
104+
Host: api.acme.com
105+
```
106+
107+
**Response:**
108+
```json
109+
{
110+
"data": {
111+
"name": "ObjectOS",
112+
"version": "1.0.0",
113+
"environment": "production",
114+
"routes": {
115+
"data": "/api/v1/data",
116+
"metadata": "/api/v1/meta",
117+
"packages": "/api/v1/packages",
118+
"auth": "/api/v1/auth",
119+
"ui": "/api/v1/ui",
120+
"i18n": "/api/v1/i18n"
121+
},
122+
"features": {
123+
"search": false,
124+
"websockets": false,
125+
"files": false,
126+
"analytics": false,
127+
"ai": false,
128+
"notifications": false,
129+
"i18n": true
130+
},
131+
"services": {
132+
"metadata": { "enabled": true, "status": "available", "handlerReady": true, "route": "/api/v1/meta", "provider": "kernel" },
133+
"data": { "enabled": true, "status": "available", "handlerReady": true, "route": "/api/v1/data", "provider": "kernel" },
134+
"auth": { "enabled": true, "status": "available", "handlerReady": true, "route": "/api/v1/auth" },
135+
"search": { "enabled": false, "status": "unavailable", "handlerReady": false, "message": "No implementation ships for the 'search' slot — register a service under it to enable" }
136+
},
137+
"locale": {
138+
"default": "en-US",
139+
"supported": ["en-US", "zh-CN"],
140+
"timezone": "UTC"
141+
}
63142
}
64143
}
65144
```
66145

146+
`name` and `version` are the dispatcher's own build identity, not your app's name — they
147+
are fixed strings, so do not display them as the deployment's title. `environment` is the
148+
process `NODE_ENV`. `locale` is derived from the registered i18n service (`getDefaultLocale()`
149+
/ `getLocales()`); with no i18n service it degrades to `{ "default": "en", "supported":
150+
["en"], "timezone": "UTC" }`. The body also repeats `routes` under an `endpoints` key as a
151+
backward-compatibility alias, and carries **no** `capabilities` map — that one exists only
152+
on the REST-served response above.
153+
154+
<Callout type="warn">
155+
**"Both paths return the same document" holds only in a REST-less composition.** There, the
156+
dispatcher owns `/api/v1/discovery` as the fallback registrant, so that path and
157+
`/.well-known/objectstack` both answer with the dispatcher payload above (the bare
158+
`/api/v1` is registered by `@objectstack/rest` alone and is not served at all). As soon as
159+
`@objectstack/rest` is mounted it takes `/api/v1/discovery` under the single-owner rule
160+
(ADR-0076 D11) and the two paths answer different shapes. Never write a client that reads
161+
`locale` or `environment` off `/api/v1/discovery`.
162+
</Callout>
163+
67164
**Why discovery matters:**
68165
- **Environment agnostic:** Works across dev, staging, production without hardcoding URLs
69166
- **Version tolerance:** API routes can change without breaking clients

0 commit comments

Comments
 (0)