@xeplr/base-apis 3.0.0 → 3.0.1

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.
Files changed (3) hide show
  1. package/README.md +20 -2
  2. package/express.js +29 -2
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -82,6 +82,7 @@ createApp(3000, 'myapp', {
82
82
  | `requiredEnv` | variable names; any missing → message and `process.exit(1)` before anything starts |
83
83
  | `auth` | the gate — see below. **On when omitted** |
84
84
  | `middleware` | array, mounted after the gate |
85
+ | `rawBodyPaths` | path prefixes whose body is also kept byte for byte as `req.rawBody` (a `Buffer`) — see below |
85
86
  | `routes` | `{ '/path': router }` |
86
87
  | `log` | `@xeplr/logs` settings (`logDir` `'./logs'`, `fatalThreshold`, `emailThreshold`, `emailTo`, `emailService`), or `false` |
87
88
  | `corsOptions` | passed to `cors` (default: allow all) |
@@ -94,7 +95,7 @@ createApp(3000, 'myapp', {
94
95
  ### Order of the middleware
95
96
 
96
97
  1. request logger (`@xeplr/logs` — sets `req.log`), unless `log: false`
97
- 2. view engine, `cors`, `express.json`, `express.urlencoded`, `cookie-parser`
98
+ 2. view engine, `cors`, `express.json`, `express.urlencoded` (and the raw body, for `rawBodyPaths`), `cookie-parser`
98
99
  3. `staticDir`
99
100
  4. uploaded files served at `upload.serveRoute` — **before the gate, so public**
100
101
  5. **the auth gate**
@@ -104,6 +105,23 @@ createApp(3000, 'myapp', {
104
105
  9. `options.routes`
105
106
  10. 404, then the error handler
106
107
 
108
+ ### Webhooks: the raw body
109
+
110
+ 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:
111
+
112
+ ```js
113
+ createApp(3000, 'myapp', {
114
+ auth: { publicPaths: ['/billing/webhook/'] }, // a provider cannot sign in
115
+ rawBodyPaths: ['/billing/webhook/'], // so it must be checked by signature, over these
116
+ routes
117
+ })
118
+ // in the route: hmac(secret).update(req.rawBody) === req.headers['x-signature']
119
+ ```
120
+
121
+ - JSON and form bodies are still parsed into `req.body`; any other type (`text/plain`, `application/octet-stream`) is read only into `req.rawBody`.
122
+ - Only the paths named: every other request keeps just its parsed body, not a second copy.
123
+ - A prefix, matched like `publicPaths`.
124
+
107
125
  ### The auth gate
108
126
 
109
127
  | `auth` | result |
@@ -283,7 +301,7 @@ broadcast({ type: 'cache-clear' }) // from a worker: relayed to every other wo
283
301
  npm test
284
302
  ```
285
303
 
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.
304
+ 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
305
 
288
306
  ## License
289
307
 
package/express.js CHANGED
@@ -25,6 +25,9 @@ var logs = require('@xeplr/logs');
25
25
  * { middleware } supply your own gate instead of the HTTP one
26
26
  * false this service is public ON PURPOSE
27
27
  * @param {Function[]} [options.middleware] - Middleware to apply before routes (runs AFTER the auth gate)
28
+ * @param {string[]} [options.rawBodyPaths] - Path prefixes whose body is also kept, byte for byte, as
29
+ * req.rawBody (a Buffer) — what a webhook's signature is computed over. JSON and form
30
+ * bodies are still parsed into req.body; any other type is read only as req.rawBody.
28
31
  * @param {object} [options.views] - { engine: 'pug', dir: '/abs/path' }
29
32
  * @param {string} [options.staticDir] - Path to static files directory
30
33
  * @param {string} [options.corsOptions] - Custom CORS options (default: allow all)
@@ -77,8 +80,32 @@ function createApp(port, appName, options) {
77
80
 
78
81
  // ── Standard middleware ──
79
82
  app.use(cors(options.corsOptions));
80
- app.use(express.json());
81
- app.use(express.urlencoded({ extended: false }));
83
+ // A webhook signs the bytes it sent, and req.body is those bytes parsed:
84
+ // JSON.stringify(req.body) is not them (spacing, key order, number
85
+ // formatting). So a rawBodyPaths route also gets the bytes, as req.rawBody.
86
+ // Only for the paths named — every other request keeps just its parsed body.
87
+ var rawBodyPaths = options.rawBodyPaths || [];
88
+ function keepsRaw(req) {
89
+ for (var i = 0; i < rawBodyPaths.length; i++) {
90
+ if (req.path === rawBodyPaths[i] || req.path.indexOf(rawBodyPaths[i]) === 0) return true;
91
+ }
92
+ return false;
93
+ }
94
+ function keepRaw(req, res, buf) { if (keepsRaw(req)) req.rawBody = buf; }
95
+ var verify = rawBodyPaths.length ? keepRaw : undefined;
96
+ app.use(express.json({ verify: verify }));
97
+ app.use(express.urlencoded({ extended: false, verify: verify }));
98
+ if (rawBodyPaths.length) {
99
+ // A type neither parser reads (text/plain, application/octet-stream…).
100
+ var rawParser = express.raw({ type: function () { return true; } });
101
+ app.use(function (req, res, next) {
102
+ if (req.rawBody || !keepsRaw(req)) return next();
103
+ rawParser(req, res, function (err) {
104
+ if (!err && Buffer.isBuffer(req.body)) req.rawBody = req.body;
105
+ next(err);
106
+ });
107
+ });
108
+ }
82
109
  app.use(cookieParser());
83
110
 
84
111
  // ── Static files ──
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.1",
4
4
  "description": "Base API bootstrapper — Express and plain HTTP server factories with middleware injection",
5
5
  "main": "index.js",
6
6
  "bin": {