@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 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`, `cookie-parser`
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
- app.use(express.json());
81
- app.use(express.urlencoded({ extended: false }));
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
- server.listen(normalizedPort);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xeplr/base-apis",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
4
  "description": "Base API bootstrapper — Express and plain HTTP server factories with middleware injection",
5
5
  "main": "index.js",
6
6
  "bin": {