@nmbrco/server 0.0.0-stage → 0.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/CHANGELOG.md +7 -0
- package/LICENSE.md +12 -0
- package/README.md +74 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +113 -0
- package/dist/index.js.map +1 -0
- package/dist/sign-request.d.ts +8 -0
- package/dist/sign.d.ts +10 -0
- package/dist/token.d.ts +3 -0
- package/package.json +52 -4
package/CHANGELOG.md
ADDED
package/LICENSE.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Copyright 2026, The Nmbr Company Ltd.
|
|
2
|
+
|
|
3
|
+
This package is licensed for use by Nmbr Partners or employees for the express
|
|
4
|
+
purpose of integrating Nmbr Components with their application(s) and the Nmbr API.
|
|
5
|
+
|
|
6
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
7
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
8
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
9
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
10
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
11
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
12
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,75 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nmbrco/server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Server middleware for [Nmbr Components](https://docs.nmbr.co/components/getting-started-v2).
|
|
4
|
+
|
|
5
|
+
Every request the Component makes to the Nmbr API carries a JSON Web Token signed with your API secret. `sign` is the endpoint that creates those tokens.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @nmbrco/server
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Node 22 or later.
|
|
14
|
+
|
|
15
|
+
## Sign
|
|
16
|
+
|
|
17
|
+
Mount `sign` behind your own authentication, then pass its path as `signUrl` when you initialize the Component.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import express from 'express';
|
|
21
|
+
import type { Request } from 'express';
|
|
22
|
+
import { sign } from '@nmbrco/server';
|
|
23
|
+
|
|
24
|
+
const app = express();
|
|
25
|
+
|
|
26
|
+
app.post(
|
|
27
|
+
'/sign_nmbr_request',
|
|
28
|
+
requireSession, // your auth
|
|
29
|
+
sign({
|
|
30
|
+
apiSecret: process.env.NMBR_API_SECRET!,
|
|
31
|
+
partnerId: process.env.NMBR_PARTNER_ID!,
|
|
32
|
+
// Return true only if the signed-in user can have this scope for this company.
|
|
33
|
+
authorize: (req: Request, { companyId, scope }) =>
|
|
34
|
+
scope === 'admin:*' && req.user.payrollAdminCompanyIds.includes(companyId),
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Why `authorize` is required
|
|
40
|
+
|
|
41
|
+
The Nmbr API checks that the company in a token belongs to your partner account. It cannot know which of your users may act on which of your companies. Without `authorize`, any signed-in user of your app could get a token for any company under your partner account.
|
|
42
|
+
|
|
43
|
+
`authorize` also gets the `scope` that the Component requests. Today the only scope is `admin:*`, which gives full payroll access to the company. Check the scope as well as the company.
|
|
44
|
+
|
|
45
|
+
Rate-limit the sign route. The Component calls it once for each API request, and each call runs `authorize`.
|
|
46
|
+
|
|
47
|
+
### What `sign` sets
|
|
48
|
+
|
|
49
|
+
The Component sends the claims for each API request. `sign` keeps only the company, scope, HTTP method, path and body hash from that message. It sets `iss` and `exp` itself:
|
|
50
|
+
|
|
51
|
+
| Claim | Value |
|
|
52
|
+
| -------- | ---------------------------------------- |
|
|
53
|
+
| `iss` | `partnerId` |
|
|
54
|
+
| `sub` | the company, after `authorize` allows it |
|
|
55
|
+
| `scope` | from the Component, after `authorize` allows it |
|
|
56
|
+
| `exp` | 5 minutes from now |
|
|
57
|
+
| `method` | from the Component |
|
|
58
|
+
| `uri` | from the Component (a path only) |
|
|
59
|
+
| `hash` | from the Component, when present |
|
|
60
|
+
|
|
61
|
+
### Responses
|
|
62
|
+
|
|
63
|
+
| Status | When |
|
|
64
|
+
| ------ | -------------------------------------------------------- |
|
|
65
|
+
| `201` | The token, as `text/plain` |
|
|
66
|
+
| `400` | The body is not valid JSON, or has invalid claims |
|
|
67
|
+
| `403` | `authorize` returned `false` |
|
|
68
|
+
| `413` | The body is larger than 8 KiB. `sign` stops reading it and closes the connection. |
|
|
69
|
+
| `405` | The request is not a `POST` |
|
|
70
|
+
|
|
71
|
+
If `authorize` throws, `sign` passes the error to `next` (Express), or responds `500` when there is no `next`.
|
|
72
|
+
|
|
73
|
+
### Other frameworks
|
|
74
|
+
|
|
75
|
+
`sign` takes a Node `IncomingMessage` and `ServerResponse`, so it also works with `node:http` and Connect-style servers. It reads the request body itself, or uses `req.body` when a body parser already ran.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { SignJWT } from "jose";
|
|
4
|
+
//#region src/sign-request.ts
|
|
5
|
+
var METHODS = [
|
|
6
|
+
"GET",
|
|
7
|
+
"POST",
|
|
8
|
+
"PUT",
|
|
9
|
+
"PATCH",
|
|
10
|
+
"DELETE"
|
|
11
|
+
];
|
|
12
|
+
var isNonEmptyString = (value) => typeof value === "string" && value.length > 0;
|
|
13
|
+
var parseSignRequest = (body) => {
|
|
14
|
+
if (typeof body !== "object" || body === null) return void 0;
|
|
15
|
+
const { sub, method, uri, scope, hash } = body;
|
|
16
|
+
if (!isNonEmptyString(sub) || !isNonEmptyString(method) || !METHODS.includes(method) || !isNonEmptyString(uri) || !uri.startsWith("/") || uri.startsWith("//") || !isNonEmptyString(scope) || hash !== void 0 && typeof hash !== "string") return;
|
|
17
|
+
return {
|
|
18
|
+
companyId: sub,
|
|
19
|
+
method,
|
|
20
|
+
uri,
|
|
21
|
+
scope,
|
|
22
|
+
...hash && { hash }
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/token.ts
|
|
27
|
+
var TOKEN_TTL = "5m";
|
|
28
|
+
var createSigningKey = (apiSecret) => new TextEncoder().encode(createHash("sha256").update(apiSecret).digest("hex"));
|
|
29
|
+
var signToken = (partnerId, { companyId, method, uri, scope, hash }, key) => new SignJWT({
|
|
30
|
+
uri,
|
|
31
|
+
method,
|
|
32
|
+
scope,
|
|
33
|
+
...hash && { hash }
|
|
34
|
+
}).setProtectedHeader({
|
|
35
|
+
alg: "HS256",
|
|
36
|
+
typ: "JWT"
|
|
37
|
+
}).setIssuer(partnerId).setSubject(companyId).setExpirationTime(TOKEN_TTL).sign(key);
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/sign.ts
|
|
40
|
+
var MAX_BODY_BYTES = 8192;
|
|
41
|
+
var LINGER_MS = 1e3;
|
|
42
|
+
var TOO_LARGE = Symbol("TOO_LARGE");
|
|
43
|
+
var parseJson = (text) => {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(text);
|
|
46
|
+
} catch {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var readText = (req) => new Promise((resolve, reject) => {
|
|
51
|
+
const chunks = [];
|
|
52
|
+
let size = 0;
|
|
53
|
+
const onData = (chunk) => {
|
|
54
|
+
size += chunk.length;
|
|
55
|
+
if (size > MAX_BODY_BYTES) {
|
|
56
|
+
req.off("data", onData);
|
|
57
|
+
req.pause();
|
|
58
|
+
resolve(TOO_LARGE);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
chunks.push(chunk);
|
|
62
|
+
};
|
|
63
|
+
req.on("data", onData);
|
|
64
|
+
req.once("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
65
|
+
req.once("error", reject);
|
|
66
|
+
});
|
|
67
|
+
var isSkippedBody = (req) => !req.readableEnded && typeof req.body === "object" && req.body !== null && Object.keys(req.body).length === 0;
|
|
68
|
+
var readBody = async (req) => {
|
|
69
|
+
if (typeof req.body === "string") return parseJson(req.body);
|
|
70
|
+
if (Buffer.isBuffer(req.body)) return parseJson(req.body.toString("utf8"));
|
|
71
|
+
if (req.body !== void 0 && !isSkippedBody(req)) return req.body;
|
|
72
|
+
if (Number(req.headers["content-length"]) > MAX_BODY_BYTES) return TOO_LARGE;
|
|
73
|
+
const text = await readText(req);
|
|
74
|
+
return text === TOO_LARGE ? TOO_LARGE : parseJson(text);
|
|
75
|
+
};
|
|
76
|
+
var send = (res, status, body) => {
|
|
77
|
+
res.statusCode = status;
|
|
78
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
79
|
+
res.setHeader("Cache-Control", "no-store");
|
|
80
|
+
res.end(body);
|
|
81
|
+
};
|
|
82
|
+
var sign = ({ apiSecret, partnerId, authorize }) => {
|
|
83
|
+
if (!apiSecret) throw new Error("@nmbrco/server: `apiSecret` is required");
|
|
84
|
+
if (!partnerId) throw new Error("@nmbrco/server: `partnerId` is required");
|
|
85
|
+
if (typeof authorize !== "function") throw new Error("@nmbrco/server: `authorize` is required");
|
|
86
|
+
const key = createSigningKey(apiSecret);
|
|
87
|
+
return async (req, res, next) => {
|
|
88
|
+
try {
|
|
89
|
+
if (req.method !== "POST") {
|
|
90
|
+
res.setHeader("Allow", "POST");
|
|
91
|
+
return send(res, 405, "Method Not Allowed");
|
|
92
|
+
}
|
|
93
|
+
const body = await readBody(req);
|
|
94
|
+
if (body === TOO_LARGE) {
|
|
95
|
+
res.once("finish", () => {
|
|
96
|
+
setTimeout(() => req.destroy(), LINGER_MS).unref();
|
|
97
|
+
});
|
|
98
|
+
return send(res, 413, "Payload Too Large");
|
|
99
|
+
}
|
|
100
|
+
const request = parseSignRequest(body);
|
|
101
|
+
if (!request) return send(res, 400, "Invalid signing request");
|
|
102
|
+
if (!await authorize(req, request)) return send(res, 403, "Forbidden");
|
|
103
|
+
send(res, 201, await signToken(partnerId, request, key));
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (next) return next(error);
|
|
106
|
+
send(res, 500, "Internal Server Error");
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
//#endregion
|
|
111
|
+
export { sign };
|
|
112
|
+
|
|
113
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/sign-request.ts","../src/token.ts","../src/sign.ts"],"sourcesContent":["const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];\n\nexport type SignRequest = {\n companyId: string;\n method: string;\n uri: string;\n scope: string;\n hash?: string;\n};\n\nconst isNonEmptyString = (value: unknown): value is string =>\n typeof value === 'string' && value.length > 0;\n\n// The browser can choose only these claims. The server sets `iss` and `exp`.\n// `authorize` decides if the signed-in user can have the requested `scope`.\nexport const parseSignRequest = (body: unknown): SignRequest | undefined => {\n if (typeof body !== 'object' || body === null) return undefined;\n\n const { sub, method, uri, scope, hash } = body as Record<string, unknown>;\n\n if (\n !isNonEmptyString(sub) ||\n !isNonEmptyString(method) ||\n !METHODS.includes(method) ||\n !isNonEmptyString(uri) ||\n !uri.startsWith('/') ||\n uri.startsWith('//') ||\n !isNonEmptyString(scope) ||\n (hash !== undefined && typeof hash !== 'string')\n ) {\n return undefined;\n }\n\n return { companyId: sub, method, uri, scope, ...(hash && { hash }) };\n};\n","import type { SignRequest } from './sign-request.js';\n\nimport { createHash } from 'node:crypto';\n\nimport { SignJWT } from 'jose';\n\nconst TOKEN_TTL = '5m';\n\n// The API stores the SHA-256 hex digest of the API secret, not the secret.\n// The API uses that digest as the HS256 key.\nexport const createSigningKey = (apiSecret: string) =>\n new TextEncoder().encode(\n createHash('sha256').update(apiSecret).digest('hex')\n );\n\nexport const signToken = (\n partnerId: string,\n { companyId, method, uri, scope, hash }: SignRequest,\n key: Uint8Array\n) =>\n new SignJWT({\n uri,\n method,\n scope,\n ...(hash && { hash }),\n })\n .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })\n .setIssuer(partnerId)\n .setSubject(companyId)\n .setExpirationTime(TOKEN_TTL)\n .sign(key);\n","import type { SignRequest } from './sign-request.js';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\n\nimport { Buffer } from 'node:buffer';\n\nimport { parseSignRequest } from './sign-request.js';\nimport { createSigningKey, signToken } from './token.js';\n\nexport type SignOptions<Req extends IncomingMessage = IncomingMessage> = {\n apiSecret: string;\n partnerId: string;\n // The API checks that the company belongs to the partner.\n // It does not check that the signed-in user can access the company.\n authorize: (req: Req, request: SignRequest) => boolean | Promise<boolean>;\n};\n\nconst MAX_BODY_BYTES = 8 * 1024;\n\nconst LINGER_MS = 1000;\n\nconst TOO_LARGE = Symbol('TOO_LARGE');\n\nconst parseJson = (text: string): unknown => {\n try {\n return JSON.parse(text);\n } catch {\n return undefined;\n }\n};\n\n// Pause the stream at the limit. A destroyed stream also closes the socket,\n// and then the client gets no 413 response.\nconst readText = (req: IncomingMessage) =>\n new Promise<string | typeof TOO_LARGE>((resolve, reject) => {\n const chunks: Array<Buffer> = [];\n let size = 0;\n\n const onData = (chunk: Buffer) => {\n size += chunk.length;\n\n if (size > MAX_BODY_BYTES) {\n req.off('data', onData);\n req.pause();\n resolve(TOO_LARGE);\n return;\n }\n\n chunks.push(chunk);\n };\n\n req.on('data', onData);\n req.once('end', () => resolve(Buffer.concat(chunks).toString('utf8')));\n req.once('error', reject);\n });\n\n// Express 4 body parsers set `req.body` to `{}` when they skip a request.\n// Then the stream is not read yet, and `readBody` must read it.\nconst isSkippedBody = (req: IncomingMessage & { body?: unknown }) =>\n !req.readableEnded &&\n typeof req.body === 'object' &&\n req.body !== null &&\n Object.keys(req.body).length === 0;\n\n// Use `req.body` when a parser such as `express.json()` read the stream before.\nconst readBody = async (req: IncomingMessage & { body?: unknown }) => {\n if (typeof req.body === 'string') return parseJson(req.body);\n if (Buffer.isBuffer(req.body)) return parseJson(req.body.toString('utf8'));\n if (req.body !== undefined && !isSkippedBody(req)) return req.body;\n\n if (Number(req.headers['content-length']) > MAX_BODY_BYTES) return TOO_LARGE;\n\n const text = await readText(req);\n\n return text === TOO_LARGE ? TOO_LARGE : parseJson(text);\n};\n\nconst send = (res: ServerResponse, status: number, body: string) => {\n res.statusCode = status;\n res.setHeader('Content-Type', 'text/plain; charset=utf-8');\n res.setHeader('Cache-Control', 'no-store');\n res.end(body);\n};\n\nexport const sign = <Req extends IncomingMessage = IncomingMessage>({\n apiSecret,\n partnerId,\n authorize,\n}: SignOptions<Req>) => {\n if (!apiSecret) throw new Error('@nmbrco/server: `apiSecret` is required');\n if (!partnerId) throw new Error('@nmbrco/server: `partnerId` is required');\n if (typeof authorize !== 'function') {\n throw new Error('@nmbrco/server: `authorize` is required');\n }\n\n const key = createSigningKey(apiSecret);\n\n return async (\n req: Req & { body?: unknown },\n res: ServerResponse,\n next?: (error?: unknown) => void\n ) => {\n try {\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST');\n return send(res, 405, 'Method Not Allowed');\n }\n\n const body = await readBody(req);\n\n // Close the connection a short time after the 413, so the client cannot\n // send the rest of the body. Node discards the data until then. Do not\n // close at once: a close with unread data resets the connection, and then\n // the client loses the 413 response.\n if (body === TOO_LARGE) {\n res.once('finish', () => {\n setTimeout(() => req.destroy(), LINGER_MS).unref();\n });\n return send(res, 413, 'Payload Too Large');\n }\n\n const request = parseSignRequest(body);\n\n if (!request) return send(res, 400, 'Invalid signing request');\n\n if (!(await authorize(req, request))) {\n return send(res, 403, 'Forbidden');\n }\n\n send(res, 201, await signToken(partnerId, request, key));\n } catch (error) {\n if (next) return next(error);\n\n send(res, 500, 'Internal Server Error');\n }\n };\n};\n"],"mappings":"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"}
|
package/dist/sign.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SignRequest } from './sign-request.js';
|
|
2
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
|
+
export type SignOptions<Req extends IncomingMessage = IncomingMessage> = {
|
|
4
|
+
apiSecret: string;
|
|
5
|
+
partnerId: string;
|
|
6
|
+
authorize: (req: Req, request: SignRequest) => boolean | Promise<boolean>;
|
|
7
|
+
};
|
|
8
|
+
export declare const sign: <Req extends IncomingMessage = IncomingMessage>({ apiSecret, partnerId, authorize, }: SignOptions<Req>) => (req: Req & {
|
|
9
|
+
body?: unknown;
|
|
10
|
+
}, res: ServerResponse, next?: (error?: unknown) => void) => Promise<void>;
|
package/dist/token.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { SignRequest } from './sign-request.js';
|
|
2
|
+
export declare const createSigningKey: (apiSecret: string) => NodeJS.NonSharedUint8Array;
|
|
3
|
+
export declare const signToken: (partnerId: string, { companyId, method, uri, scope, hash }: SignRequest, key: Uint8Array) => Promise<string>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nmbrco/server",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Server middleware that signs Nmbr Components requests",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "The Nmbr Company Ltd. (https://nmbr.co)",
|
|
7
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
8
|
+
"homepage": "https://docs.nmbr.co/components/getting-started-v2",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/nmbrco/components.git",
|
|
12
|
+
"directory": "packages/server"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "vite build",
|
|
16
|
+
"type-check": "tsc --noEmit",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
19
|
+
"lint": "eslint . --max-warnings 0 --no-warn-ignored",
|
|
20
|
+
"lint:fix": "eslint . --fix --max-warnings 0 --no-warn-ignored",
|
|
21
|
+
"format": "prettier --write . --ignore-path=../../.prettierignore",
|
|
22
|
+
"format:check": "prettier --check . --ignore-path=../../.prettierignore"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"CHANGELOG.md"
|
|
27
|
+
],
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=22"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"jose": "^6.2.12"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/express": "^5.0.6",
|
|
46
|
+
"@types/node": "26.5.1",
|
|
47
|
+
"@typescript/typescript6": "^6.0.2",
|
|
48
|
+
"express": "^5.2.1",
|
|
49
|
+
"typescript": "^7.0.2",
|
|
50
|
+
"unplugin-dts": "^1.1.1",
|
|
51
|
+
"vite": "^8.3.0",
|
|
52
|
+
"vitest": "^5.0.0"
|
|
53
|
+
}
|
|
54
|
+
}
|