@xeplr/base-apis 3.0.0 → 3.0.2
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 +21 -2
- package/express.js +32 -3
- package/lib/server.js +4 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,8 +80,10 @@ createApp(3000, 'myapp', {
|
|
|
80
80
|
| option | |
|
|
81
81
|
|---|---|
|
|
82
82
|
| `requiredEnv` | variable names; any missing → message and `process.exit(1)` before anything starts |
|
|
83
|
+
| `host` | the address to listen on, e.g. `'127.0.0.1'` for a service that only nginx on the same machine may reach. Default: every address |
|
|
83
84
|
| `auth` | the gate — see below. **On when omitted** |
|
|
84
85
|
| `middleware` | array, mounted after the gate |
|
|
86
|
+
| `rawBodyPaths` | path prefixes whose body is also kept byte for byte as `req.rawBody` (a `Buffer`) — see below |
|
|
85
87
|
| `routes` | `{ '/path': router }` |
|
|
86
88
|
| `log` | `@xeplr/logs` settings (`logDir` `'./logs'`, `fatalThreshold`, `emailThreshold`, `emailTo`, `emailService`), or `false` |
|
|
87
89
|
| `corsOptions` | passed to `cors` (default: allow all) |
|
|
@@ -94,7 +96,7 @@ createApp(3000, 'myapp', {
|
|
|
94
96
|
### Order of the middleware
|
|
95
97
|
|
|
96
98
|
1. request logger (`@xeplr/logs` — sets `req.log`), unless `log: false`
|
|
97
|
-
2. view engine, `cors`, `express.json`, `express.urlencoded
|
|
99
|
+
2. view engine, `cors`, `express.json`, `express.urlencoded` (and the raw body, for `rawBodyPaths`), `cookie-parser`
|
|
98
100
|
3. `staticDir`
|
|
99
101
|
4. uploaded files served at `upload.serveRoute` — **before the gate, so public**
|
|
100
102
|
5. **the auth gate**
|
|
@@ -104,6 +106,23 @@ createApp(3000, 'myapp', {
|
|
|
104
106
|
9. `options.routes`
|
|
105
107
|
10. 404, then the error handler
|
|
106
108
|
|
|
109
|
+
### Webhooks: the raw body
|
|
110
|
+
|
|
111
|
+
A payment provider (Stripe, Razorpay…) signs **the bytes it sent**, and `req.body` is those bytes parsed. `JSON.stringify(req.body)` is not them — spacing, key order and `1.50` vs `1.5` all change — so a signature checked against it fails, or worse, gets "fixed" by skipping the check. Name the webhook paths and they also get the bytes:
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
createApp(3000, 'myapp', {
|
|
115
|
+
auth: { publicPaths: ['/billing/webhook/'] }, // a provider cannot sign in
|
|
116
|
+
rawBodyPaths: ['/billing/webhook/'], // so it must be checked by signature, over these
|
|
117
|
+
routes
|
|
118
|
+
})
|
|
119
|
+
// in the route: hmac(secret).update(req.rawBody) === req.headers['x-signature']
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- JSON and form bodies are still parsed into `req.body`; any other type (`text/plain`, `application/octet-stream`) is read only into `req.rawBody`.
|
|
123
|
+
- Only the paths named: every other request keeps just its parsed body, not a second copy.
|
|
124
|
+
- A prefix, matched like `publicPaths`.
|
|
125
|
+
|
|
107
126
|
### The auth gate
|
|
108
127
|
|
|
109
128
|
| `auth` | result |
|
|
@@ -283,7 +302,7 @@ broadcast({ type: 'cache-clear' }) // from a worker: relayed to every other wo
|
|
|
283
302
|
npm test
|
|
284
303
|
```
|
|
285
304
|
|
|
286
|
-
The auth gate against a stand-in auth service (refusal to boot, 401 / 503, public paths, caching, `X-New-Token`), SSE publish and subscribe, and ticket-authorised SSE. `tenant-gate.test.js`: the gate naming the company and workspace, refusing a non-member before the app sees the request, refusing an auth that did not check, caching per company; a real multipart upload landing in the company's own `uploads/` and served back only to it; `createApp`'s uploads no longer public; `sendTenantFile` refusing another company's name, a path out and no tenant.
|
|
305
|
+
The auth gate against a stand-in auth service (refusal to boot, 401 / 503, public paths, caching, `X-New-Token`), SSE publish and subscribe, and ticket-authorised SSE. `tenant-gate.test.js`: the gate naming the company and workspace, refusing a non-member before the app sees the request, refusing an auth that did not check, caching per company; a real multipart upload landing in the company's own `uploads/` and served back only to it; `createApp`'s uploads no longer public; `sendTenantFile` refusing another company's name, a path out and no tenant. `raw-body.test.js`: a `rawBodyPaths` route gets the exact bytes a provider signed (JSON, form and other types), and every other path does not.
|
|
287
306
|
|
|
288
307
|
## License
|
|
289
308
|
|
package/express.js
CHANGED
|
@@ -14,6 +14,8 @@ var logs = require('@xeplr/logs');
|
|
|
14
14
|
* @param {string} appName - Service name (used for debug namespace and console logs)
|
|
15
15
|
* @param {object} [options] - Configuration options
|
|
16
16
|
* @param {string[]} [options.requiredEnv] - Mandatory env var names; missing ones abort startup
|
|
17
|
+
* @param {string} [options.host] - The address to listen on, e.g. '127.0.0.1' for a service only a
|
|
18
|
+
* proxy on the same machine may reach. Default: every address (Node's own default).
|
|
17
19
|
* @param {object} [options.routes] - Route map: { '/path': router }
|
|
18
20
|
* @param {object|false} [options.auth] - THE GATE, ON BY DEFAULT.
|
|
19
21
|
* Omit it and every route requires a valid token, validated by asking the
|
|
@@ -25,6 +27,9 @@ var logs = require('@xeplr/logs');
|
|
|
25
27
|
* { middleware } supply your own gate instead of the HTTP one
|
|
26
28
|
* false this service is public ON PURPOSE
|
|
27
29
|
* @param {Function[]} [options.middleware] - Middleware to apply before routes (runs AFTER the auth gate)
|
|
30
|
+
* @param {string[]} [options.rawBodyPaths] - Path prefixes whose body is also kept, byte for byte, as
|
|
31
|
+
* req.rawBody (a Buffer) — what a webhook's signature is computed over. JSON and form
|
|
32
|
+
* bodies are still parsed into req.body; any other type is read only as req.rawBody.
|
|
28
33
|
* @param {object} [options.views] - { engine: 'pug', dir: '/abs/path' }
|
|
29
34
|
* @param {string} [options.staticDir] - Path to static files directory
|
|
30
35
|
* @param {string} [options.corsOptions] - Custom CORS options (default: allow all)
|
|
@@ -77,8 +82,32 @@ function createApp(port, appName, options) {
|
|
|
77
82
|
|
|
78
83
|
// ── Standard middleware ──
|
|
79
84
|
app.use(cors(options.corsOptions));
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
// A webhook signs the bytes it sent, and req.body is those bytes parsed:
|
|
86
|
+
// JSON.stringify(req.body) is not them (spacing, key order, number
|
|
87
|
+
// formatting). So a rawBodyPaths route also gets the bytes, as req.rawBody.
|
|
88
|
+
// Only for the paths named — every other request keeps just its parsed body.
|
|
89
|
+
var rawBodyPaths = options.rawBodyPaths || [];
|
|
90
|
+
function keepsRaw(req) {
|
|
91
|
+
for (var i = 0; i < rawBodyPaths.length; i++) {
|
|
92
|
+
if (req.path === rawBodyPaths[i] || req.path.indexOf(rawBodyPaths[i]) === 0) return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
function keepRaw(req, res, buf) { if (keepsRaw(req)) req.rawBody = buf; }
|
|
97
|
+
var verify = rawBodyPaths.length ? keepRaw : undefined;
|
|
98
|
+
app.use(express.json({ verify: verify }));
|
|
99
|
+
app.use(express.urlencoded({ extended: false, verify: verify }));
|
|
100
|
+
if (rawBodyPaths.length) {
|
|
101
|
+
// A type neither parser reads (text/plain, application/octet-stream…).
|
|
102
|
+
var rawParser = express.raw({ type: function () { return true; } });
|
|
103
|
+
app.use(function (req, res, next) {
|
|
104
|
+
if (req.rawBody || !keepsRaw(req)) return next();
|
|
105
|
+
rawParser(req, res, function (err) {
|
|
106
|
+
if (!err && Buffer.isBuffer(req.body)) req.rawBody = req.body;
|
|
107
|
+
next(err);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
82
111
|
app.use(cookieParser());
|
|
83
112
|
|
|
84
113
|
// ── Static files ──
|
|
@@ -224,7 +253,7 @@ function createApp(port, appName, options) {
|
|
|
224
253
|
|
|
225
254
|
// ── Start HTTP server ──
|
|
226
255
|
app.set('port', port);
|
|
227
|
-
var server = createServer(app, port, appName);
|
|
256
|
+
var server = createServer(app, port, appName, options.host);
|
|
228
257
|
|
|
229
258
|
return { app: app, server: server };
|
|
230
259
|
}
|
package/lib/server.js
CHANGED
|
@@ -7,12 +7,14 @@ function normalizePort(val) {
|
|
|
7
7
|
return false;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
function createServer(handler, port, name) {
|
|
10
|
+
function createServer(handler, port, name, host) {
|
|
11
11
|
const dbg = require('debug')('architects:' + name);
|
|
12
12
|
const normalizedPort = normalizePort(port);
|
|
13
13
|
|
|
14
14
|
const server = http.createServer(handler);
|
|
15
|
-
|
|
15
|
+
// A host only for a port: a named pipe has none.
|
|
16
|
+
if (host && typeof normalizedPort === 'number') server.listen(normalizedPort, host);
|
|
17
|
+
else server.listen(normalizedPort);
|
|
16
18
|
|
|
17
19
|
server.on('error', function onError(error) {
|
|
18
20
|
if (error.syscall !== 'listen') throw error;
|