@stonyx/rest-server 0.2.1-beta.9 → 0.2.1-beta.90
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/README.md +114 -0
- package/config/environment.js +30 -3
- package/dist/main.d.ts +18 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +93 -0
- package/dist/main.js.map +1 -0
- package/dist/request.d.ts +16 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +93 -0
- package/dist/request.js.map +1 -0
- package/dist/route-matching.d.ts +35 -0
- package/dist/route-matching.d.ts.map +1 -0
- package/dist/route-matching.js +38 -0
- package/dist/route-matching.js.map +1 -0
- package/package.json +25 -7
- package/.claude/improvements.md +0 -44
- package/.claude/project-structure.md +0 -149
- package/.github/workflows/ci.yml +0 -16
- package/.github/workflows/publish.yml +0 -51
- package/src/main.js +0 -89
- package/src/request.js +0 -85
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
[](https://github.com/abofs/stonyx-rest-server/actions/workflows/ci.yml)
|
|
2
|
+
[](https://www.npmjs.com/package/@stonyx/rest-server)
|
|
3
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
4
|
+
|
|
1
5
|
# @stonyx/rest-server
|
|
2
6
|
|
|
3
7
|
REST server module for the [Stonyx framework](https://github.com/abofs/stonyx), providing dynamic route registration and built-in request handling with optional authentication hooks.
|
|
@@ -75,8 +79,118 @@ Configuration is read from `stonyx/config` under `restServer`:
|
|
|
75
79
|
| `origin` | **String \| Array** | `'*'` | CORS origin(s) allowed |
|
|
76
80
|
| `methods` | **String** | `'GET,POST,PATCH,PUT,DELETE'` | CORS allowed methods |
|
|
77
81
|
| `enableHealthCheck` | **Boolean** | `true` | Register `GET /health` endpoint (disable via `REST_HEALTH_CHECK_DISABLE=true`) |
|
|
82
|
+
| `caseSensitiveRoutes` | **Boolean** | `true` | Match route paths case-sensitively. Disable via `REST_CASE_SENSITIVE_ROUTES=false`. See [Case-Sensitive Routing](#case-sensitive-routing) — **disabling this re-opens a security hole**. |
|
|
83
|
+
| `trustProxy` | **Boolean** | `false` | Trust reverse proxy headers (e.g. `X-Forwarded-Proto`). Enable via `REST_TRUST_PROXY=true` when running behind a load balancer such as AWS ALB/ELB to ensure correct protocol detection. |
|
|
78
84
|
| `statusMap` | **Object** | `{}` | Optional mapping of HTTP status codes to custom messages |
|
|
79
85
|
|
|
86
|
+
### Case-Sensitive Routing
|
|
87
|
+
|
|
88
|
+
Routes match **case-sensitively by default**. `GET /users` reaches a route
|
|
89
|
+
mounted at `/users`; `GET /Users` does not reach that mount, and
|
|
90
|
+
`GET /users/Success` does not reach a `/success` handler registered inside it.
|
|
91
|
+
|
|
92
|
+
Read [What this does not do](#what-this-does-not-do) before you rely on that
|
|
93
|
+
sentence. Two things it does not say: "does not reach the handler" is not the
|
|
94
|
+
same as "404", and casing is only one of the two ways express matches more
|
|
95
|
+
loosely than the authorization predicates written against it.
|
|
96
|
+
|
|
97
|
+
This is deliberate and security-relevant. Express matches case-insensitively by
|
|
98
|
+
default, which means any authorization written against the request URL can be
|
|
99
|
+
walked past by changing the case of the request:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
GET /owners/angela -> 404 (correctly filtered)
|
|
103
|
+
GET /OwNeRs/angela -> 200 (full record)
|
|
104
|
+
DELETE /ANIMALS/22 -> 204 (record destroyed)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The consumer's predicate is stricter than the router that dispatched the
|
|
108
|
+
request, so the router hands the handler a request the predicate would have
|
|
109
|
+
rejected. Case-sensitive matching closes the **casing** half of that asymmetry:
|
|
110
|
+
the path a handler sees can only ever be the exact registered casing.
|
|
111
|
+
|
|
112
|
+
It does not close the asymmetry itself. Express exposes `case sensitive
|
|
113
|
+
routing` and `strict routing` as a pair of loose-by-default router settings and
|
|
114
|
+
this change sets only the first, so the identical bypass is still reachable by
|
|
115
|
+
appending a slash. Measured on this release against this repo's own fixture:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
GET /private/failure -> 505 (auth hook fires, request blocked)
|
|
119
|
+
GET /private/failure/ -> 200 (auth hook never fires, handler runs)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
That is the same defect, one character instead of a case shift — translated to
|
|
123
|
+
the example above, `DELETE /animals/22` is filtered and `DELETE /animals/22/`
|
|
124
|
+
destroys the record. It is tracked as
|
|
125
|
+
[#50](https://github.com/abofs/stonyx-rest-server/issues/50) and is not fixed
|
|
126
|
+
here; it is a second consumer-visible behaviour change that needs its own flag
|
|
127
|
+
and its own release note.
|
|
128
|
+
|
|
129
|
+
**So do not drop a URL-normalizing defence you already have on the strength of
|
|
130
|
+
this section.** If your authorization compares `req.path` or `req.originalUrl`,
|
|
131
|
+
keep whatever normalization you have until #50 ships.
|
|
132
|
+
|
|
133
|
+
#### What this does not do
|
|
134
|
+
|
|
135
|
+
**It does not normalize path *parameter values*.** If your `auth()` hook rejects
|
|
136
|
+
`params.id === 'restricted'`, then `GET /private/RESTRICTED` still reaches the
|
|
137
|
+
handler — the router matched the route correctly, and `restricted` and
|
|
138
|
+
`RESTRICTED` are different values. Record ids are legitimately case-sensitive,
|
|
139
|
+
so this is a comparison your application owns. Compare param values with the
|
|
140
|
+
same case-handling you use when you look them up.
|
|
141
|
+
|
|
142
|
+
**A sub-path that misses is not necessarily a 404.** If the route class also
|
|
143
|
+
registers a param route such as `/:id`, a mis-cased sub-path is absorbed by it
|
|
144
|
+
rather than rejected. `GET /private/FAILURE` misses `/failure` and is dispatched
|
|
145
|
+
to `/:id` with `id="FAILURE"` — a different handler, at 200, not a miss; this
|
|
146
|
+
repo's AC5 asserts exactly that. A class exposing `/orders/summary` alongside
|
|
147
|
+
`/orders/:id` will send `GET /orders/SUMMARY` into the `/:id` handler and its
|
|
148
|
+
database lookup. The param route's own `auth()` hook still runs, so this is an
|
|
149
|
+
expectation defect rather than a bypass — but plan for a reroute, not a 404.
|
|
150
|
+
|
|
151
|
+
**It does not cover trailing slashes.** See
|
|
152
|
+
[#50](https://github.com/abofs/stonyx-rest-server/issues/50) above.
|
|
153
|
+
|
|
154
|
+
**It does not redirect or rewrite** mixed-case requests to their canonical
|
|
155
|
+
casing. Whether `/Users` is a typo to forgive or an attack to reject is an
|
|
156
|
+
application policy decision, and encoding it here would mint another variant of
|
|
157
|
+
the bug above.
|
|
158
|
+
|
|
159
|
+
#### Opting out
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
REST_CASE_SENSITIVE_ROUTES=false
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**This restores the vulnerability described above** — any URL-based
|
|
166
|
+
authorization in your application becomes bypassable by changing case. It
|
|
167
|
+
exists as a one-line remediation for an existing deployment, not as a
|
|
168
|
+
configuration to run on.
|
|
169
|
+
|
|
170
|
+
You need it if clients call your endpoints with casing that does not match the
|
|
171
|
+
mount path. Mount paths come from filenames, so this is not hypothetical:
|
|
172
|
+
|
|
173
|
+
- with `camelCaseRoutes` truthy, `phone-number.ts` mounts at `/phoneNumber`, and
|
|
174
|
+
`GET /phonenumber` now returns 404
|
|
175
|
+
- with `camelCaseRoutes` falsy, filenames are used verbatim, so
|
|
176
|
+
`Users.ts` mounts at `/Users` and `GET /users` now returns 404
|
|
177
|
+
|
|
178
|
+
A request that stops matching returns express's default `404 Cannot GET /x` with
|
|
179
|
+
no log line and no stack, so it looks like a deploy that dropped a route. Set
|
|
180
|
+
the flag to restore service, then fix the client's casing and remove the flag.
|
|
181
|
+
|
|
182
|
+
### Running Behind a Load Balancer
|
|
183
|
+
|
|
184
|
+
When your application runs behind a reverse proxy or load balancer (e.g. AWS ALB/ELB), the load balancer terminates SSL and forwards requests to your server over HTTP internally. This means Express sees `http` as the protocol even though the original client request used `https`.
|
|
185
|
+
|
|
186
|
+
To fix this, enable the `trustProxy` option:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
REST_TRUST_PROXY=true
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
This tells Express to trust the `X-Forwarded-Proto` header set by the load balancer, so `request.protocol` correctly returns `https`. This is important for any functionality that generates URLs based on the incoming request protocol, such as JSON:API relationship links.
|
|
193
|
+
|
|
80
194
|
## Request Class
|
|
81
195
|
|
|
82
196
|
The `Request` class provides a structured way to define route handlers and authorization hooks. Route classes extend `Request` and define:
|
package/config/environment.js
CHANGED
|
@@ -1,17 +1,44 @@
|
|
|
1
1
|
const {
|
|
2
|
+
REST_CASE_SENSITIVE_ROUTES,
|
|
2
3
|
REST_CORS_ORIGIN,
|
|
3
4
|
REST_CORS_METHODS,
|
|
4
5
|
REST_HEALTH_CHECK_DISABLE,
|
|
5
6
|
REST_PORT,
|
|
6
|
-
REST_REQUEST_PATH
|
|
7
|
-
|
|
7
|
+
REST_REQUEST_PATH,
|
|
8
|
+
REST_TRUST_PROXY
|
|
9
|
+
} = process.env;
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
const config = {
|
|
12
|
+
// Secure by default: routes match case-sensitively so a consumer's
|
|
13
|
+
// URL-based authorization cannot be walked past by changing case
|
|
14
|
+
// (abofs/stonyx-rest-server#47). Opt out with REST_CASE_SENSITIVE_ROUTES=false
|
|
15
|
+
// only as a temporary remediation for a client that relies on loose casing.
|
|
16
|
+
//
|
|
17
|
+
// DELIBERATELY NOT PINNED in test/config/environment.ts -- do not "fix" this
|
|
18
|
+
// as part of abofs/stonyx-rest-server#43. This line is the only thing the
|
|
19
|
+
// suite still checks about the SHIPPED default. Inverting it to
|
|
20
|
+
// `=== 'true'` turns AC3, AC4 and AC5 red; AC6 stays GREEN, because AC6
|
|
21
|
+
// stubs `caseSensitiveRoutes` to `undefined` and src/route-matching.ts reads
|
|
22
|
+
// `!== false`, so AC6 guards the source's read and not this default.
|
|
23
|
+
// Measured: pin `caseSensitiveRoutes: true` in test/config/environment.ts AND
|
|
24
|
+
// invert this line, and the suite reports 28 pass / 0 fail. A naive pin makes
|
|
25
|
+
// an insecure published default completely invisible to a green suite --
|
|
26
|
+
// quieter and weaker, which is the outcome pinning was supposed to prevent.
|
|
27
|
+
//
|
|
28
|
+
// The cost of leaving it unpinned is that the suite is ambient-sensitive here
|
|
29
|
+
// (`REST_CASE_SENSITIVE_ROUTES=false pnpm test` => 25 pass / 3 fail), but it
|
|
30
|
+
// fails LOUDLY, so there is no false green. Closing #43 for this key needs
|
|
31
|
+
// the subprocess-based env isolation this repo does not yet have; any fix
|
|
32
|
+
// must keep a live assertion on this default.
|
|
33
|
+
caseSensitiveRoutes: REST_CASE_SENSITIVE_ROUTES !== 'false',
|
|
10
34
|
enableHealthCheck: REST_HEALTH_CHECK_DISABLE !== 'true',
|
|
11
35
|
origin: REST_CORS_ORIGIN ?? '*',
|
|
12
36
|
methods: REST_CORS_METHODS ?? 'GET,POST,PATCH,PUT,DELETE',
|
|
13
37
|
dir: REST_REQUEST_PATH ?? './requests',
|
|
14
38
|
port: REST_PORT ?? 2666,
|
|
39
|
+
trustProxy: REST_TRUST_PROXY === 'true',
|
|
15
40
|
logColor: 'yellow',
|
|
16
41
|
logMethod: 'api'
|
|
17
42
|
};
|
|
43
|
+
|
|
44
|
+
export default config;
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Express } from 'express';
|
|
2
|
+
import type { Server } from 'http';
|
|
3
|
+
export { default as Request } from './request.js';
|
|
4
|
+
export default class RestServer {
|
|
5
|
+
static instance: RestServer;
|
|
6
|
+
api: Express;
|
|
7
|
+
server: Server;
|
|
8
|
+
constructor();
|
|
9
|
+
static close(): void;
|
|
10
|
+
init(): Promise<void>;
|
|
11
|
+
setupRouter(): Promise<void>;
|
|
12
|
+
setupGlobalMiddleware(): void;
|
|
13
|
+
mountRoute(routeClassUntyped: unknown, { name, options }: {
|
|
14
|
+
name: string;
|
|
15
|
+
options?: unknown;
|
|
16
|
+
}): void;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=main.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAiBA,OAAgB,EAAE,KAAK,OAAO,EAAoE,MAAM,SAAS,CAAC;AAKlH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,CAAC,OAAO,OAAO,UAAU;IAC7B,MAAM,CAAC,QAAQ,EAAE,UAAU,CAAC;IAE5B,GAAG,EAAG,OAAO,CAAC;IACd,MAAM,EAAG,MAAM,CAAC;;IAgBhB,MAAM,CAAC,KAAK,IAAI,IAAI;IAQd,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAerB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAelC,qBAAqB,IAAI,IAAI;IAW7B,UAAU,CAAC,iBAAiB,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI;CAarG"}
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Stone Costa
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the 'License');
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import cors from 'cors';
|
|
17
|
+
import express from 'express';
|
|
18
|
+
import config from 'stonyx/config';
|
|
19
|
+
import log from 'stonyx/log';
|
|
20
|
+
import { forEachFileImport } from '@stonyx/utils/file';
|
|
21
|
+
import applyRouteMatching from './route-matching.js';
|
|
22
|
+
export { default as Request } from './request.js';
|
|
23
|
+
export default class RestServer {
|
|
24
|
+
static instance;
|
|
25
|
+
api;
|
|
26
|
+
server;
|
|
27
|
+
constructor() {
|
|
28
|
+
if (RestServer.instance)
|
|
29
|
+
return RestServer.instance;
|
|
30
|
+
RestServer.instance = this;
|
|
31
|
+
this.api = express();
|
|
32
|
+
// Closes the mount segment (/PUBLIC/...) for abofs/stonyx-rest-server#47.
|
|
33
|
+
// Must stay in the constructor: the router is materialized lazily on first
|
|
34
|
+
// route registration, so applying this after setupRouter() is silently
|
|
35
|
+
// ineffective. The matching call in Request's constructor is what closes
|
|
36
|
+
// sub-paths -- see src/route-matching.ts for why both are required.
|
|
37
|
+
applyRouteMatching(this.api);
|
|
38
|
+
}
|
|
39
|
+
static close() {
|
|
40
|
+
if (!RestServer.instance)
|
|
41
|
+
throw new Error('RestServer has not been initialized yet');
|
|
42
|
+
const { server } = RestServer.instance;
|
|
43
|
+
server.closeAllConnections();
|
|
44
|
+
server.close();
|
|
45
|
+
}
|
|
46
|
+
async init() {
|
|
47
|
+
// Self-register so log.api works even when @stonyx/rest-server is in the
|
|
48
|
+
// consumer's `dependencies` (stonyx loader only merges devDependencies).
|
|
49
|
+
const { logColor = 'yellow', logMethod = 'api' } = config.restServer;
|
|
50
|
+
log.defineType(logMethod, logColor);
|
|
51
|
+
await this.setupRouter();
|
|
52
|
+
const { port } = config.restServer;
|
|
53
|
+
// start REST server
|
|
54
|
+
this.server = this.api.listen(port);
|
|
55
|
+
log.title(`API Server is listening on port ${port}`);
|
|
56
|
+
}
|
|
57
|
+
async setupRouter() {
|
|
58
|
+
const { camelCaseRoutes, dir, enableHealthCheck } = config.restServer;
|
|
59
|
+
this.setupGlobalMiddleware();
|
|
60
|
+
try {
|
|
61
|
+
await forEachFileImport(dir, this.mountRoute.bind(this), { rawName: !camelCaseRoutes, ignoreAccessFailure: true });
|
|
62
|
+
if (enableHealthCheck)
|
|
63
|
+
this.api.get('/health', (_req, res) => res.sendStatus(200));
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (config.debug)
|
|
67
|
+
console.log(error);
|
|
68
|
+
log.error(`Unable to dynamically configure routes from files in ${dir}`);
|
|
69
|
+
throw new Error(`Unable to dynamically configure routes from files in ${dir}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
setupGlobalMiddleware() {
|
|
73
|
+
const { origin, methods, trustProxy } = config.restServer;
|
|
74
|
+
if (trustProxy)
|
|
75
|
+
this.api.set('trust proxy', true);
|
|
76
|
+
this.api.use([
|
|
77
|
+
cors({ origin, methods }),
|
|
78
|
+
express.json()
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
mountRoute(routeClassUntyped, { name, options }) {
|
|
82
|
+
const routeClass = routeClassUntyped;
|
|
83
|
+
const { api } = this;
|
|
84
|
+
const classInstance = new routeClass(options);
|
|
85
|
+
const route = name === 'index' ? '/' : `/${name}`;
|
|
86
|
+
const { expressInstance } = classInstance;
|
|
87
|
+
classInstance.registerCalls();
|
|
88
|
+
expressInstance.mountpath = route;
|
|
89
|
+
// Mount handler to main api instance
|
|
90
|
+
api.use(route, expressInstance);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=main.js.map
|
package/dist/main.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Request as ExpressRequest, type Response as ExpressResponse, type Express } from 'express';
|
|
2
|
+
export type RequestState = Record<string, unknown>;
|
|
3
|
+
export type RequestHandler = (req: ExpressRequest, state: RequestState) => unknown | Promise<unknown>;
|
|
4
|
+
export type AuthHandler = (req: ExpressRequest, state: RequestState) => number | undefined;
|
|
5
|
+
export type RouteHandlers = Record<string, Record<string, RequestHandler | RequestHandler[]>>;
|
|
6
|
+
export default class Request {
|
|
7
|
+
static stateProp: string;
|
|
8
|
+
static getState(req: ExpressRequest): RequestState;
|
|
9
|
+
static sendStatusResponse(res: ExpressResponse, status: number): void;
|
|
10
|
+
expressInstance: Express;
|
|
11
|
+
handlers: RouteHandlers;
|
|
12
|
+
auth?: AuthHandler;
|
|
13
|
+
constructor();
|
|
14
|
+
registerCalls(): void;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=request.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,KAAK,OAAO,IAAI,cAAc,EAAE,KAAK,QAAQ,IAAI,eAAe,EAAE,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAOlH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AACnD,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AACtG,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,MAAM,GAAG,SAAS,CAAC;AAC3F,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,EAAE,CAAC,CAAC,CAAC;AAE9F,MAAM,CAAC,OAAO,OAAO,OAAO;IAC1B,MAAM,CAAC,SAAS,SAAmB;IAEnC,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,YAAY;IASlD,MAAM,CAAC,kBAAkB,CAAC,GAAG,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAWrE,eAAe,EAAE,OAAO,CAAC;IACzB,QAAQ,EAAG,aAAa,CAAC;IACjB,IAAI,CAAC,EAAE,WAAW,CAAC;;IAe3B,aAAa,IAAI,IAAI;CAqDtB"}
|
package/dist/request.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import config from 'stonyx/config';
|
|
3
|
+
import { makeArray } from '@stonyx/utils/object';
|
|
4
|
+
import applyRouteMatching from './route-matching.js';
|
|
5
|
+
const METHODS = new Set(['get', 'post', 'put', 'delete', 'patch']);
|
|
6
|
+
export default class Request {
|
|
7
|
+
static stateProp = '__stonyxState';
|
|
8
|
+
static getState(req) {
|
|
9
|
+
const { stateProp } = Request;
|
|
10
|
+
const record = req;
|
|
11
|
+
if (record[stateProp] !== undefined)
|
|
12
|
+
return record[stateProp];
|
|
13
|
+
record[stateProp] = {};
|
|
14
|
+
return record[stateProp];
|
|
15
|
+
}
|
|
16
|
+
static sendStatusResponse(res, status) {
|
|
17
|
+
const statusMap = config.restServer?.statusMap ?? {};
|
|
18
|
+
const message = statusMap[status] || '';
|
|
19
|
+
if (message) {
|
|
20
|
+
res.status(status).send(message);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
res.sendStatus(status);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
expressInstance;
|
|
27
|
+
handlers;
|
|
28
|
+
constructor() {
|
|
29
|
+
const api = express();
|
|
30
|
+
api.disable('x-powered-by');
|
|
31
|
+
// Closes sub-paths (/public/SUCCESS) for abofs/stonyx-rest-server#47.
|
|
32
|
+
// Must stay in the constructor: registerCalls() materializes this router,
|
|
33
|
+
// and a set applied afterwards has no effect. The parent app's setting
|
|
34
|
+
// does not reach here -- see src/route-matching.ts.
|
|
35
|
+
applyRouteMatching(api);
|
|
36
|
+
this.expressInstance = api;
|
|
37
|
+
}
|
|
38
|
+
registerCalls() {
|
|
39
|
+
const { expressInstance } = this;
|
|
40
|
+
const { getState, sendStatusResponse } = Request;
|
|
41
|
+
for (const [method, handlers] of Object.entries(this.handlers)) {
|
|
42
|
+
if (!METHODS.has(method)) {
|
|
43
|
+
console.warn(`Method "${method}" is not a valid HTTP method. Skipping...`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
for (const [route, handler] of Object.entries(handlers)) {
|
|
47
|
+
expressInstance[method](route, async (req, res) => {
|
|
48
|
+
// Run auth after route matching so request.params is populated
|
|
49
|
+
if (this.auth) {
|
|
50
|
+
const status = this.auth(req, getState(req));
|
|
51
|
+
if (status)
|
|
52
|
+
return sendStatusResponse(res, status);
|
|
53
|
+
}
|
|
54
|
+
const callStack = [...makeArray(handler)];
|
|
55
|
+
const mainCall = callStack.pop();
|
|
56
|
+
let response;
|
|
57
|
+
// Run middleware
|
|
58
|
+
while (callStack.length) {
|
|
59
|
+
response = await callStack.shift().bind(this)(req, getState(req));
|
|
60
|
+
if (response !== undefined)
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
if (response === undefined)
|
|
64
|
+
response = await mainCall(req, getState(req));
|
|
65
|
+
if (Number.isInteger(response))
|
|
66
|
+
return sendStatusResponse(res, response);
|
|
67
|
+
// Handle redirect if set via call state object
|
|
68
|
+
const state = getState(req);
|
|
69
|
+
const { redirect } = state;
|
|
70
|
+
if (redirect)
|
|
71
|
+
return res.redirect(redirect);
|
|
72
|
+
// Handle pipe if set via call state object
|
|
73
|
+
const { pipe } = state;
|
|
74
|
+
if (pipe) {
|
|
75
|
+
const { headers, source } = pipe;
|
|
76
|
+
if (headers)
|
|
77
|
+
for (const [key, value] of Object.entries(headers))
|
|
78
|
+
res.set(key, value);
|
|
79
|
+
return source.pipe(res);
|
|
80
|
+
}
|
|
81
|
+
if (response === undefined) {
|
|
82
|
+
res.sendStatus(200);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (typeof response !== 'object')
|
|
86
|
+
return sendStatusResponse(res, 500);
|
|
87
|
+
res.send(response);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=request.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request.js","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Express } from 'express';
|
|
2
|
+
/**
|
|
3
|
+
* Applies this module's route-matching settings to an express app.
|
|
4
|
+
*
|
|
5
|
+
* Called from BOTH express construction sites (abofs/stonyx-rest-server#47):
|
|
6
|
+
* `RestServer`'s constructor closes the mount segment (`/PUBLIC/...`), and
|
|
7
|
+
* `Request`'s constructor closes sub-paths (`/public/SUCCESS`). Neither alone
|
|
8
|
+
* is sufficient -- settings are inherited on mount, but `mountRoute()` calls
|
|
9
|
+
* `registerCalls()` before `api.use()`, so each child router is already built
|
|
10
|
+
* by the time the parent's setting could reach it.
|
|
11
|
+
*
|
|
12
|
+
* Both callers invoke this from a constructor, and must keep doing so: express
|
|
13
|
+
* materializes a router lazily on first route registration, and a setting
|
|
14
|
+
* applied afterwards is silently ineffective -- no throw, no warning.
|
|
15
|
+
*
|
|
16
|
+
* The guard is `!== false`, not a plain truthy check, and that polarity is
|
|
17
|
+
* load-bearing. `trustProxy` and `enableHealthCheck` default to the falsy
|
|
18
|
+
* direction, so a missing key fails safe for them. This flag defaults to the
|
|
19
|
+
* truthy direction, so `if (config.restServer?.caseSensitiveRoutes)` would
|
|
20
|
+
* silently fail OPEN for a consumer whose shipped config predates the key.
|
|
21
|
+
*
|
|
22
|
+
* It lives here, in one place, rather than being written out at each call
|
|
23
|
+
* site, so that a single test can anchor it. The invariant is duplicated the
|
|
24
|
+
* moment the expression is: `test/unit/request-test.ts` AC6 reaches this
|
|
25
|
+
* function through `Request`, which means the same assertion now also covers
|
|
26
|
+
* the `RestServer` half. Two copies of the predicate left the parent's copy
|
|
27
|
+
* free to drift -- inverting it, or dropping the condition entirely, kept the
|
|
28
|
+
* suite green.
|
|
29
|
+
*
|
|
30
|
+
* Note `express({ caseSensitive: true })` does NOT work: express 5's
|
|
31
|
+
* `createApplication()` takes zero arguments and forwards nothing. The app
|
|
32
|
+
* setting is the only mechanism.
|
|
33
|
+
*/
|
|
34
|
+
export default function applyRouteMatching(api: Express): void;
|
|
35
|
+
//# sourceMappingURL=route-matching.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-matching.d.ts","sourceRoot":"","sources":["../src/route-matching.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAGvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAE7D"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import config from 'stonyx/config';
|
|
2
|
+
/**
|
|
3
|
+
* Applies this module's route-matching settings to an express app.
|
|
4
|
+
*
|
|
5
|
+
* Called from BOTH express construction sites (abofs/stonyx-rest-server#47):
|
|
6
|
+
* `RestServer`'s constructor closes the mount segment (`/PUBLIC/...`), and
|
|
7
|
+
* `Request`'s constructor closes sub-paths (`/public/SUCCESS`). Neither alone
|
|
8
|
+
* is sufficient -- settings are inherited on mount, but `mountRoute()` calls
|
|
9
|
+
* `registerCalls()` before `api.use()`, so each child router is already built
|
|
10
|
+
* by the time the parent's setting could reach it.
|
|
11
|
+
*
|
|
12
|
+
* Both callers invoke this from a constructor, and must keep doing so: express
|
|
13
|
+
* materializes a router lazily on first route registration, and a setting
|
|
14
|
+
* applied afterwards is silently ineffective -- no throw, no warning.
|
|
15
|
+
*
|
|
16
|
+
* The guard is `!== false`, not a plain truthy check, and that polarity is
|
|
17
|
+
* load-bearing. `trustProxy` and `enableHealthCheck` default to the falsy
|
|
18
|
+
* direction, so a missing key fails safe for them. This flag defaults to the
|
|
19
|
+
* truthy direction, so `if (config.restServer?.caseSensitiveRoutes)` would
|
|
20
|
+
* silently fail OPEN for a consumer whose shipped config predates the key.
|
|
21
|
+
*
|
|
22
|
+
* It lives here, in one place, rather than being written out at each call
|
|
23
|
+
* site, so that a single test can anchor it. The invariant is duplicated the
|
|
24
|
+
* moment the expression is: `test/unit/request-test.ts` AC6 reaches this
|
|
25
|
+
* function through `Request`, which means the same assertion now also covers
|
|
26
|
+
* the `RestServer` half. Two copies of the predicate left the parent's copy
|
|
27
|
+
* free to drift -- inverting it, or dropping the condition entirely, kept the
|
|
28
|
+
* suite green.
|
|
29
|
+
*
|
|
30
|
+
* Note `express({ caseSensitive: true })` does NOT work: express 5's
|
|
31
|
+
* `createApplication()` takes zero arguments and forwards nothing. The app
|
|
32
|
+
* setting is the only mechanism.
|
|
33
|
+
*/
|
|
34
|
+
export default function applyRouteMatching(api) {
|
|
35
|
+
if (config.restServer?.caseSensitiveRoutes !== false)
|
|
36
|
+
api.set('case sensitive routing', true);
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=route-matching.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-matching.js","sourceRoot":"","sources":["../src/route-matching.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,eAAe,CAAC;AAEnC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,GAAY;IACrD,IAAI,MAAM,CAAC,UAAU,EAAE,mBAAmB,KAAK,KAAK;QAAE,GAAG,CAAC,GAAG,CAAC,wBAAwB,EAAE,IAAI,CAAC,CAAC;AAChG,CAAC"}
|
package/package.json
CHANGED
|
@@ -4,22 +4,31 @@
|
|
|
4
4
|
"stonyx-async",
|
|
5
5
|
"stonyx-module"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.2.1-beta.
|
|
7
|
+
"version": "0.2.1-beta.90",
|
|
8
8
|
"description": "Rest Server Module for Stonyx Framework",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "https://github.com/abofs/stonyx-rest-server"
|
|
12
12
|
},
|
|
13
|
-
"main": "
|
|
13
|
+
"main": "dist/main.js",
|
|
14
|
+
"types": "dist/main.d.ts",
|
|
14
15
|
"type": "module",
|
|
15
16
|
"exports": {
|
|
16
|
-
".":
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/main.d.ts",
|
|
19
|
+
"default": "./dist/main.js"
|
|
20
|
+
}
|
|
17
21
|
},
|
|
18
22
|
"author": "Stone Costa",
|
|
19
23
|
"license": "Apache-2.0",
|
|
20
24
|
"contributors": [
|
|
21
25
|
"Stone Costa <stone.costa@synamicd.com>"
|
|
22
26
|
],
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"config",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
23
32
|
"publishConfig": {
|
|
24
33
|
"access": "public",
|
|
25
34
|
"provenance": true
|
|
@@ -27,14 +36,23 @@
|
|
|
27
36
|
"dependencies": {
|
|
28
37
|
"cors": "^2.8.5",
|
|
29
38
|
"express": "^5.1.0",
|
|
30
|
-
"stonyx": "0.2.3-beta.
|
|
39
|
+
"stonyx": "0.2.3-beta.78"
|
|
31
40
|
},
|
|
32
41
|
"devDependencies": {
|
|
33
|
-
"@stonyx/utils": "0.2.3-beta.
|
|
42
|
+
"@stonyx/utils": "0.2.3-beta.26",
|
|
43
|
+
"@types/cors": "^2.8.17",
|
|
44
|
+
"@types/express": "^5.0.6",
|
|
45
|
+
"@types/node": "^25.5.2",
|
|
46
|
+
"@types/qunit": "^2.19.13",
|
|
47
|
+
"@types/sinon": "^21.0.1",
|
|
34
48
|
"qunit": "^2.24.1",
|
|
35
|
-
"sinon": "^21.0.0"
|
|
49
|
+
"sinon": "^21.0.0",
|
|
50
|
+
"tsx": "^4.21.0",
|
|
51
|
+
"typescript": "^5.8.3"
|
|
36
52
|
},
|
|
37
53
|
"scripts": {
|
|
38
|
-
"
|
|
54
|
+
"build": "tsc",
|
|
55
|
+
"typecheck": "tsc -p tsconfig.test.json",
|
|
56
|
+
"test": "pnpm build && pnpm typecheck && NODE_ENV=test node --import tsx/esm --import ./test/setup.ts node_modules/qunit/bin/qunit.js 'test/**/*-test.ts'"
|
|
39
57
|
}
|
|
40
58
|
}
|
package/.claude/improvements.md
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# Improvement Opportunities
|
|
2
|
-
|
|
3
|
-
## 1. `new express()` in RestServer constructor
|
|
4
|
-
|
|
5
|
-
**File**: `src/main.js`, line 30
|
|
6
|
-
|
|
7
|
-
```js
|
|
8
|
-
this.api = new express();
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Express 5 (which this project targets via `"express": "^5.1.0"`) documents calling `express()` as a plain function, not as a constructor with `new`. While `new express()` works in practice because the function returns a new object regardless, it is unconventional and may break if Express ever enforces non-constructor semantics. Consider changing to:
|
|
12
|
-
|
|
13
|
-
```js
|
|
14
|
-
this.api = express();
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Note: The `Request` class in `src/request.js` line 26 already uses the correct pattern (`const api = express();` without `new`).
|
|
18
|
-
|
|
19
|
-
## 2. `setupGlobalMiddleware` is `async` but contains no awaits
|
|
20
|
-
|
|
21
|
-
**File**: `src/main.js`, lines 63-70
|
|
22
|
-
|
|
23
|
-
```js
|
|
24
|
-
async setupGlobalMiddleware() {
|
|
25
|
-
const { origin, methods } = config.restServer;
|
|
26
|
-
|
|
27
|
-
this.api.use([
|
|
28
|
-
cors({ origin, methods }),
|
|
29
|
-
express.json()
|
|
30
|
-
]);
|
|
31
|
-
}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
The `async` keyword is unnecessary here since the method body contains no `await` expressions and `app.use()` is synchronous. The `async` keyword causes the method to return a `Promise` wrapping `undefined`, but the caller (`setupRouter`) does not `await` it either — it calls `this.setupGlobalMiddleware()` without `await` on line 52. Removing `async` would make the intent clearer.
|
|
35
|
-
|
|
36
|
-
## 3. `logMethod` config option is not used in rest-server source
|
|
37
|
-
|
|
38
|
-
**File**: `config/environment.js`, line 16
|
|
39
|
-
|
|
40
|
-
```js
|
|
41
|
-
logMethod: 'api'
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
The `logMethod` property is defined in the default config but is never referenced anywhere in `src/main.js` or `src/request.js`. It is likely consumed by the Stonyx framework core (`stonyx/log`) for registering a named log method, but this should be verified. If it is indeed framework-level plumbing, consider documenting that it is a Stonyx convention rather than a rest-server feature. If it is unused, consider removing it to reduce config surface area.
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
# @stonyx/rest-server — Project Structure
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
REST server module for the Stonyx framework. Provides dynamic route registration from a file directory, built-in CORS/JSON middleware, per-route authorization hooks, and a structured `Request` base class for defining handlers.
|
|
6
|
-
|
|
7
|
-
- **Package**: `@stonyx/rest-server` (v0.2.1-beta.1)
|
|
8
|
-
- **License**: Apache-2.0
|
|
9
|
-
- **Entry point**: `src/main.js`
|
|
10
|
-
- **Module type**: ESM (`"type": "module"`)
|
|
11
|
-
- **Node version**: v24.13.0 (per `.nvmrc`)
|
|
12
|
-
- **Package manager**: pnpm
|
|
13
|
-
|
|
14
|
-
## Architecture
|
|
15
|
-
|
|
16
|
-
### RestServer (src/main.js)
|
|
17
|
-
|
|
18
|
-
Singleton class wrapping an Express 5 instance.
|
|
19
|
-
|
|
20
|
-
- **Constructor** — enforces singleton via `RestServer.instance`; creates the Express app with `new express()`
|
|
21
|
-
- **`init()`** — calls `setupRouter()`, then starts listening on the configured port
|
|
22
|
-
- **`setupRouter()`** — calls `setupGlobalMiddleware()`, then uses `forEachFileImport` (from `@stonyx/utils/file`) to dynamically import all files in the configured `dir` and mount each as a route via `mountRoute()`. Optionally registers a `/health` endpoint.
|
|
23
|
-
- **`setupGlobalMiddleware()`** — attaches `cors()` and `express.json()` middleware to the Express app
|
|
24
|
-
- **`mountRoute(routeClass, { name, options })`** — instantiates the imported Request subclass, wires up the `authorization` middleware if present, calls `registerCalls()`, and mounts the sub-app at `/<filename>`
|
|
25
|
-
- **`RestServer.close()`** — static method to close the server
|
|
26
|
-
|
|
27
|
-
### Request (src/request.js)
|
|
28
|
-
|
|
29
|
-
Base class for route definitions. Each file in the requests directory exports a class extending `Request`.
|
|
30
|
-
|
|
31
|
-
- **Constructor** — creates a child Express instance with `x-powered-by` disabled
|
|
32
|
-
- **`handlers`** — instance property: object mapping HTTP methods (`get`, `post`, `put`, `delete`, `patch`) to route-path/handler pairs
|
|
33
|
-
- **`auth(req, state)`** — optional hook. Return an integer status code to reject the request; return nothing to allow it through.
|
|
34
|
-
- **`authorization(req, res, next)`** — wrapper that calls `auth()` and short-circuits with a status response if it returns a code
|
|
35
|
-
- **`registerCalls()`** — iterates `handlers`, registers each route on the child Express instance. Supports:
|
|
36
|
-
- Single handler function or array (last element is the main handler, preceding elements are middleware)
|
|
37
|
-
- Middleware functions are bound to the class instance and executed in order
|
|
38
|
-
- Integer return = status code response (via `sendStatusResponse`)
|
|
39
|
-
- Object return = JSON response
|
|
40
|
-
- `undefined` return = 200 OK
|
|
41
|
-
- Pipe support via `state.pipe` (sets headers and pipes a stream)
|
|
42
|
-
- **`Request.getState(req)`** — attaches/retrieves a `__stonyxState` object on the Express request
|
|
43
|
-
- **`Request.sendStatusResponse(res, status)`** — sends status with optional custom message from `config.restServer.statusMap`
|
|
44
|
-
|
|
45
|
-
Valid HTTP methods (enforced): `get`, `post`, `put`, `delete`, `patch`
|
|
46
|
-
|
|
47
|
-
## Configuration Reference
|
|
48
|
-
|
|
49
|
-
From `config/environment.js`. All values are overridable via environment variables.
|
|
50
|
-
|
|
51
|
-
| Option | Type | Default | Env Var | Description |
|
|
52
|
-
|---------------------|-------------------|-------------------------------|----------------------------|-----------------------------------------------------------------|
|
|
53
|
-
| `enableHealthCheck` | **Boolean** | `true` | `REST_HEALTH_CHECK_DISABLE=true` to disable | Registers `GET /health` returning 200 |
|
|
54
|
-
| `origin` | **String** | `'*'` | `REST_CORS_ORIGIN` | CORS allowed origin(s) |
|
|
55
|
-
| `methods` | **String** | `'GET,POST,PATCH,PUT,DELETE'` | `REST_CORS_METHODS` | CORS allowed methods |
|
|
56
|
-
| `dir` | **String** | `'./requests'` | `REST_REQUEST_PATH` | Directory containing Request class files to mount as routes |
|
|
57
|
-
| `port` | **Number/String** | `2666` | `REST_PORT` | Port the REST server listens on |
|
|
58
|
-
| `logColor` | **String** | `'yellow'` | — | Console log color for this module (Stonyx logging integration) |
|
|
59
|
-
| `logMethod` | **String** | `'api'` | — | Log method name (Stonyx logging integration) |
|
|
60
|
-
|
|
61
|
-
Additional config used (not rest-server-specific):
|
|
62
|
-
- `config.debug` (top-level Stonyx config) — if truthy, logs errors during route setup
|
|
63
|
-
- `config.restServer.statusMap` (optional, no default in environment.js) — maps status codes to custom message strings
|
|
64
|
-
- `config.restServer.camelCaseRoutes` (optional, no default in environment.js) — when falsy, passes `rawName: true` to `forEachFileImport` so filenames are used as-is for route paths
|
|
65
|
-
|
|
66
|
-
## Test Structure
|
|
67
|
-
|
|
68
|
-
Tests use **QUnit** and run via `stonyx test` (the `npm test` script).
|
|
69
|
-
|
|
70
|
-
### test/config/environment.js
|
|
71
|
-
Overrides `restServer.dir` to `'./test/sample/requests'` so tests load sample request classes.
|
|
72
|
-
|
|
73
|
-
### test/unit/request-test.js
|
|
74
|
-
Unit tests for `Request` static methods:
|
|
75
|
-
- `getState` — creates/returns state object on request
|
|
76
|
-
- `sendStatusResponse` — sends status with optional `statusMap` message
|
|
77
|
-
|
|
78
|
-
### test/integration/rest-server-test.js
|
|
79
|
-
Integration tests that boot the full server and make HTTP requests:
|
|
80
|
-
- 404 for non-existent routes
|
|
81
|
-
- `/public` — JSON response, 200 OK default, URL params, middleware (success/failure), `this` binding for handlers and middleware
|
|
82
|
-
- `/private` — authenticated success, auth hook rejection (505)
|
|
83
|
-
- `/health` — health check endpoint returns 200
|
|
84
|
-
|
|
85
|
-
### test/sample/requests/
|
|
86
|
-
Sample Request subclasses used by integration tests:
|
|
87
|
-
- `public.js` — `PublicRequest` with various GET handlers demonstrating middleware, params, binding
|
|
88
|
-
- `private.js` — `PrivateRequest` with `auth()` hook that rejects `/failure` with 505
|
|
89
|
-
|
|
90
|
-
## CI/CD
|
|
91
|
-
|
|
92
|
-
### .github/workflows/ci.yml
|
|
93
|
-
Runs on pull requests to `dev` and `main`. Delegates to shared workflow at `abofs/stonyx-workflows`.
|
|
94
|
-
|
|
95
|
-
### .github/workflows/publish.yml
|
|
96
|
-
Publishes to NPM. Triggered by:
|
|
97
|
-
- `workflow_dispatch` with version-type selection (patch/minor/major) or custom version
|
|
98
|
-
- Pull requests to `main`/`dev`
|
|
99
|
-
- Pushes to `main`
|
|
100
|
-
|
|
101
|
-
Delegates to `abofs/stonyx-workflows/.github/workflows/npm-publish.yml@main`.
|
|
102
|
-
|
|
103
|
-
## File Structure
|
|
104
|
-
|
|
105
|
-
```
|
|
106
|
-
stonyx-rest-server/
|
|
107
|
-
├── .claude/
|
|
108
|
-
│ ├── improvements.md
|
|
109
|
-
│ └── project-structure.md # this file
|
|
110
|
-
├── .github/
|
|
111
|
-
│ └── workflows/
|
|
112
|
-
│ ├── ci.yml # PR CI — delegates to shared workflow
|
|
113
|
-
│ └── publish.yml # NPM publish — delegates to shared workflow
|
|
114
|
-
├── config/
|
|
115
|
-
│ └── environment.js # Default config with env var overrides
|
|
116
|
-
├── src/
|
|
117
|
-
│ ├── main.js # RestServer class (singleton, Express wrapper)
|
|
118
|
-
│ └── request.js # Request base class (handler registration, auth hook)
|
|
119
|
-
├── test/
|
|
120
|
-
│ ├── config/
|
|
121
|
-
│ │ └── environment.js # Test config override (dir → test/sample/requests)
|
|
122
|
-
│ ├── integration/
|
|
123
|
-
│ │ └── rest-server-test.js # Integration tests (QUnit)
|
|
124
|
-
│ ├── sample/
|
|
125
|
-
│ │ └── requests/
|
|
126
|
-
│ │ ├── private.js # Sample private request with auth hook
|
|
127
|
-
│ │ └── public.js # Sample public request with middleware demos
|
|
128
|
-
│ └── unit/
|
|
129
|
-
│ └── request-test.js # Unit tests for Request statics (QUnit)
|
|
130
|
-
├── .gitignore
|
|
131
|
-
├── .npmignore
|
|
132
|
-
├── .nvmrc # Node v24.13.0
|
|
133
|
-
├── LICENSE.md # Apache 2.0
|
|
134
|
-
├── package.json
|
|
135
|
-
├── pnpm-lock.yaml
|
|
136
|
-
└── README.md
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
## Dependencies
|
|
140
|
-
|
|
141
|
-
### Runtime
|
|
142
|
-
- `cors` ^2.8.5 — CORS middleware
|
|
143
|
-
- `express` ^5.1.0 — HTTP framework
|
|
144
|
-
- `stonyx` (local link) — Framework core (config, logging)
|
|
145
|
-
|
|
146
|
-
### Dev
|
|
147
|
-
- `@stonyx/utils` (local link) — Utility functions (file import, object helpers)
|
|
148
|
-
- `qunit` ^2.24.1 — Test framework
|
|
149
|
-
- `sinon` ^21.0.0 — Test stubs/spies
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
branches: [dev, main]
|
|
6
|
-
|
|
7
|
-
concurrency:
|
|
8
|
-
group: ci-${{ github.head_ref || github.ref }}
|
|
9
|
-
cancel-in-progress: true
|
|
10
|
-
|
|
11
|
-
permissions:
|
|
12
|
-
contents: read
|
|
13
|
-
|
|
14
|
-
jobs:
|
|
15
|
-
test:
|
|
16
|
-
uses: abofs/stonyx-workflows/.github/workflows/ci.yml@main
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
name: Publish to NPM
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
repository_dispatch:
|
|
5
|
-
types: [cascade-publish]
|
|
6
|
-
workflow_dispatch:
|
|
7
|
-
inputs:
|
|
8
|
-
version-type:
|
|
9
|
-
description: 'Version type'
|
|
10
|
-
required: true
|
|
11
|
-
type: choice
|
|
12
|
-
options:
|
|
13
|
-
- patch
|
|
14
|
-
- minor
|
|
15
|
-
- major
|
|
16
|
-
custom-version:
|
|
17
|
-
description: 'Custom version (optional, overrides version-type)'
|
|
18
|
-
required: false
|
|
19
|
-
type: string
|
|
20
|
-
pull_request:
|
|
21
|
-
types: [opened, synchronize, reopened]
|
|
22
|
-
branches: [main]
|
|
23
|
-
push:
|
|
24
|
-
branches: [main]
|
|
25
|
-
|
|
26
|
-
concurrency:
|
|
27
|
-
group: ${{ github.event_name == 'repository_dispatch' && 'cascade-update' || format('publish-{0}', github.ref) }}
|
|
28
|
-
cancel-in-progress: false
|
|
29
|
-
|
|
30
|
-
permissions:
|
|
31
|
-
contents: write
|
|
32
|
-
id-token: write
|
|
33
|
-
pull-requests: write
|
|
34
|
-
|
|
35
|
-
jobs:
|
|
36
|
-
publish:
|
|
37
|
-
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
|
38
|
-
uses: abofs/stonyx-workflows/.github/workflows/npm-publish.yml@main
|
|
39
|
-
with:
|
|
40
|
-
version-type: ${{ github.event.inputs.version-type }}
|
|
41
|
-
custom-version: ${{ github.event.inputs.custom-version }}
|
|
42
|
-
cascade-source: ${{ github.event.client_payload.source_package || '' }}
|
|
43
|
-
secrets: inherit
|
|
44
|
-
|
|
45
|
-
cascade:
|
|
46
|
-
needs: publish
|
|
47
|
-
uses: abofs/stonyx-workflows/.github/workflows/cascade.yml@main
|
|
48
|
-
with:
|
|
49
|
-
package-name: ${{ needs.publish.outputs.package-name }}
|
|
50
|
-
published-version: ${{ needs.publish.outputs.published-version }}
|
|
51
|
-
secrets: inherit
|
package/src/main.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2025 Stone Costa
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the 'License');
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import cors from 'cors';
|
|
18
|
-
import express from 'express';
|
|
19
|
-
import config from 'stonyx/config';
|
|
20
|
-
import log from 'stonyx/log';
|
|
21
|
-
import { forEachFileImport } from '@stonyx/utils/file';
|
|
22
|
-
|
|
23
|
-
export { default as Request } from './request.js';
|
|
24
|
-
|
|
25
|
-
export default class RestServer {
|
|
26
|
-
constructor() {
|
|
27
|
-
if (RestServer.instance) return RestServer.instance;
|
|
28
|
-
RestServer.instance = this;
|
|
29
|
-
|
|
30
|
-
this.api = new express();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
static close() {
|
|
34
|
-
if (!RestServer.instance) throw new Error('RestServer has not been initialized yet');
|
|
35
|
-
|
|
36
|
-
RestServer.instance.server.close();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async init() {
|
|
40
|
-
await this.setupRouter();
|
|
41
|
-
|
|
42
|
-
const { port } = config.restServer;
|
|
43
|
-
|
|
44
|
-
// start REST server
|
|
45
|
-
this.server = this.api.listen(port);
|
|
46
|
-
log.title(`API Server is listening on port ${port}`);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async setupRouter() {
|
|
50
|
-
const { camelCaseRoutes, dir, enableHealthCheck } = config.restServer;
|
|
51
|
-
this.setupGlobalMiddleware();
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
await forEachFileImport(dir, this.mountRoute.bind(this), { rawName: !camelCaseRoutes, ignoreAccessFailure: true });
|
|
55
|
-
|
|
56
|
-
if (enableHealthCheck) this.api.get('/health', (_req, res) => res.sendStatus(200));
|
|
57
|
-
} catch (error) {
|
|
58
|
-
if (config.debug) console.log(error);
|
|
59
|
-
throw log.error(`Unable to dynamically configure routes from files in ${dir}`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async setupGlobalMiddleware() {
|
|
64
|
-
const { origin, methods } = config.restServer;
|
|
65
|
-
|
|
66
|
-
this.api.use([
|
|
67
|
-
cors({ origin, methods }),
|
|
68
|
-
express.json()
|
|
69
|
-
]);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async mountRoute(routeClass, { name, options }) {
|
|
73
|
-
const { api } = this;
|
|
74
|
-
const classInstance = new routeClass(options);
|
|
75
|
-
const route = name === 'index' ? '/' : `/${name}`;
|
|
76
|
-
const { expressInstance, authorization } = classInstance;
|
|
77
|
-
|
|
78
|
-
const routeCalls = [ expressInstance ];
|
|
79
|
-
|
|
80
|
-
// Assign auth callback if it exists
|
|
81
|
-
if (authorization) routeCalls.unshift(authorization.bind(classInstance));
|
|
82
|
-
|
|
83
|
-
classInstance.registerCalls();
|
|
84
|
-
expressInstance.mountpath = route;
|
|
85
|
-
|
|
86
|
-
// Mount handler to main api instance
|
|
87
|
-
api.use(route, ...routeCalls);
|
|
88
|
-
}
|
|
89
|
-
}
|
package/src/request.js
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
|
-
import config from 'stonyx/config';
|
|
3
|
-
import { makeArray } from '@stonyx/utils/object';
|
|
4
|
-
|
|
5
|
-
const METHODS = new Set(['get', 'post', 'put', 'delete', 'patch']);
|
|
6
|
-
|
|
7
|
-
export default class Request {
|
|
8
|
-
static stateProp = '__stonyxState';
|
|
9
|
-
|
|
10
|
-
static getState(req) {
|
|
11
|
-
const { stateProp } = Request;
|
|
12
|
-
if (req[stateProp] !== undefined) return req[stateProp];
|
|
13
|
-
|
|
14
|
-
req[stateProp] = {};
|
|
15
|
-
return req[stateProp];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
static sendStatusResponse(res, status) {
|
|
19
|
-
const statusMap = config.restServer?.statusMap || {};
|
|
20
|
-
const message = statusMap[status] || '';
|
|
21
|
-
|
|
22
|
-
return message ? res.status(status).send(message) : res.sendStatus(status);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
constructor() {
|
|
26
|
-
const api = express();
|
|
27
|
-
api.disable('x-powered-by');
|
|
28
|
-
|
|
29
|
-
this.expressInstance = api;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// auth hook wrapper
|
|
33
|
-
authorization(req, res, next) {
|
|
34
|
-
if (!this.auth) return next();
|
|
35
|
-
|
|
36
|
-
const status = this.auth(req, Request.getState(req));
|
|
37
|
-
if (status) return Request.sendStatusResponse(res, status);
|
|
38
|
-
|
|
39
|
-
next();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
registerCalls() {
|
|
43
|
-
const { expressInstance } = this;
|
|
44
|
-
const { getState } = Request;
|
|
45
|
-
|
|
46
|
-
for (const [method, handlers] of Object.entries(this.handlers)) {
|
|
47
|
-
if (!METHODS.has(method)) {
|
|
48
|
-
console.warn(`Method "${method}" is not a valid HTTP method. Skipping...`);
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
for (const [route, handler] of Object.entries(handlers)) {
|
|
53
|
-
expressInstance[method](route, async (req, res) => {
|
|
54
|
-
const callStack = [...makeArray(handler)];
|
|
55
|
-
const mainCall = callStack.pop();
|
|
56
|
-
const { sendStatusResponse } = Request;
|
|
57
|
-
let response;
|
|
58
|
-
|
|
59
|
-
// Run middleware
|
|
60
|
-
while(callStack.length) {
|
|
61
|
-
response = await callStack.shift().bind(this)(req, getState(req));
|
|
62
|
-
if (response !== undefined) break;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (response === undefined) response = await mainCall(req, getState(req));
|
|
66
|
-
if (Number.isInteger(response)) return sendStatusResponse(res, response);
|
|
67
|
-
|
|
68
|
-
// Handle pipe if set via call state object
|
|
69
|
-
const { pipe } = getState(req);
|
|
70
|
-
if (pipe) {
|
|
71
|
-
const { headers, source } = pipe;
|
|
72
|
-
|
|
73
|
-
if (headers) for (const [key, value] of Object.entries(headers)) res.set(key, value);
|
|
74
|
-
return source.pipe(res);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (response === undefined) return res.sendStatus(200);
|
|
78
|
-
if (typeof response !== 'object') return sendStatusResponse(res, 500);
|
|
79
|
-
|
|
80
|
-
res.send(response);
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|