@hardenlabs/hmac 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/dist/canonical.d.ts +26 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +87 -0
- package/dist/canonical.js.map +1 -0
- package/dist/client-factory.d.ts +23 -0
- package/dist/client-factory.d.ts.map +1 -0
- package/dist/client-factory.js +53 -0
- package/dist/client-factory.js.map +1 -0
- package/dist/config.d.ts +75 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +85 -0
- package/dist/config.js.map +1 -0
- package/dist/env-loader.d.ts +21 -0
- package/dist/env-loader.d.ts.map +1 -0
- package/dist/env-loader.js +145 -0
- package/dist/env-loader.js.map +1 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +10 -0
- package/dist/errors.js.map +1 -0
- package/dist/http-client.d.ts +61 -0
- package/dist/http-client.d.ts.map +1 -0
- package/dist/http-client.js +51 -0
- package/dist/http-client.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/express.d.ts +24 -0
- package/dist/middleware/express.d.ts.map +1 -0
- package/dist/middleware/express.js +145 -0
- package/dist/middleware/express.js.map +1 -0
- package/dist/middleware/fetch.d.ts +16 -0
- package/dist/middleware/fetch.d.ts.map +1 -0
- package/dist/middleware/fetch.js +105 -0
- package/dist/middleware/fetch.js.map +1 -0
- package/dist/signing.d.ts +18 -0
- package/dist/signing.d.ts.map +1 -0
- package/dist/signing.js +62 -0
- package/dist/signing.js.map +1 -0
- package/dist/validation.d.ts +21 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +47 -0
- package/dist/validation.js.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Options for HTTP request methods. */
|
|
2
|
+
export interface RequestOptions {
|
|
3
|
+
headers?: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
/** Normalized HTTP response, adapter-independent. */
|
|
6
|
+
export interface HmacResponse {
|
|
7
|
+
status: number;
|
|
8
|
+
headers: Record<string, string>;
|
|
9
|
+
json(): Promise<unknown>;
|
|
10
|
+
text(): Promise<string>;
|
|
11
|
+
}
|
|
12
|
+
/** HTTP client with automatic HMAC signing. */
|
|
13
|
+
export interface HmacClient {
|
|
14
|
+
get(path: string, options?: RequestOptions): Promise<HmacResponse>;
|
|
15
|
+
post(path: string, body?: string, options?: RequestOptions): Promise<HmacResponse>;
|
|
16
|
+
put(path: string, body?: string, options?: RequestOptions): Promise<HmacResponse>;
|
|
17
|
+
patch(path: string, body?: string, options?: RequestOptions): Promise<HmacResponse>;
|
|
18
|
+
delete(path: string, options?: RequestOptions): Promise<HmacResponse>;
|
|
19
|
+
/** Generic request method for non-standard HTTP methods. */
|
|
20
|
+
request(method: string, path: string, body?: string, options?: RequestOptions): Promise<HmacResponse>;
|
|
21
|
+
}
|
|
22
|
+
/** Adapter interface that abstracts the underlying HTTP transport (fetch vs axios). */
|
|
23
|
+
export interface HttpAdapter {
|
|
24
|
+
request(url: string, method: string, body: string | undefined, headers: Record<string, string>): Promise<HmacResponse>;
|
|
25
|
+
}
|
|
26
|
+
/** Adapter that uses the Fetch API as the underlying HTTP transport. */
|
|
27
|
+
export declare class FetchAdapter implements HttpAdapter {
|
|
28
|
+
private fetchFn;
|
|
29
|
+
constructor(fetchFn?: typeof globalThis.fetch);
|
|
30
|
+
request(url: string, method: string, body: string | undefined, headers: Record<string, string>): Promise<HmacResponse>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Minimal axios instance type to avoid requiring the axios package at compile time.
|
|
34
|
+
* Users pass their own axios instance; we only depend on the shape.
|
|
35
|
+
*/
|
|
36
|
+
export interface AxiosInstance {
|
|
37
|
+
request(config: {
|
|
38
|
+
url: string;
|
|
39
|
+
method: string;
|
|
40
|
+
data?: string;
|
|
41
|
+
headers?: Record<string, string>;
|
|
42
|
+
}): Promise<{
|
|
43
|
+
status: number;
|
|
44
|
+
headers: Record<string, string>;
|
|
45
|
+
data: unknown;
|
|
46
|
+
}>;
|
|
47
|
+
}
|
|
48
|
+
/** Adapter that uses an axios instance as the underlying HTTP transport. */
|
|
49
|
+
export declare class AxiosAdapter implements HttpAdapter {
|
|
50
|
+
private axios;
|
|
51
|
+
constructor(axiosInstance: AxiosInstance);
|
|
52
|
+
request(url: string, method: string, body: string | undefined, headers: Record<string, string>): Promise<HmacResponse>;
|
|
53
|
+
}
|
|
54
|
+
/** Options for creating an HmacClientFactory. */
|
|
55
|
+
export interface HmacClientFactoryOptions {
|
|
56
|
+
/** Custom fetch function. Ignored if axios is provided. */
|
|
57
|
+
fetchFn?: typeof globalThis.fetch;
|
|
58
|
+
/** Axios instance to use instead of fetch. */
|
|
59
|
+
axios?: AxiosInstance;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=http-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-client.d.ts","sourceRoot":"","sources":["../src/http-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Adapter that uses the Fetch API as the underlying HTTP transport. */
|
|
2
|
+
export class FetchAdapter {
|
|
3
|
+
fetchFn;
|
|
4
|
+
constructor(fetchFn) {
|
|
5
|
+
this.fetchFn = fetchFn ?? globalThis.fetch;
|
|
6
|
+
}
|
|
7
|
+
async request(url, method, body, headers) {
|
|
8
|
+
const resp = await this.fetchFn(url, { method, body, headers });
|
|
9
|
+
const responseHeaders = {};
|
|
10
|
+
resp.headers.forEach((value, key) => {
|
|
11
|
+
responseHeaders[key] = value;
|
|
12
|
+
});
|
|
13
|
+
// Cache the body text so both json() and text() work without double-consuming
|
|
14
|
+
let cachedText;
|
|
15
|
+
return {
|
|
16
|
+
status: resp.status,
|
|
17
|
+
headers: responseHeaders,
|
|
18
|
+
async json() {
|
|
19
|
+
cachedText ??= await resp.text();
|
|
20
|
+
return JSON.parse(cachedText);
|
|
21
|
+
},
|
|
22
|
+
async text() {
|
|
23
|
+
cachedText ??= await resp.text();
|
|
24
|
+
return cachedText;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Adapter that uses an axios instance as the underlying HTTP transport. */
|
|
30
|
+
export class AxiosAdapter {
|
|
31
|
+
axios;
|
|
32
|
+
constructor(axiosInstance) {
|
|
33
|
+
this.axios = axiosInstance;
|
|
34
|
+
}
|
|
35
|
+
async request(url, method, body, headers) {
|
|
36
|
+
const resp = await this.axios.request({ url, method, data: body, headers });
|
|
37
|
+
const data = resp.data;
|
|
38
|
+
const text = typeof data === "string" ? data : JSON.stringify(data);
|
|
39
|
+
return {
|
|
40
|
+
status: resp.status,
|
|
41
|
+
headers: resp.headers,
|
|
42
|
+
async json() {
|
|
43
|
+
return typeof data === "string" ? JSON.parse(data) : data;
|
|
44
|
+
},
|
|
45
|
+
async text() {
|
|
46
|
+
return text;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=http-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-client.js","sourceRoot":"","sources":["../src/http-client.ts"],"names":[],"mappings":"AAkCA,wEAAwE;AACxE,MAAM,OAAO,YAAY;IACf,OAAO,CAA0B;IAEzC,YAAY,OAAiC;QAC3C,IAAI,CAAC,OAAO,GAAG,OAAO,IAAI,UAAU,CAAC,KAAK,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,OAAO,CACX,GAAW,EACX,MAAc,EACd,IAAwB,EACxB,OAA+B;QAE/B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAChE,MAAM,eAAe,GAA2B,EAAE,CAAC;QACnD,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YAClC,eAAe,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QAC/B,CAAC,CAAC,CAAC;QAEH,8EAA8E;QAC9E,IAAI,UAA8B,CAAC;QACnC,OAAO;YACL,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,OAAO,EAAE,eAAe;YACxB,KAAK,CAAC,IAAI;gBACR,UAAU,KAAK,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;gBACjC,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YAChC,CAAC;YACD,KAAK,CAAC,IAAI;gBACR,UAAU,KAAK,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;gBACjC,OAAO,UAAU,CAAC;YACpB,CAAC;SACF,CAAC;IACJ,CAAC;CACF;AAmBD,4EAA4E;AAC5E,MAAM,OAAO,YAAY;IACf,KAAK,CAAgB;IAE7B,YAAY,aAA4B;QACtC,IAAI,CAAC,KAAK,GAAG,aAAa,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,OAAO,CACX,GAAW,EACX,MAAc,EACd,IAAwB,EACxB,OAA+B;QAE/B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAC5E,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACvB,MAAM,IAAI,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACpE,OAAO;YACL,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,OAAO,EAAE,IAAI,CAAC,OAAiC;YAC/C,KAAK,CAAC,IAAI;gBACR,OAAO,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAC5D,CAAC;YACD,KAAK,CAAC,IAAI;gBACR,OAAO,IAAI,CAAC;YACd,CAAC;SACF,CAAC;IACJ,CAAC;CACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { buildCanonicalString, buildSignedHeaders, type CanonicalStringParams, type SignedHeadersResult, } from "./canonical.js";
|
|
2
|
+
export { type HmacClientIdentity, type HmacConfig, type HmacTargetConfig, type SignedHeadersConfig, SIGNATURE_HEADER, TIMESTAMP_HEADER, SIGNED_HEADERS_HEADER, CLIENT_ID_HEADER, DEFAULT_TIMESTAMP_TOLERANCE_SECONDS, defaultSignedHeadersConfig, noneSignedHeadersConfig, createHmacConfig, getEffectiveSecret, getEffectiveSignedHeaders, getEffectiveTimestampTolerance, configForTarget, } from "./config.js";
|
|
3
|
+
export { HmacValidationError, type HmacErrorType } from "./errors.js";
|
|
4
|
+
export { sign, verify } from "./signing.js";
|
|
5
|
+
export { validateRequest, type ValidateRequestParams } from "./validation.js";
|
|
6
|
+
export { signRequestHeaders, createSignedFetch } from "./middleware/fetch.js";
|
|
7
|
+
export { hardenHmacMiddleware, type SecretResolver } from "./middleware/express.js";
|
|
8
|
+
export { fromEnv } from "./env-loader.js";
|
|
9
|
+
export { createHmacClientFactory, type HmacClientFactory, } from "./client-factory.js";
|
|
10
|
+
export { FetchAdapter, AxiosAdapter, type HmacClient, type HmacResponse, type RequestOptions, type HttpAdapter, type AxiosInstance, type HmacClientFactoryOptions, } from "./http-client.js";
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,kBAAkB,EAClB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACrB,gBAAgB,EAChB,mCAAmC,EACnC,0BAA0B,EAC1B,uBAAuB,EACvB,gBAAgB,EAChB,kBAAkB,EAClB,yBAAyB,EACzB,8BAA8B,EAC9B,eAAe,GAChB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,mBAAmB,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAEtE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE5C,OAAO,EAAE,eAAe,EAAE,KAAK,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAE9E,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE9E,OAAO,EAAE,oBAAoB,EAAE,KAAK,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAEpF,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE1C,OAAO,EACL,uBAAuB,EACvB,KAAK,iBAAiB,GACvB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,wBAAwB,GAC9B,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { buildCanonicalString, buildSignedHeaders, } from "./canonical.js";
|
|
2
|
+
export { SIGNATURE_HEADER, TIMESTAMP_HEADER, SIGNED_HEADERS_HEADER, CLIENT_ID_HEADER, DEFAULT_TIMESTAMP_TOLERANCE_SECONDS, defaultSignedHeadersConfig, noneSignedHeadersConfig, createHmacConfig, getEffectiveSecret, getEffectiveSignedHeaders, getEffectiveTimestampTolerance, configForTarget, } from "./config.js";
|
|
3
|
+
export { HmacValidationError } from "./errors.js";
|
|
4
|
+
export { sign, verify } from "./signing.js";
|
|
5
|
+
export { validateRequest } from "./validation.js";
|
|
6
|
+
export { signRequestHeaders, createSignedFetch } from "./middleware/fetch.js";
|
|
7
|
+
export { hardenHmacMiddleware } from "./middleware/express.js";
|
|
8
|
+
export { fromEnv } from "./env-loader.js";
|
|
9
|
+
export { createHmacClientFactory, } from "./client-factory.js";
|
|
10
|
+
export { FetchAdapter, AxiosAdapter, } from "./http-client.js";
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,kBAAkB,GAGnB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAKL,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACrB,gBAAgB,EAChB,mCAAmC,EACnC,0BAA0B,EAC1B,uBAAuB,EACvB,gBAAgB,EAChB,kBAAkB,EAClB,yBAAyB,EACzB,8BAA8B,EAC9B,eAAe,GAChB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,mBAAmB,EAAsB,MAAM,aAAa,CAAC;AAEtE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE5C,OAAO,EAAE,eAAe,EAA8B,MAAM,iBAAiB,CAAC;AAE9E,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE9E,OAAO,EAAE,oBAAoB,EAAuB,MAAM,yBAAyB,CAAC;AAEpF,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE1C,OAAO,EACL,uBAAuB,GAExB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,YAAY,EACZ,YAAY,GAOb,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from "express";
|
|
2
|
+
import type { HmacConfig } from "../config.js";
|
|
3
|
+
/**
|
|
4
|
+
* Optional callback to resolve the shared secret per-request.
|
|
5
|
+
* Return the Base64-encoded secret, or null/undefined to fall back to config.sharedSecretBase64.
|
|
6
|
+
*/
|
|
7
|
+
export type SecretResolver = (req: Request) => string | null | undefined | Promise<string | null | undefined>;
|
|
8
|
+
/**
|
|
9
|
+
* Create an Express middleware that validates incoming HMAC-signed requests.
|
|
10
|
+
*
|
|
11
|
+
* IMPORTANT: This middleware requires the raw request body as a string or Buffer.
|
|
12
|
+
* Use `express.text({ type: "*\/*" })` or `express.raw()` upstream — NOT `express.json()`.
|
|
13
|
+
*
|
|
14
|
+
* If `express.json()` runs first, `req.body` will be a parsed object and
|
|
15
|
+
* `JSON.stringify(req.body)` may produce different output than the original raw body,
|
|
16
|
+
* causing signature verification to fail. In that case this middleware returns 400
|
|
17
|
+
* with a descriptive error rather than silently producing incorrect results.
|
|
18
|
+
*
|
|
19
|
+
* @param config - HMAC configuration with shared secret and tolerance.
|
|
20
|
+
* @param secretResolver - Optional callback to resolve the secret per-request (e.g., for multi-tenant).
|
|
21
|
+
* @returns Express middleware function.
|
|
22
|
+
*/
|
|
23
|
+
export declare function hardenHmacMiddleware(config: HmacConfig, secretResolver?: SecretResolver): (req: Request, res: Response, next: NextFunction) => void;
|
|
24
|
+
//# sourceMappingURL=express.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/middleware/express.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAK/C;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,CAC3B,GAAG,EAAE,OAAO,KACT,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;AAEpE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,UAAU,EAClB,cAAc,CAAC,EAAE,cAAc,GAC9B,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAsI3D"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { CLIENT_ID_HEADER, SIGNATURE_HEADER, TIMESTAMP_HEADER } from "../config.js";
|
|
2
|
+
import { HmacValidationError } from "../errors.js";
|
|
3
|
+
import { validateRequest } from "../validation.js";
|
|
4
|
+
/**
|
|
5
|
+
* Create an Express middleware that validates incoming HMAC-signed requests.
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: This middleware requires the raw request body as a string or Buffer.
|
|
8
|
+
* Use `express.text({ type: "*\/*" })` or `express.raw()` upstream — NOT `express.json()`.
|
|
9
|
+
*
|
|
10
|
+
* If `express.json()` runs first, `req.body` will be a parsed object and
|
|
11
|
+
* `JSON.stringify(req.body)` may produce different output than the original raw body,
|
|
12
|
+
* causing signature verification to fail. In that case this middleware returns 400
|
|
13
|
+
* with a descriptive error rather than silently producing incorrect results.
|
|
14
|
+
*
|
|
15
|
+
* @param config - HMAC configuration with shared secret and tolerance.
|
|
16
|
+
* @param secretResolver - Optional callback to resolve the secret per-request (e.g., for multi-tenant).
|
|
17
|
+
* @returns Express middleware function.
|
|
18
|
+
*/
|
|
19
|
+
export function hardenHmacMiddleware(config, secretResolver) {
|
|
20
|
+
return (req, res, next) => {
|
|
21
|
+
// Collect body as string.
|
|
22
|
+
// Reject parsed objects — they cannot be reliably re-serialized to the original wire format.
|
|
23
|
+
let body = "";
|
|
24
|
+
if (typeof req.body === "string") {
|
|
25
|
+
body = req.body;
|
|
26
|
+
}
|
|
27
|
+
else if (Buffer.isBuffer(req.body)) {
|
|
28
|
+
body = req.body.toString("utf8");
|
|
29
|
+
}
|
|
30
|
+
else if (req.body !== undefined && req.body !== null) {
|
|
31
|
+
// If it's a plain object with no keys, treat as empty body.
|
|
32
|
+
// Express can set req.body = {} when no body parser matches.
|
|
33
|
+
if (typeof req.body === "object" &&
|
|
34
|
+
Object.keys(req.body).length === 0) {
|
|
35
|
+
body = "";
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// req.body is a parsed object (e.g. from express.json()). This is not safe for HMAC
|
|
39
|
+
// because JSON.stringify may differ from the original wire bytes.
|
|
40
|
+
res.status(400).json({
|
|
41
|
+
error: "body_not_raw",
|
|
42
|
+
message: "HardenHMAC middleware requires the raw request body. " +
|
|
43
|
+
"Use express.text() or express.raw() instead of express.json() " +
|
|
44
|
+
"before this middleware.",
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const path = req.originalUrl ?? req.url;
|
|
50
|
+
// Extract headers as record
|
|
51
|
+
const requestHeaders = {};
|
|
52
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
53
|
+
if (typeof value === "string") {
|
|
54
|
+
requestHeaders[name] = value;
|
|
55
|
+
}
|
|
56
|
+
else if (Array.isArray(value)) {
|
|
57
|
+
requestHeaders[name] = value.join(", ");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const rawSig = req.headers[SIGNATURE_HEADER.toLowerCase()];
|
|
61
|
+
const signatureHeader = Array.isArray(rawSig) ? rawSig[0] : rawSig;
|
|
62
|
+
const rawTs = req.headers[TIMESTAMP_HEADER.toLowerCase()];
|
|
63
|
+
const timestampHeader = Array.isArray(rawTs) ? rawTs[0] : rawTs;
|
|
64
|
+
// Validate with the resolved secret
|
|
65
|
+
const validateWithSecret = (secret) => {
|
|
66
|
+
const effectiveConfig = {
|
|
67
|
+
...config,
|
|
68
|
+
sharedSecretBase64: secret,
|
|
69
|
+
};
|
|
70
|
+
try {
|
|
71
|
+
validateRequest(effectiveConfig, {
|
|
72
|
+
method: req.method,
|
|
73
|
+
path,
|
|
74
|
+
body,
|
|
75
|
+
signatureHeader,
|
|
76
|
+
timestampHeader,
|
|
77
|
+
requestHeaders,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error instanceof HmacValidationError) {
|
|
82
|
+
const statusCode = error.errorType === "missing_signature" ||
|
|
83
|
+
error.errorType === "missing_timestamp" ||
|
|
84
|
+
error.errorType === "invalid_timestamp"
|
|
85
|
+
? 400
|
|
86
|
+
: 401;
|
|
87
|
+
res.status(statusCode).json({
|
|
88
|
+
error: error.errorType,
|
|
89
|
+
message: error.message,
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
next();
|
|
96
|
+
};
|
|
97
|
+
// Resolution chain: resolver -> Clients dict -> config fallback
|
|
98
|
+
const resolveAndValidate = (resolverResult) => {
|
|
99
|
+
// 1. secretResolver callback result
|
|
100
|
+
if (resolverResult) {
|
|
101
|
+
validateWithSecret(resolverResult);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// 2. X-Harden-Client-Id header -> look up in config.clients
|
|
105
|
+
const clientId = requestHeaders[CLIENT_ID_HEADER.toLowerCase()];
|
|
106
|
+
if (clientId) {
|
|
107
|
+
const clientIdentity = config.clients?.[clientId];
|
|
108
|
+
if (clientIdentity?.sharedSecret) {
|
|
109
|
+
validateWithSecret(clientIdentity.sharedSecret);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Client ID was provided but not found
|
|
113
|
+
res.status(401).json({
|
|
114
|
+
error: "unknown_client",
|
|
115
|
+
message: "Unknown or unconfigured client.",
|
|
116
|
+
});
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// 3. Fall back to config.sharedSecretBase64
|
|
120
|
+
if (config.sharedSecretBase64) {
|
|
121
|
+
validateWithSecret(config.sharedSecretBase64);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// 4. No secret available
|
|
125
|
+
res.status(401).json({
|
|
126
|
+
error: "no_secret",
|
|
127
|
+
message: "No shared secret configured for this request.",
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
if (secretResolver) {
|
|
131
|
+
const result = secretResolver(req);
|
|
132
|
+
if (result instanceof Promise) {
|
|
133
|
+
result.then(resolveAndValidate).catch((err) => {
|
|
134
|
+
next(err);
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
resolveAndValidate(result);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
resolveAndValidate(null);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=express.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express.js","sourceRoot":"","sources":["../../src/middleware/express.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { HmacConfig } from "../config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Sign headers for an outgoing request.
|
|
4
|
+
*
|
|
5
|
+
* Returns a Record of headers to add to the request.
|
|
6
|
+
*/
|
|
7
|
+
export declare function signRequestHeaders(config: HmacConfig, method: string, path: string, body?: string, requestHeaders?: Record<string, string>, timestamp?: number): Record<string, string>;
|
|
8
|
+
/**
|
|
9
|
+
* Create a fetch wrapper that automatically signs outgoing requests.
|
|
10
|
+
*
|
|
11
|
+
* @param config - HMAC configuration with shared secret.
|
|
12
|
+
* @param fetchFn - The fetch function to wrap (defaults to globalThis.fetch).
|
|
13
|
+
* @returns A function that signs requests before passing them to the underlying fetch.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createSignedFetch(config: HmacConfig, fetchFn?: typeof globalThis.fetch): (url: string | URL, init?: RequestInit) => Promise<Response>;
|
|
16
|
+
//# sourceMappingURL=fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/middleware/fetch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAQ/C;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,MAAW,EACjB,cAAc,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,EAC3C,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA6BxB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,UAAU,EAClB,OAAO,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,GAChC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CA+E9D"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { buildCanonicalString, buildSignedHeaders } from "../canonical.js";
|
|
2
|
+
import { SIGNATURE_HEADER, SIGNED_HEADERS_HEADER, TIMESTAMP_HEADER, } from "../config.js";
|
|
3
|
+
import { sign } from "../signing.js";
|
|
4
|
+
/**
|
|
5
|
+
* Sign headers for an outgoing request.
|
|
6
|
+
*
|
|
7
|
+
* Returns a Record of headers to add to the request.
|
|
8
|
+
*/
|
|
9
|
+
export function signRequestHeaders(config, method, path, body = "", requestHeaders = {}, timestamp) {
|
|
10
|
+
const ts = timestamp ?? Math.floor(Date.now() / 1000);
|
|
11
|
+
const { headerNames } = buildSignedHeaders(config.signedHeaders, requestHeaders);
|
|
12
|
+
const canonicalString = buildCanonicalString({
|
|
13
|
+
method,
|
|
14
|
+
path,
|
|
15
|
+
body,
|
|
16
|
+
timestamp: ts,
|
|
17
|
+
signedHeadersConfig: config.signedHeaders,
|
|
18
|
+
requestHeaders,
|
|
19
|
+
});
|
|
20
|
+
const signature = sign(config.sharedSecretBase64, canonicalString);
|
|
21
|
+
const result = {
|
|
22
|
+
[SIGNATURE_HEADER]: signature,
|
|
23
|
+
[TIMESTAMP_HEADER]: String(ts),
|
|
24
|
+
};
|
|
25
|
+
if (headerNames.length > 0) {
|
|
26
|
+
result[SIGNED_HEADERS_HEADER] = headerNames.join(";");
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create a fetch wrapper that automatically signs outgoing requests.
|
|
32
|
+
*
|
|
33
|
+
* @param config - HMAC configuration with shared secret.
|
|
34
|
+
* @param fetchFn - The fetch function to wrap (defaults to globalThis.fetch).
|
|
35
|
+
* @returns A function that signs requests before passing them to the underlying fetch.
|
|
36
|
+
*/
|
|
37
|
+
export function createSignedFetch(config, fetchFn) {
|
|
38
|
+
const baseFetch = fetchFn ?? globalThis.fetch;
|
|
39
|
+
return async (url, init) => {
|
|
40
|
+
let path;
|
|
41
|
+
if (typeof url === "string") {
|
|
42
|
+
// Handle both absolute URLs and relative paths
|
|
43
|
+
if (url.startsWith("/") || url.startsWith("?")) {
|
|
44
|
+
path = url;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
try {
|
|
48
|
+
const parsedUrl = new URL(url);
|
|
49
|
+
path = parsedUrl.pathname + parsedUrl.search;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Treat as relative path if URL parsing fails
|
|
53
|
+
path = url;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
path = url.pathname + url.search;
|
|
59
|
+
}
|
|
60
|
+
const method = init?.method ?? "GET";
|
|
61
|
+
// Only string bodies are supported for HMAC signing.
|
|
62
|
+
// Non-string bodies (Blob, FormData, ReadableStream, etc.) cannot be
|
|
63
|
+
// reliably converted to the same bytes the server will receive.
|
|
64
|
+
let body = "";
|
|
65
|
+
if (init?.body !== undefined && init?.body !== null) {
|
|
66
|
+
if (typeof init.body === "string") {
|
|
67
|
+
body = init.body;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
throw new Error("HardenHMAC: Only string request bodies are supported for HMAC signing. " +
|
|
71
|
+
"Convert your body to a string before passing it to fetch.");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Collect existing headers, normalizing keys to lowercase
|
|
75
|
+
// to avoid case-sensitive duplicates (HTTP headers are case-insensitive)
|
|
76
|
+
const existingHeaders = {};
|
|
77
|
+
if (init?.headers) {
|
|
78
|
+
if (init.headers instanceof Headers) {
|
|
79
|
+
init.headers.forEach((value, key) => {
|
|
80
|
+
existingHeaders[key.toLowerCase()] = value;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
else if (Array.isArray(init.headers)) {
|
|
84
|
+
for (const [key, value] of init.headers) {
|
|
85
|
+
existingHeaders[key.toLowerCase()] = String(value);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
for (const [key, value] of Object.entries(init.headers)) {
|
|
90
|
+
existingHeaders[key.toLowerCase()] = String(value);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const sigHeaders = signRequestHeaders(config, method, path, body, existingHeaders);
|
|
95
|
+
const mergedHeaders = {
|
|
96
|
+
...existingHeaders,
|
|
97
|
+
...sigHeaders,
|
|
98
|
+
};
|
|
99
|
+
return baseFetch(url, {
|
|
100
|
+
...init,
|
|
101
|
+
headers: mergedHeaders,
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../src/middleware/fetch.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute the HMAC-SHA256 signature of a canonical string.
|
|
3
|
+
*
|
|
4
|
+
* @param sharedSecretBase64 - The shared secret as a Base64-encoded string.
|
|
5
|
+
* @param canonicalString - The canonical string to sign.
|
|
6
|
+
* @returns Lowercase hexadecimal signature string (64 characters).
|
|
7
|
+
*/
|
|
8
|
+
export declare function sign(sharedSecretBase64: string, canonicalString: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Verify a signature against a canonical string using constant-time comparison.
|
|
11
|
+
*
|
|
12
|
+
* @param sharedSecretBase64 - The shared secret as a Base64-encoded string.
|
|
13
|
+
* @param canonicalString - The canonical string that was signed.
|
|
14
|
+
* @param signature - The signature to verify (64-char lowercase hex).
|
|
15
|
+
* @returns True if the signature is valid.
|
|
16
|
+
*/
|
|
17
|
+
export declare function verify(sharedSecretBase64: string, canonicalString: string, signature: string): boolean;
|
|
18
|
+
//# sourceMappingURL=signing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signing.d.ts","sourceRoot":"","sources":["../src/signing.ts"],"names":[],"mappings":"AAgCA;;;;;;GAMG;AACH,wBAAgB,IAAI,CAClB,kBAAkB,EAAE,MAAM,EAC1B,eAAe,EAAE,MAAM,GACtB,MAAM,CAWR;AAED;;;;;;;GAOG;AACH,wBAAgB,MAAM,CACpB,kBAAkB,EAAE,MAAM,EAC1B,eAAe,EAAE,MAAM,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAWT"}
|
package/dist/signing.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
const BASE64_REGEX = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
3
|
+
/**
|
|
4
|
+
* Validate that a string is valid standard Base64.
|
|
5
|
+
* Throws a descriptive error if the input contains invalid characters.
|
|
6
|
+
*/
|
|
7
|
+
function validateBase64(input, label) {
|
|
8
|
+
const stripped = input.replace(/\s/g, "");
|
|
9
|
+
if (stripped.length === 0) {
|
|
10
|
+
throw new Error(`${label} is empty after stripping whitespace.`);
|
|
11
|
+
}
|
|
12
|
+
if (stripped.length % 4 !== 0) {
|
|
13
|
+
throw new Error(`${label} is not valid Base64. Length must be a multiple of 4 (got ${stripped.length}).`);
|
|
14
|
+
}
|
|
15
|
+
if (!BASE64_REGEX.test(stripped)) {
|
|
16
|
+
throw new Error(`${label} is not valid Base64. Contains characters outside the Base64 alphabet.`);
|
|
17
|
+
}
|
|
18
|
+
// Round-trip validation: decode then re-encode to catch padding mismatches
|
|
19
|
+
const decoded = Buffer.from(stripped, "base64");
|
|
20
|
+
if (decoded.toString("base64") !== stripped) {
|
|
21
|
+
throw new Error(`${label} is not valid Base64. Decoded/re-encoded value does not match input.`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Compute the HMAC-SHA256 signature of a canonical string.
|
|
26
|
+
*
|
|
27
|
+
* @param sharedSecretBase64 - The shared secret as a Base64-encoded string.
|
|
28
|
+
* @param canonicalString - The canonical string to sign.
|
|
29
|
+
* @returns Lowercase hexadecimal signature string (64 characters).
|
|
30
|
+
*/
|
|
31
|
+
export function sign(sharedSecretBase64, canonicalString) {
|
|
32
|
+
const stripped = sharedSecretBase64.replace(/\s/g, "");
|
|
33
|
+
validateBase64(stripped, "sharedSecretBase64");
|
|
34
|
+
const keyBytes = Buffer.from(stripped, "base64");
|
|
35
|
+
try {
|
|
36
|
+
const hmac = createHmac("sha256", keyBytes);
|
|
37
|
+
hmac.update(canonicalString, "utf8");
|
|
38
|
+
return hmac.digest("hex");
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
keyBytes.fill(0);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Verify a signature against a canonical string using constant-time comparison.
|
|
46
|
+
*
|
|
47
|
+
* @param sharedSecretBase64 - The shared secret as a Base64-encoded string.
|
|
48
|
+
* @param canonicalString - The canonical string that was signed.
|
|
49
|
+
* @param signature - The signature to verify (64-char lowercase hex).
|
|
50
|
+
* @returns True if the signature is valid.
|
|
51
|
+
*/
|
|
52
|
+
export function verify(sharedSecretBase64, canonicalString, signature) {
|
|
53
|
+
// sign() handles stripping and validation
|
|
54
|
+
const expected = sign(sharedSecretBase64, canonicalString);
|
|
55
|
+
const expectedBuf = Buffer.from(expected, "utf8");
|
|
56
|
+
const signatureBuf = Buffer.from(signature, "utf8");
|
|
57
|
+
if (expectedBuf.length !== signatureBuf.length) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return timingSafeEqual(expectedBuf, signatureBuf);
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=signing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signing.js","sourceRoot":"","sources":["../src/signing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE1D,MAAM,YAAY,GAAG,wBAAwB,CAAC;AAE9C;;;GAGG;AACH,SAAS,cAAc,CAAC,KAAa,EAAE,KAAa;IAClD,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC1C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,uCAAuC,CAAC,CAAC;IACnE,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CACb,GAAG,KAAK,6DAA6D,QAAQ,CAAC,MAAM,IAAI,CACzF,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CACb,GAAG,KAAK,wEAAwE,CACjF,CAAC;IACJ,CAAC;IACD,2EAA2E;IAC3E,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAChD,IAAI,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,QAAQ,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CACb,GAAG,KAAK,sEAAsE,CAC/E,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,IAAI,CAClB,kBAA0B,EAC1B,eAAuB;IAEvB,MAAM,QAAQ,GAAG,kBAAkB,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACvD,cAAc,CAAC,QAAQ,EAAE,oBAAoB,CAAC,CAAC;IAC/C,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACjD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC5C,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;QACrC,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5B,CAAC;YAAS,CAAC;QACT,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,MAAM,CACpB,kBAA0B,EAC1B,eAAuB,EACvB,SAAiB;IAEjB,0CAA0C;IAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,kBAAkB,EAAE,eAAe,CAAC,CAAC;IAC3D,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAClD,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAEpD,IAAI,WAAW,CAAC,MAAM,KAAK,YAAY,CAAC,MAAM,EAAE,CAAC;QAC/C,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,eAAe,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;AACpD,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { HmacConfig } from "./config.js";
|
|
2
|
+
/** Parameters for request validation. */
|
|
3
|
+
export interface ValidateRequestParams {
|
|
4
|
+
method: string;
|
|
5
|
+
path: string;
|
|
6
|
+
body: string;
|
|
7
|
+
signatureHeader: string | undefined | null;
|
|
8
|
+
timestampHeader: string | undefined | null;
|
|
9
|
+
requestHeaders?: Record<string, string>;
|
|
10
|
+
/** Override current timestamp for testing. */
|
|
11
|
+
currentTimestamp?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Validate an HMAC-signed request.
|
|
15
|
+
*
|
|
16
|
+
* Checks timestamp freshness and signature correctness.
|
|
17
|
+
*
|
|
18
|
+
* @throws {HmacValidationError} If validation fails.
|
|
19
|
+
*/
|
|
20
|
+
export declare function validateRequest(config: HmacConfig, params: ValidateRequestParams): void;
|
|
21
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAI9C,yCAAyC;AACzC,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;IAC3C,eAAe,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;IAC3C,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,8CAA8C;IAC9C,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,qBAAqB,GAC5B,IAAI,CAyEN"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { buildCanonicalString } from "./canonical.js";
|
|
2
|
+
import { HmacValidationError } from "./errors.js";
|
|
3
|
+
import { verify } from "./signing.js";
|
|
4
|
+
/**
|
|
5
|
+
* Validate an HMAC-signed request.
|
|
6
|
+
*
|
|
7
|
+
* Checks timestamp freshness and signature correctness.
|
|
8
|
+
*
|
|
9
|
+
* @throws {HmacValidationError} If validation fails.
|
|
10
|
+
*/
|
|
11
|
+
export function validateRequest(config, params) {
|
|
12
|
+
const { method, path, body, signatureHeader, timestampHeader, requestHeaders, currentTimestamp, } = params;
|
|
13
|
+
if (!signatureHeader) {
|
|
14
|
+
throw new HmacValidationError("missing_signature", "X-Harden-Signature header is required.");
|
|
15
|
+
}
|
|
16
|
+
if (!timestampHeader) {
|
|
17
|
+
throw new HmacValidationError("missing_timestamp", "X-Harden-Timestamp header is required.");
|
|
18
|
+
}
|
|
19
|
+
const trimmedTimestamp = timestampHeader.trim();
|
|
20
|
+
if (!/^-?\d+$/.test(trimmedTimestamp)) {
|
|
21
|
+
throw new HmacValidationError("invalid_timestamp", "X-Harden-Timestamp header is not a valid integer.");
|
|
22
|
+
}
|
|
23
|
+
const requestTimestamp = parseInt(trimmedTimestamp, 10);
|
|
24
|
+
if (isNaN(requestTimestamp)) {
|
|
25
|
+
throw new HmacValidationError("invalid_timestamp", "X-Harden-Timestamp header is not a valid integer.");
|
|
26
|
+
}
|
|
27
|
+
const now = currentTimestamp ?? Math.floor(Date.now() / 1000);
|
|
28
|
+
const delta = now - requestTimestamp;
|
|
29
|
+
if (delta > config.timestampToleranceSeconds) {
|
|
30
|
+
throw new HmacValidationError("timestamp_expired", `Request timestamp is ${delta} seconds in the past (tolerance: ${config.timestampToleranceSeconds}s).`);
|
|
31
|
+
}
|
|
32
|
+
if (delta < -config.timestampToleranceSeconds) {
|
|
33
|
+
throw new HmacValidationError("timestamp_out_of_range", `Request timestamp is ${-delta} seconds in the future (tolerance: ${config.timestampToleranceSeconds}s).`);
|
|
34
|
+
}
|
|
35
|
+
const canonicalString = buildCanonicalString({
|
|
36
|
+
method,
|
|
37
|
+
path,
|
|
38
|
+
body: body ?? "",
|
|
39
|
+
timestamp: requestTimestamp,
|
|
40
|
+
signedHeadersConfig: config.signedHeaders,
|
|
41
|
+
requestHeaders,
|
|
42
|
+
});
|
|
43
|
+
if (!verify(config.sharedSecretBase64, canonicalString, signatureHeader)) {
|
|
44
|
+
throw new HmacValidationError("signature_invalid", "HMAC signature does not match the expected value.");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=validation.js.map
|