@telorun/http-server 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +28 -5
- package/dist/http-server-controller.d.ts +3 -2
- package/dist/http-server-controller.js +53 -12
- package/package.json +2 -2
- package/src/http-server-controller.ts +54 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# @telorun/http-server
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b1dd65c: `Http.Server`: the generated OpenAPI `servers` URL now defaults to a **relative** path (the mount prefix), so the reference UI and spec are correct behind any proxy/ingress/origin with no configuration — fixing the previous hardcoded `http://<bind-host>:<port>` that didn't match the reachable URL. Add an opt-in `trustForwardedHeaders` boolean: when enabled the server honors the standard `X-Forwarded-Proto` / `X-Forwarded-Host` headers and advertises an absolute `servers` URL (and request protocol/host) matching the fronting proxy. An explicit `baseUrl` still overrides both.
|
|
8
|
+
|
|
9
|
+
## 0.10.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- ee8926f: Unify resource references on the `!ref` YAML tag. The object form `{ kind, name }`
|
|
14
|
+
and bare-string references are removed: the analyzer rejects them up front
|
|
15
|
+
(`INVALID_REFERENCE_FORM`) and `!ref <name>` / `!ref <Alias>.<name>` is the only
|
|
16
|
+
authored shape. `resolveRefSentinels` now resolves `!ref` sentinels across the
|
|
17
|
+
whole manifest tree (including step `invoke`s and refs nested in inline
|
|
18
|
+
definitions), so every consumer sees the uniform resolved shape. The
|
|
19
|
+
http-server mount slot is renamed `mounts[].type` → `mounts[].mount`, and the
|
|
20
|
+
mcp transports / clients read their Phase-5-injected ref instances directly.
|
|
21
|
+
|
|
22
|
+
Schema validation (analyzer and kernel) now drops the stale scalar `type` a ref
|
|
23
|
+
slot may still pin (older published modules encode references as `type: string`)
|
|
24
|
+
before running AJV, so a resolved reference object validates against a legacy
|
|
25
|
+
`x-telo-ref` slot. This keeps an app that consumes a not-yet-republished
|
|
26
|
+
dependency analyzable and bootable during the migration. Object-typed ref slots
|
|
27
|
+
that also accept an inline value (e.g. `inputType` / `outputType`) are left
|
|
28
|
+
untouched.
|
|
29
|
+
|
|
30
|
+
`Run.Sequence` reference slots are brought onto the same enforcement path: a
|
|
31
|
+
step `invoke` and a scope `targets` entry now require a `!ref` (the `targets`
|
|
32
|
+
slot gains an `x-telo-ref` constraint and the `with` scope's visibility extends
|
|
33
|
+
to `/targets`), so a bare-string ref at either is rejected with
|
|
34
|
+
`INVALID_REFERENCE_FORM` at `telo check` — uniform with `Telo.Application`
|
|
35
|
+
targets — instead of failing as an obscure runtime error. The controller reads
|
|
36
|
+
the resolved reference rather than a bare name.
|
|
37
|
+
|
|
3
38
|
## 0.9.0
|
|
4
39
|
|
|
5
40
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -24,16 +24,16 @@ Language- and framework-agnostic HTTP server for Telo. Declarative routes, schem
|
|
|
24
24
|
kind: Telo.Application
|
|
25
25
|
metadata: { name: hello-http, version: 1.0.0 }
|
|
26
26
|
imports:
|
|
27
|
-
Http:
|
|
28
|
-
JS:
|
|
29
|
-
targets: [Server]
|
|
27
|
+
Http: std/http-server@0.11.0
|
|
28
|
+
JS: std/javascript@0.5.0
|
|
29
|
+
targets: [ !ref Server ]
|
|
30
30
|
---
|
|
31
31
|
kind: Http.Server
|
|
32
32
|
metadata: { name: Server }
|
|
33
33
|
port: 8080
|
|
34
34
|
mounts:
|
|
35
35
|
- path: /api
|
|
36
|
-
|
|
36
|
+
mount: !ref Api
|
|
37
37
|
---
|
|
38
38
|
kind: Http.Api
|
|
39
39
|
metadata: { name: Api }
|
|
@@ -54,7 +54,7 @@ routes:
|
|
|
54
54
|
examples: [ "Ada" ]
|
|
55
55
|
inputs:
|
|
56
56
|
name: "${{ request.params.name }}"
|
|
57
|
-
handler:
|
|
57
|
+
handler: !ref Greet
|
|
58
58
|
returns:
|
|
59
59
|
- status: 200
|
|
60
60
|
content:
|
|
@@ -206,3 +206,26 @@ request:
|
|
|
206
206
|
description: "Validation schema for the request payload"
|
|
207
207
|
required: ["path", "method"]
|
|
208
208
|
```
|
|
209
|
+
|
|
210
|
+
### 5. External URL & OpenAPI `servers`
|
|
211
|
+
|
|
212
|
+
A server is usually reached through a reverse proxy / ingress, so its own bound
|
|
213
|
+
`host:port` is not the URL clients use. The generated OpenAPI `servers` block MUST
|
|
214
|
+
follow this resolution, identically across runtimes (Node/Rust/Go) — the inputs
|
|
215
|
+
are standard HTTP, never a framework's proxy-config object:
|
|
216
|
+
|
|
217
|
+
| Manifest | `servers[].url` |
|
|
218
|
+
| --- | --- |
|
|
219
|
+
| `baseUrl: <url>` | `<url><mountPrefix>` — explicit, fixed; wins over everything |
|
|
220
|
+
| `trustForwardedHeaders: true` | `<X-Forwarded-Proto>://<X-Forwarded-Host><mountPrefix>`, derived per request |
|
|
221
|
+
| neither (default) | `<mountPrefix>` — **relative**; the client resolves it against the origin the document was loaded from |
|
|
222
|
+
|
|
223
|
+
- The default is **relative** so the document is correct behind any proxy, ingress,
|
|
224
|
+
or origin with zero configuration.
|
|
225
|
+
- `trustForwardedHeaders` is a **boolean** on purpose: the only portable cross-runtime
|
|
226
|
+
signal is the standard `X-Forwarded-Proto` / `X-Forwarded-Host` (RFC 7239 `Forwarded`)
|
|
227
|
+
headers. Fine-grained "trusted proxy IP/CIDR/hop" lists are framework-specific and
|
|
228
|
+
MUST NOT leak into the manifest. Default `false`; only enable behind a trusted proxy
|
|
229
|
+
(a client with direct network access could otherwise spoof the headers).
|
|
230
|
+
- When `trustForwardedHeaders` is set, the request protocol/host exposed to handlers
|
|
231
|
+
MUST also reflect the forwarded headers.
|
|
@@ -2,7 +2,7 @@ import { CatchEntry, ReturnEntry } from "@telorun/http-dispatch";
|
|
|
2
2
|
import { type Invocable, type KindRef, type ResourceContext, type ResourceInstance, type RuntimeResource } from "@telorun/sdk";
|
|
3
3
|
import { FastifyInstance } from "fastify";
|
|
4
4
|
/** A mounted Telo.Mount instance (Http.Api, Mcp.HttpEndpoint, …). The kernel injects the
|
|
5
|
-
* live instance into a mount's `
|
|
5
|
+
* live instance into a mount's `mount` slot (x-telo-ref "telo#Mount") — cross-module refs
|
|
6
6
|
* resolve to an imported library's exported mount — and every mountable exposes register(). */
|
|
7
7
|
interface Mountable {
|
|
8
8
|
register(app: FastifyInstance, prefix: string): void | Promise<void>;
|
|
@@ -25,6 +25,7 @@ type HttpServerResource = RuntimeResource & {
|
|
|
25
25
|
host?: string;
|
|
26
26
|
port?: number;
|
|
27
27
|
baseUrl?: string;
|
|
28
|
+
trustForwardedHeaders?: boolean;
|
|
28
29
|
logger?: boolean;
|
|
29
30
|
cors?: CorsOptions;
|
|
30
31
|
contentTypeParsers?: Array<{
|
|
@@ -40,7 +41,7 @@ type HttpServerResource = RuntimeResource & {
|
|
|
40
41
|
};
|
|
41
42
|
mounts?: Array<{
|
|
42
43
|
path?: string;
|
|
43
|
-
|
|
44
|
+
mount?: Mountable;
|
|
44
45
|
}>;
|
|
45
46
|
notFoundHandler?: {
|
|
46
47
|
invoke: KindRef<Invocable>;
|
|
@@ -13,6 +13,7 @@ class HttpServer {
|
|
|
13
13
|
host;
|
|
14
14
|
port;
|
|
15
15
|
baseUrl;
|
|
16
|
+
trustForwardedHeaders;
|
|
16
17
|
resource;
|
|
17
18
|
ctx;
|
|
18
19
|
resolvedNotFoundHandler;
|
|
@@ -22,12 +23,16 @@ class HttpServer {
|
|
|
22
23
|
this.host = resource.host || "0.0.0.0";
|
|
23
24
|
this.port = Number(resource.port || 0);
|
|
24
25
|
this.baseUrl = resource.baseUrl ?? `http://${this.host}:${this.port}`;
|
|
26
|
+
this.trustForwardedHeaders = resource.trustForwardedHeaders === true;
|
|
25
27
|
this.resolvedNotFoundHandler = resolvedNotFoundHandler;
|
|
26
28
|
if (!this.port) {
|
|
27
29
|
throw new Error("Http.Server port is required");
|
|
28
30
|
}
|
|
29
31
|
this.app = Fastify({
|
|
30
32
|
logger: resource.logger,
|
|
33
|
+
// Honour X-Forwarded-Proto / X-Forwarded-Host so request.protocol/host (and
|
|
34
|
+
// the OpenAPI servers derived from them) reflect a fronting proxy's URL.
|
|
35
|
+
trustProxy: this.trustForwardedHeaders,
|
|
31
36
|
ajv: { customOptions: { useDefaults: true }, plugins: [addFormats.default] },
|
|
32
37
|
});
|
|
33
38
|
}
|
|
@@ -91,16 +96,15 @@ class HttpServer {
|
|
|
91
96
|
throw error;
|
|
92
97
|
});
|
|
93
98
|
if (this.resource.openapi) {
|
|
94
|
-
const servers = [];
|
|
95
|
-
// const routesByName = new Map<string, HttpRouteResource>();
|
|
96
99
|
const mounts = this.resource.mounts || [];
|
|
97
|
-
const prefixes = new Set();
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
const prefixes = [...new Set(mounts.map((mount) => mount.path || ""))];
|
|
101
|
+
// Server URL precedence: an explicit `baseUrl` is an absolute, fixed
|
|
102
|
+
// override; otherwise the URLs are relative (just the mount prefix) so the
|
|
103
|
+
// doc is correct behind any proxy/ingress/origin without configuration —
|
|
104
|
+
// the client resolves them against wherever the reference was loaded.
|
|
105
|
+
const servers = prefixes.map((prefix) => ({
|
|
106
|
+
url: this.resource.baseUrl ? this.resource.baseUrl + prefix : prefix || "/",
|
|
107
|
+
}));
|
|
104
108
|
await this.app.register(swagger, {
|
|
105
109
|
openapi: {
|
|
106
110
|
openapi: "3.0.0",
|
|
@@ -108,8 +112,45 @@ class HttpServer {
|
|
|
108
112
|
servers,
|
|
109
113
|
},
|
|
110
114
|
});
|
|
115
|
+
const referencePrefix = "/reference";
|
|
116
|
+
// `trustForwardedHeaders` (and no fixed baseUrl) upgrades the relative
|
|
117
|
+
// default to absolute URLs built per-request from the now-trusted
|
|
118
|
+
// X-Forwarded-* headers, so the served spec advertises the real proxy URL.
|
|
119
|
+
if (this.trustForwardedHeaders && !this.resource.baseUrl) {
|
|
120
|
+
// Couples to the Scalar plugin's default spec endpoint
|
|
121
|
+
// (`<routePrefix>/openapi.json`); if it ever served the doc elsewhere the
|
|
122
|
+
// rewrite would no-op and the relative default would still apply. The
|
|
123
|
+
// `openapi-server-url` integration test guards this path.
|
|
124
|
+
const specPath = `${referencePrefix}/openapi.json`;
|
|
125
|
+
this.app.addHook("onSend", async (request, reply, payload) => {
|
|
126
|
+
if (request.url.split("?")[0] !== specPath)
|
|
127
|
+
return payload;
|
|
128
|
+
const text = typeof payload === "string"
|
|
129
|
+
? payload
|
|
130
|
+
: Buffer.isBuffer(payload)
|
|
131
|
+
? payload.toString("utf8")
|
|
132
|
+
: null;
|
|
133
|
+
if (text === null)
|
|
134
|
+
return payload;
|
|
135
|
+
try {
|
|
136
|
+
const doc = JSON.parse(text);
|
|
137
|
+
if (doc && typeof doc === "object" && Array.isArray(doc.servers)) {
|
|
138
|
+
doc.servers = prefixes.map((prefix) => ({
|
|
139
|
+
url: `${request.protocol}://${request.host}${prefix}`,
|
|
140
|
+
}));
|
|
141
|
+
const out = JSON.stringify(doc);
|
|
142
|
+
reply.header("content-length", Buffer.byteLength(out));
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Not a JSON document we can rewrite — leave the response untouched.
|
|
148
|
+
}
|
|
149
|
+
return payload;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
111
152
|
await this.app.register(apiReference, {
|
|
112
|
-
routePrefix:
|
|
153
|
+
routePrefix: referencePrefix,
|
|
113
154
|
});
|
|
114
155
|
}
|
|
115
156
|
}
|
|
@@ -119,9 +160,9 @@ class HttpServer {
|
|
|
119
160
|
// const resolveSchema = createSchemaResolver(this.ctx);
|
|
120
161
|
for (const mount of mounts) {
|
|
121
162
|
const prefix = mount.path || "";
|
|
122
|
-
// `mount.
|
|
163
|
+
// `mount.mount` is the live Telo.Mount instance injected by the kernel at Phase 5
|
|
123
164
|
// (x-telo-ref "telo#Mount") — a same-module or imported-library mount, uniformly.
|
|
124
|
-
const api = mount.
|
|
165
|
+
const api = mount.mount;
|
|
125
166
|
if (!api || typeof api.register !== "function") {
|
|
126
167
|
throw new Error(`Failed to mount at "${prefix}": mount target did not resolve to a Telo.Mount instance`);
|
|
127
168
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/http-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Telo HTTP Server module - HTTP server and API resource kinds for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@types/node": "^20.0.0",
|
|
50
50
|
"typescript": "^5.0.0",
|
|
51
51
|
"vitest": "^2.1.8",
|
|
52
|
-
"@telorun/sdk": "0.
|
|
52
|
+
"@telorun/sdk": "0.26.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@telorun/sdk": "*"
|
|
@@ -20,7 +20,7 @@ import Fastify, { FastifyInstance } from "fastify";
|
|
|
20
20
|
import { fastifyReplySink } from "./fastify-reply-sink.js";
|
|
21
21
|
|
|
22
22
|
/** A mounted Telo.Mount instance (Http.Api, Mcp.HttpEndpoint, …). The kernel injects the
|
|
23
|
-
* live instance into a mount's `
|
|
23
|
+
* live instance into a mount's `mount` slot (x-telo-ref "telo#Mount") — cross-module refs
|
|
24
24
|
* resolve to an imported library's exported mount — and every mountable exposes register(). */
|
|
25
25
|
interface Mountable {
|
|
26
26
|
register(app: FastifyInstance, prefix: string): void | Promise<void>;
|
|
@@ -45,6 +45,7 @@ type HttpServerResource = RuntimeResource & {
|
|
|
45
45
|
host?: string;
|
|
46
46
|
port?: number;
|
|
47
47
|
baseUrl?: string;
|
|
48
|
+
trustForwardedHeaders?: boolean;
|
|
48
49
|
logger?: boolean;
|
|
49
50
|
cors?: CorsOptions;
|
|
50
51
|
contentTypeParsers?: Array<{ contentType: string; parser?: Invocable; stream?: boolean }>;
|
|
@@ -58,7 +59,7 @@ type HttpServerResource = RuntimeResource & {
|
|
|
58
59
|
path?: string;
|
|
59
60
|
// x-telo-ref "telo#Mount": Phase 5 replaces this slot with the live mounted
|
|
60
61
|
// instance (Http.Api, Mcp.HttpEndpoint, …), local or imported.
|
|
61
|
-
|
|
62
|
+
mount?: Mountable;
|
|
62
63
|
}>;
|
|
63
64
|
notFoundHandler?: {
|
|
64
65
|
invoke: KindRef<Invocable>;
|
|
@@ -83,6 +84,7 @@ class HttpServer implements ResourceInstance {
|
|
|
83
84
|
private readonly host: string;
|
|
84
85
|
private readonly port: number;
|
|
85
86
|
private readonly baseUrl: string;
|
|
87
|
+
private readonly trustForwardedHeaders: boolean;
|
|
86
88
|
private readonly resource: HttpServerResource;
|
|
87
89
|
private readonly ctx: ResourceContext;
|
|
88
90
|
private readonly resolvedNotFoundHandler: ResolvedHandler | null;
|
|
@@ -97,6 +99,7 @@ class HttpServer implements ResourceInstance {
|
|
|
97
99
|
this.host = resource.host || "0.0.0.0";
|
|
98
100
|
this.port = Number(resource.port || 0);
|
|
99
101
|
this.baseUrl = resource.baseUrl ?? `http://${this.host}:${this.port}`;
|
|
102
|
+
this.trustForwardedHeaders = resource.trustForwardedHeaders === true;
|
|
100
103
|
this.resolvedNotFoundHandler = resolvedNotFoundHandler;
|
|
101
104
|
|
|
102
105
|
if (!this.port) {
|
|
@@ -104,6 +107,9 @@ class HttpServer implements ResourceInstance {
|
|
|
104
107
|
}
|
|
105
108
|
this.app = Fastify({
|
|
106
109
|
logger: resource.logger,
|
|
110
|
+
// Honour X-Forwarded-Proto / X-Forwarded-Host so request.protocol/host (and
|
|
111
|
+
// the OpenAPI servers derived from them) reflect a fronting proxy's URL.
|
|
112
|
+
trustProxy: this.trustForwardedHeaders,
|
|
107
113
|
ajv: { customOptions: { useDefaults: true }, plugins: [addFormats.default as any] },
|
|
108
114
|
});
|
|
109
115
|
}
|
|
@@ -172,16 +178,15 @@ class HttpServer implements ResourceInstance {
|
|
|
172
178
|
throw error;
|
|
173
179
|
});
|
|
174
180
|
if (this.resource.openapi) {
|
|
175
|
-
const servers = [];
|
|
176
|
-
// const routesByName = new Map<string, HttpRouteResource>();
|
|
177
181
|
const mounts = this.resource.mounts || [];
|
|
178
|
-
const prefixes = new Set();
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
const prefixes = [...new Set(mounts.map((mount) => mount.path || ""))];
|
|
183
|
+
// Server URL precedence: an explicit `baseUrl` is an absolute, fixed
|
|
184
|
+
// override; otherwise the URLs are relative (just the mount prefix) so the
|
|
185
|
+
// doc is correct behind any proxy/ingress/origin without configuration —
|
|
186
|
+
// the client resolves them against wherever the reference was loaded.
|
|
187
|
+
const servers = prefixes.map((prefix) => ({
|
|
188
|
+
url: this.resource.baseUrl ? this.resource.baseUrl + prefix : prefix || "/",
|
|
189
|
+
}));
|
|
185
190
|
await this.app.register(swagger, {
|
|
186
191
|
openapi: {
|
|
187
192
|
openapi: "3.0.0",
|
|
@@ -189,8 +194,43 @@ class HttpServer implements ResourceInstance {
|
|
|
189
194
|
servers,
|
|
190
195
|
},
|
|
191
196
|
});
|
|
197
|
+
const referencePrefix = "/reference";
|
|
198
|
+
// `trustForwardedHeaders` (and no fixed baseUrl) upgrades the relative
|
|
199
|
+
// default to absolute URLs built per-request from the now-trusted
|
|
200
|
+
// X-Forwarded-* headers, so the served spec advertises the real proxy URL.
|
|
201
|
+
if (this.trustForwardedHeaders && !this.resource.baseUrl) {
|
|
202
|
+
// Couples to the Scalar plugin's default spec endpoint
|
|
203
|
+
// (`<routePrefix>/openapi.json`); if it ever served the doc elsewhere the
|
|
204
|
+
// rewrite would no-op and the relative default would still apply. The
|
|
205
|
+
// `openapi-server-url` integration test guards this path.
|
|
206
|
+
const specPath = `${referencePrefix}/openapi.json`;
|
|
207
|
+
this.app.addHook("onSend", async (request, reply, payload) => {
|
|
208
|
+
if (request.url.split("?")[0] !== specPath) return payload;
|
|
209
|
+
const text =
|
|
210
|
+
typeof payload === "string"
|
|
211
|
+
? payload
|
|
212
|
+
: Buffer.isBuffer(payload)
|
|
213
|
+
? payload.toString("utf8")
|
|
214
|
+
: null;
|
|
215
|
+
if (text === null) return payload;
|
|
216
|
+
try {
|
|
217
|
+
const doc = JSON.parse(text);
|
|
218
|
+
if (doc && typeof doc === "object" && Array.isArray(doc.servers)) {
|
|
219
|
+
doc.servers = prefixes.map((prefix) => ({
|
|
220
|
+
url: `${request.protocol}://${request.host}${prefix}`,
|
|
221
|
+
}));
|
|
222
|
+
const out = JSON.stringify(doc);
|
|
223
|
+
reply.header("content-length", Buffer.byteLength(out));
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
// Not a JSON document we can rewrite — leave the response untouched.
|
|
228
|
+
}
|
|
229
|
+
return payload;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
192
232
|
await this.app.register(apiReference, {
|
|
193
|
-
routePrefix:
|
|
233
|
+
routePrefix: referencePrefix,
|
|
194
234
|
});
|
|
195
235
|
}
|
|
196
236
|
}
|
|
@@ -201,9 +241,9 @@ class HttpServer implements ResourceInstance {
|
|
|
201
241
|
// const resolveSchema = createSchemaResolver(this.ctx);
|
|
202
242
|
for (const mount of mounts) {
|
|
203
243
|
const prefix = mount.path || "";
|
|
204
|
-
// `mount.
|
|
244
|
+
// `mount.mount` is the live Telo.Mount instance injected by the kernel at Phase 5
|
|
205
245
|
// (x-telo-ref "telo#Mount") — a same-module or imported-library mount, uniformly.
|
|
206
|
-
const api = mount.
|
|
246
|
+
const api = mount.mount;
|
|
207
247
|
if (!api || typeof api.register !== "function") {
|
|
208
248
|
throw new Error(
|
|
209
249
|
`Failed to mount at "${prefix}": mount target did not resolve to a Telo.Mount instance`,
|