@nanolink/signing-client 0.1.0

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 ADDED
@@ -0,0 +1,43 @@
1
+ # @nanolink/signing-client
2
+
3
+ TypeScript npm package for interacting with a JWT signing service.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @nanolink/signing-client
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { SigningClient } from "@nanolink/signing-client";
15
+
16
+ const client = new SigningClient({ baseUrl: "http://localhost:8080" });
17
+
18
+ const health = await client.healthz();
19
+ const signed = await client.sign({
20
+ sub: "device-42",
21
+ kind: "access",
22
+ aud: ["api.example.com"],
23
+ ttl_seconds: 3600,
24
+ claims: { tid: "tenant-acme", ver: 3 }
25
+ });
26
+ const jwks = await client.getJwks();
27
+ const publicKeyPem = await client.getPublicKeyPem();
28
+ ```
29
+
30
+ ## API
31
+
32
+ - `healthz()` -> `{ status: "ok" }`
33
+ - `sign(request)` -> `{ token: string }`
34
+ - `getJwks()` -> `{ keys: Jwk[] }`
35
+ - `getPublicKeyPem(kid?)` -> `string` (SPKI PEM public key)
36
+
37
+ ## Error handling
38
+
39
+ All non-2xx responses throw `SigningClientError` with:
40
+
41
+ - `status`: HTTP status code
42
+ - `message`: error description from service or fallback text
43
+ - `body`: parsed JSON or raw response body (if available)
@@ -0,0 +1,2 @@
1
+ export * from "./signingClient";
2
+ export * from "./types";
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./signingClient"), exports);
18
+ __exportStar(require("./types"), exports);
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,kDAAgC;AAChC,0CAAwB"}
@@ -0,0 +1,20 @@
1
+ import { type HealthzResponse, type JwksResponse, type SignRequest, type SignResponse, type SigningClientOptions } from "./types";
2
+ export declare class SigningClientError extends Error {
3
+ readonly status?: number;
4
+ readonly body?: unknown;
5
+ readonly cause?: unknown;
6
+ constructor(message: string, options?: {
7
+ status?: number;
8
+ body?: unknown;
9
+ cause?: unknown;
10
+ });
11
+ }
12
+ export declare class SigningClient {
13
+ private readonly http;
14
+ constructor(options?: SigningClientOptions);
15
+ healthz(): Promise<HealthzResponse>;
16
+ sign(request: SignRequest): Promise<SignResponse>;
17
+ getJwks(): Promise<JwksResponse>;
18
+ getPublicKeyPem(kid?: string): Promise<string>;
19
+ private request;
20
+ }
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SigningClient = exports.SigningClientError = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const node_crypto_1 = require("node:crypto");
9
+ const VALID_KINDS = ["access", "service", "password_reset", "2fa"];
10
+ const RESERVED_CLAIM_NAMES = new Set(["iss", "sub", "aud", "iat", "exp", "kind"]);
11
+ class SigningClientError extends Error {
12
+ constructor(message, options) {
13
+ super(message);
14
+ this.name = "SigningClientError";
15
+ this.status = options?.status;
16
+ this.body = options?.body;
17
+ this.cause = options?.cause;
18
+ }
19
+ }
20
+ exports.SigningClientError = SigningClientError;
21
+ class SigningClient {
22
+ constructor(options = {}) {
23
+ this.http = options.axios ?? axios_1.default.create({
24
+ baseURL: normalizeBaseUrl(options.baseUrl ?? "http://localhost:8080"),
25
+ headers: {
26
+ "content-type": "application/json",
27
+ ...(options.headers ?? {}),
28
+ },
29
+ });
30
+ }
31
+ async healthz() {
32
+ return this.request("/healthz", {
33
+ method: "GET",
34
+ });
35
+ }
36
+ async sign(request) {
37
+ validateSignRequest(request);
38
+ return this.request("/sign", {
39
+ method: "POST",
40
+ data: request,
41
+ });
42
+ }
43
+ async getJwks() {
44
+ return this.request("/.well-known/jwks.json", {
45
+ method: "GET",
46
+ });
47
+ }
48
+ async getPublicKeyPem(kid) {
49
+ const jwks = await this.getJwks();
50
+ const selectedKey = selectJwk(jwks, kid);
51
+ return jwkToPem(selectedKey);
52
+ }
53
+ async request(path, config) {
54
+ try {
55
+ const response = await this.http.request({
56
+ url: path,
57
+ ...config,
58
+ });
59
+ return response.data;
60
+ }
61
+ catch (error) {
62
+ throw mapRequestError(error);
63
+ }
64
+ }
65
+ }
66
+ exports.SigningClient = SigningClient;
67
+ function normalizeBaseUrl(baseUrl) {
68
+ return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
69
+ }
70
+ function extractErrorMessage(body) {
71
+ if (!body || typeof body !== "object") {
72
+ return undefined;
73
+ }
74
+ const maybeError = body.error;
75
+ if (typeof maybeError === "string" && maybeError.length > 0) {
76
+ return maybeError;
77
+ }
78
+ return undefined;
79
+ }
80
+ function mapRequestError(error) {
81
+ if (isAxiosError(error)) {
82
+ const responseBody = error.response?.data;
83
+ const status = error.response?.status;
84
+ const message = extractErrorMessage(responseBody) ?? error.message ?? "Request failed";
85
+ return new SigningClientError(message, {
86
+ status,
87
+ body: responseBody,
88
+ cause: error,
89
+ });
90
+ }
91
+ return new SigningClientError("Request failed", { cause: error });
92
+ }
93
+ function isAxiosError(error) {
94
+ return !!error && typeof error === "object" && "isAxiosError" in error;
95
+ }
96
+ function validateSignRequest(request) {
97
+ if (!request || typeof request !== "object") {
98
+ throw new SigningClientError("Sign request must be an object");
99
+ }
100
+ if (!request.sub || typeof request.sub !== "string" || request.sub.trim().length === 0) {
101
+ throw new SigningClientError("'sub' is required and must be a non-empty string");
102
+ }
103
+ if (!VALID_KINDS.includes(request.kind)) {
104
+ throw new SigningClientError(`'kind' must be one of: ${VALID_KINDS.map((kind) => `"${kind}"`).join(", ")}`);
105
+ }
106
+ if (request.aud !== undefined) {
107
+ if (!Array.isArray(request.aud) || request.aud.some((item) => typeof item !== "string")) {
108
+ throw new SigningClientError("'aud' must be an array of strings when provided");
109
+ }
110
+ }
111
+ if (request.ttl_seconds !== undefined) {
112
+ if (!Number.isInteger(request.ttl_seconds) || request.ttl_seconds <= 0) {
113
+ throw new SigningClientError("'ttl_seconds' must be a positive integer when provided");
114
+ }
115
+ }
116
+ if (request.claims !== undefined) {
117
+ if (!request.claims || typeof request.claims !== "object" || Array.isArray(request.claims)) {
118
+ throw new SigningClientError("'claims' must be an object when provided");
119
+ }
120
+ for (const key of Object.keys(request.claims)) {
121
+ if (RESERVED_CLAIM_NAMES.has(key)) {
122
+ throw new SigningClientError(`claims contains reserved key: '${key}'`);
123
+ }
124
+ }
125
+ }
126
+ }
127
+ function selectJwk(jwks, kid) {
128
+ if (!Array.isArray(jwks.keys) || jwks.keys.length === 0) {
129
+ throw new SigningClientError("JWKS response did not include any keys");
130
+ }
131
+ if (!kid) {
132
+ return jwks.keys[0];
133
+ }
134
+ const matched = jwks.keys.find((key) => key.kid === kid);
135
+ if (!matched) {
136
+ throw new SigningClientError(`No JWK found for kid '${kid}'`);
137
+ }
138
+ return matched;
139
+ }
140
+ function jwkToPem(jwk) {
141
+ try {
142
+ const keyObject = (0, node_crypto_1.createPublicKey)({
143
+ key: {
144
+ kty: "RSA",
145
+ n: jwk.n,
146
+ e: jwk.e,
147
+ },
148
+ format: "jwk",
149
+ });
150
+ return keyObject.export({
151
+ type: "spki",
152
+ format: "pem",
153
+ }).toString();
154
+ }
155
+ catch (error) {
156
+ throw new SigningClientError("Failed to convert JWK to PEM", { cause: error, body: jwk });
157
+ }
158
+ }
159
+ //# sourceMappingURL=signingClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signingClient.js","sourceRoot":"","sources":["../src/signingClient.ts"],"names":[],"mappings":";;;;;;AAUA,kDAA2E;AAC3E,6CAA8C;AAE9C,MAAM,WAAW,GAAgB,CAAC,QAAQ,EAAE,SAAS,EAAE,gBAAgB,EAAE,KAAK,CAAC,CAAC;AAChF,MAAM,oBAAoB,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;AAElF,MAAa,kBAAmB,SAAQ,KAAK;IAK3C,YAAY,OAAe,EAAE,OAA8D;QACzF,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;QACjC,IAAI,CAAC,MAAM,GAAG,OAAO,EAAE,MAAM,CAAC;QAC9B,IAAI,CAAC,IAAI,GAAG,OAAO,EAAE,IAAI,CAAC;QAC1B,IAAI,CAAC,KAAK,GAAG,OAAO,EAAE,KAAK,CAAC;IAC9B,CAAC;CACF;AAZD,gDAYC;AAED,MAAa,aAAa;IAGxB,YAAY,UAAgC,EAAE;QAC5C,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,KAAK,IAAI,eAAK,CAAC,MAAM,CAAC;YACxC,OAAO,EAAE,gBAAgB,CAAC,OAAO,CAAC,OAAO,IAAI,uBAAuB,CAAC;YACrE,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;aAC3B;SACF,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,OAAO;QACX,OAAO,IAAI,CAAC,OAAO,CAAkB,UAAU,EAAE;YAC/C,MAAM,EAAE,KAAK;SACd,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,OAAoB;QAC7B,mBAAmB,CAAC,OAAO,CAAC,CAAC;QAE7B,OAAO,IAAI,CAAC,OAAO,CAAe,OAAO,EAAE;YACzC,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,OAAO;SACd,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,OAAO;QACX,OAAO,IAAI,CAAC,OAAO,CAAe,wBAAwB,EAAE;YAC1D,MAAM,EAAE,KAAK;SACd,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,GAAY;QAChC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QAClC,MAAM,WAAW,GAAG,SAAS,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAEzC,OAAO,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC/B,CAAC;IAEO,KAAK,CAAC,OAAO,CAAI,IAAY,EAAE,MAA0B;QAC/D,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAI;gBAC1C,GAAG,EAAE,IAAI;gBACT,GAAG,MAAM;aACV,CAAC,CAAC;YAEH,OAAO,QAAQ,CAAC,IAAI,CAAC;QACvB,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,MAAM,eAAe,CAAC,KAAK,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC;CACF;AArDD,sCAqDC;AAED,SAAS,gBAAgB,CAAC,OAAe;IACvC,OAAO,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;AAChE,CAAC;AAED,SAAS,mBAAmB,CAAC,IAAa;IACxC,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtC,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,UAAU,GAAI,IAA6B,CAAC,KAAK,CAAC;IACxD,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5D,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,eAAe,CAAC,KAAc;IACrC,IAAI,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,YAAY,GAAG,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC;QAC1C,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC;QACtC,MAAM,OAAO,GAAG,mBAAmB,CAAC,YAAY,CAAC,IAAI,KAAK,CAAC,OAAO,IAAI,gBAAgB,CAAC;QAEvF,OAAO,IAAI,kBAAkB,CAAC,OAAO,EAAE;YACrC,MAAM;YACN,IAAI,EAAE,YAAY;YAClB,KAAK,EAAE,KAAK;SACb,CAAC,CAAC;IACL,CAAC;IAED,OAAO,IAAI,kBAAkB,CAAC,gBAAgB,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;AACpE,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IAQlC,OAAO,CAAC,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,cAAc,IAAI,KAAK,CAAC;AACzE,CAAC;AAED,SAAS,mBAAmB,CAAC,OAAoB;IAC/C,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAC5C,MAAM,IAAI,kBAAkB,CAAC,gCAAgC,CAAC,CAAC;IACjE,CAAC;IAED,IAAI,CAAC,OAAO,CAAC,GAAG,IAAI,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvF,MAAM,IAAI,kBAAkB,CAAC,kDAAkD,CAAC,CAAC;IACnF,CAAC;IAED,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,MAAM,IAAI,kBAAkB,CAC1B,0BAA0B,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC9E,CAAC;IACJ,CAAC;IAED,IAAI,OAAO,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;QAC9B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,EAAE,CAAC;YACxF,MAAM,IAAI,kBAAkB,CAAC,iDAAiD,CAAC,CAAC;QAClF,CAAC;IACH,CAAC;IAED,IAAI,OAAO,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACtC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,OAAO,CAAC,WAAW,IAAI,CAAC,EAAE,CAAC;YACvE,MAAM,IAAI,kBAAkB,CAAC,wDAAwD,CAAC,CAAC;QACzF,CAAC;IACH,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QACjC,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,OAAO,OAAO,CAAC,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3F,MAAM,IAAI,kBAAkB,CAAC,0CAA0C,CAAC,CAAC;QAC3E,CAAC;QAED,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC9C,IAAI,oBAAoB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;gBAClC,MAAM,IAAI,kBAAkB,CAAC,kCAAkC,GAAG,GAAG,CAAC,CAAC;YACzE,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,IAAkB,EAAE,GAAY;IACjD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxD,MAAM,IAAI,kBAAkB,CAAC,wCAAwC,CAAC,CAAC;IACzE,CAAC;IAED,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACtB,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC;IACzD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,kBAAkB,CAAC,yBAAyB,GAAG,GAAG,CAAC,CAAC;IAChE,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,QAAQ,CAAC,GAAQ;IACxB,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,IAAA,6BAAe,EAAC;YAChC,GAAG,EAAE;gBACH,GAAG,EAAE,KAAK;gBACV,CAAC,EAAE,GAAG,CAAC,CAAC;gBACR,CAAC,EAAE,GAAG,CAAC,CAAC;aACT;YACD,MAAM,EAAE,KAAK;SACd,CAAC,CAAC;QAEH,OAAO,SAAS,CAAC,MAAM,CAAC;YACtB,IAAI,EAAE,MAAM;YACZ,MAAM,EAAE,KAAK;SACd,CAAC,CAAC,QAAQ,EAAE,CAAC;IAChB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,kBAAkB,CAAC,8BAA8B,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;IAC5F,CAAC;AACH,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const node_crypto_1 = require("node:crypto");
9
+ const signingClient_1 = require("./signingClient");
10
+ function createMockAxios(handler) {
11
+ return {
12
+ request: handler,
13
+ };
14
+ }
15
+ (0, node_test_1.default)("healthz calls /healthz and returns status", async () => {
16
+ const mockAxios = createMockAxios(async (config) => {
17
+ strict_1.default.equal(config.url, "/healthz");
18
+ strict_1.default.equal(config.method, "GET");
19
+ return {
20
+ data: { status: "ok" },
21
+ status: 200,
22
+ statusText: "OK",
23
+ headers: {},
24
+ config,
25
+ };
26
+ });
27
+ const client = new signingClient_1.SigningClient({ axios: mockAxios });
28
+ const result = await client.healthz();
29
+ strict_1.default.deepEqual(result, { status: "ok" });
30
+ });
31
+ (0, node_test_1.default)("sign sends payload and returns token", async () => {
32
+ const mockAxios = createMockAxios(async (config) => {
33
+ strict_1.default.equal(config.url, "/sign");
34
+ strict_1.default.equal(config.method, "POST");
35
+ strict_1.default.deepEqual(config.data, {
36
+ sub: "device-42",
37
+ kind: "access",
38
+ aud: ["api.example.com"],
39
+ ttl_seconds: 3600,
40
+ claims: { tid: "tenant-acme", ver: 3 },
41
+ });
42
+ return {
43
+ data: { token: "header.payload.signature" },
44
+ status: 200,
45
+ statusText: "OK",
46
+ headers: {},
47
+ config,
48
+ };
49
+ });
50
+ const client = new signingClient_1.SigningClient({ axios: mockAxios });
51
+ const result = await client.sign({
52
+ sub: "device-42",
53
+ kind: "access",
54
+ aud: ["api.example.com"],
55
+ ttl_seconds: 3600,
56
+ claims: { tid: "tenant-acme", ver: 3 },
57
+ });
58
+ strict_1.default.deepEqual(result, { token: "header.payload.signature" });
59
+ });
60
+ (0, node_test_1.default)("getJwks returns key set", async () => {
61
+ const mockAxios = createMockAxios(async (config) => {
62
+ strict_1.default.equal(config.url, "/.well-known/jwks.json");
63
+ return {
64
+ data: {
65
+ keys: [{
66
+ kty: "RSA",
67
+ use: "sig",
68
+ alg: "RS256",
69
+ kid: "nanolink",
70
+ n: "abc",
71
+ e: "AQAB",
72
+ }],
73
+ },
74
+ status: 200,
75
+ statusText: "OK",
76
+ headers: {},
77
+ config,
78
+ };
79
+ });
80
+ const client = new signingClient_1.SigningClient({ axios: mockAxios });
81
+ const result = await client.getJwks();
82
+ strict_1.default.equal(result.keys[0]?.kid, "nanolink");
83
+ });
84
+ (0, node_test_1.default)("throws SigningClientError on API error", async () => {
85
+ const mockAxios = createMockAxios(async () => {
86
+ throw {
87
+ isAxiosError: true,
88
+ message: "Request failed with status code 400",
89
+ response: {
90
+ status: 400,
91
+ data: { error: "invalid kind" },
92
+ },
93
+ };
94
+ });
95
+ const client = new signingClient_1.SigningClient({ axios: mockAxios });
96
+ await strict_1.default.rejects(() => client.sign({ sub: "user", kind: "access", claims: { iss: "nope" } }), (error) => {
97
+ strict_1.default.ok(error instanceof signingClient_1.SigningClientError);
98
+ strict_1.default.equal(error.message, "claims contains reserved key: 'iss'");
99
+ return true;
100
+ });
101
+ });
102
+ (0, node_test_1.default)("propagates server-side error payload", async () => {
103
+ const mockAxios = createMockAxios(async () => {
104
+ throw {
105
+ isAxiosError: true,
106
+ message: "Request failed with status code 400",
107
+ response: {
108
+ status: 400,
109
+ data: { error: "missing sub" },
110
+ },
111
+ };
112
+ });
113
+ const client = new signingClient_1.SigningClient({ axios: mockAxios });
114
+ await strict_1.default.rejects(() => client.sign({ sub: "user-1", kind: "service" }), (error) => {
115
+ strict_1.default.ok(error instanceof signingClient_1.SigningClientError);
116
+ strict_1.default.equal(error.status, 400);
117
+ strict_1.default.equal(error.message, "missing sub");
118
+ return true;
119
+ });
120
+ });
121
+ (0, node_test_1.default)("getPublicKeyPem converts JWKS key to PEM", async () => {
122
+ const { publicKey } = (0, node_crypto_1.generateKeyPairSync)("rsa", { modulusLength: 2048 });
123
+ const publicJwk = publicKey.export({ format: "jwk" });
124
+ const mockAxios = createMockAxios(async (config) => {
125
+ return {
126
+ data: {
127
+ keys: [
128
+ {
129
+ kty: "RSA",
130
+ use: "sig",
131
+ alg: "RS256",
132
+ kid: "nanolink",
133
+ n: publicJwk.n,
134
+ e: publicJwk.e,
135
+ },
136
+ ],
137
+ },
138
+ status: 200,
139
+ statusText: "OK",
140
+ headers: {},
141
+ config,
142
+ };
143
+ });
144
+ const client = new signingClient_1.SigningClient({ axios: mockAxios });
145
+ const pem = await client.getPublicKeyPem();
146
+ strict_1.default.match(pem, /BEGIN PUBLIC KEY/);
147
+ strict_1.default.match(pem, /END PUBLIC KEY/);
148
+ });
149
+ (0, node_test_1.default)("getPublicKeyPem selects key by kid", async () => {
150
+ const { publicKey } = (0, node_crypto_1.generateKeyPairSync)("rsa", { modulusLength: 2048 });
151
+ const publicJwk = publicKey.export({ format: "jwk" });
152
+ const mockAxios = createMockAxios(async (config) => {
153
+ return {
154
+ data: {
155
+ keys: [
156
+ {
157
+ kty: "RSA",
158
+ use: "sig",
159
+ alg: "RS256",
160
+ kid: "first",
161
+ n: publicJwk.n,
162
+ e: publicJwk.e,
163
+ },
164
+ {
165
+ kty: "RSA",
166
+ use: "sig",
167
+ alg: "RS256",
168
+ kid: "selected",
169
+ n: publicJwk.n,
170
+ e: publicJwk.e,
171
+ },
172
+ ],
173
+ },
174
+ status: 200,
175
+ statusText: "OK",
176
+ headers: {},
177
+ config,
178
+ };
179
+ });
180
+ const client = new signingClient_1.SigningClient({ axios: mockAxios });
181
+ const pem = await client.getPublicKeyPem("selected");
182
+ strict_1.default.match(pem, /BEGIN PUBLIC KEY/);
183
+ });
184
+ //# sourceMappingURL=signingClient.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signingClient.test.js","sourceRoot":"","sources":["../src/signingClient.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import type { AxiosInstance } from "axios";
2
+ export type TokenKind = "access" | "service" | "password_reset" | "2fa";
3
+ export interface HealthzResponse {
4
+ status: "ok";
5
+ }
6
+ export interface SignRequest {
7
+ sub: string;
8
+ kind: TokenKind;
9
+ aud?: string[];
10
+ ttl_seconds?: number;
11
+ claims?: Record<string, unknown>;
12
+ }
13
+ export interface SignResponse {
14
+ token: string;
15
+ }
16
+ export interface Jwk {
17
+ kty: "RSA";
18
+ use: "sig";
19
+ alg: "RS256";
20
+ kid: string;
21
+ n: string;
22
+ e: string;
23
+ }
24
+ export interface JwksResponse {
25
+ keys: Jwk[];
26
+ }
27
+ export interface SigningClientOptions {
28
+ baseUrl?: string;
29
+ axios?: AxiosInstance;
30
+ headers?: Record<string, string>;
31
+ }
32
+ export interface ServiceErrorResponse {
33
+ error: string;
34
+ }
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@nanolink/signing-client",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript Node.js client for the Signing Service API",
5
+ "license": "MIT",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "engines": {
12
+ "node": ">=18"
13
+ },
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.json",
16
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
17
+ "prebuild": "npm run clean",
18
+ "test": "node --test dist/**/*.test.js",
19
+ "test:src": "node --loader ts-node/esm --test src/**/*.test.ts"
20
+ },
21
+ "keywords": [
22
+ "jwt",
23
+ "jwks",
24
+ "signing",
25
+ "typescript",
26
+ "nodejs"
27
+ ],
28
+ "dependencies": {
29
+ "axios": "^1.8.4"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^22.15.3",
33
+ "ts-node": "^10.9.2",
34
+ "typescript": "^5.8.3"
35
+ }
36
+ }