@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.
- package/README.md +20 -2
- package/express.js +29 -2
- 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
|
|
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
|
-
|
|
81
|
-
|
|
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 ──
|