@zedtech/zedkit-server 0.0.0 → 0.1.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 +15 -1
- package/dist/backoff.d.ts +7 -0
- package/dist/backoff.d.ts.map +1 -0
- package/dist/backoff.js +18 -0
- package/dist/backoff.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/pg-error.d.ts +7 -0
- package/dist/pg-error.d.ts.map +1 -0
- package/dist/pg-error.js +14 -0
- package/dist/pg-error.js.map +1 -0
- package/dist/rate-limit.d.ts +7 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +42 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/redaction.d.ts +2 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +69 -0
- package/dist/redaction.js.map +1 -0
- package/dist/webhook-signature.d.ts +19 -0
- package/dist/webhook-signature.d.ts.map +1 -0
- package/dist/webhook-signature.js +48 -0
- package/dist/webhook-signature.js.map +1 -0
- package/package.json +20 -2
package/README.md
CHANGED
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
# @zedtech/zedkit-server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@zedtech/zedkit-server)
|
|
4
|
+
[](https://github.com/zedtech-fr/zedkit/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
Harden any Node server in minutes: in-process rate limiting, secret/PII
|
|
7
|
+
redaction for logs, HKDF crypto and HTTP helpers. Original work by the
|
|
4
8
|
same author. Zero runtime dependencies.
|
|
5
9
|
|
|
10
|
+
## Install
|
|
11
|
+
|
|
6
12
|
```sh
|
|
7
13
|
pnpm add @zedtech/zedkit-server
|
|
8
14
|
```
|
|
9
15
|
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
10
18
|
```ts
|
|
11
19
|
import { redactValue } from "@zedtech/zedkit-server";
|
|
12
20
|
|
|
13
21
|
logger.info({ event: "login", user: redactValue(user) });
|
|
14
22
|
```
|
|
15
23
|
|
|
24
|
+
## When to use
|
|
25
|
+
|
|
26
|
+
Reach for this package when you rate-limit, redact logs, sign webhooks,
|
|
27
|
+
or read database error codes on a Node server. For typed errors and IDs
|
|
28
|
+
see `@zedtech/zedkit-core` instead.
|
|
29
|
+
|
|
16
30
|
## API
|
|
17
31
|
|
|
18
32
|
- `MemoryRateLimiter` — fixed-window, in-process rate limiting:
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry backoff: 2s x 3^(n-1) with +/-20% jitter, floored at 1s and
|
|
3
|
+
* capped at 1h. attemptNo is 1-based (the attempt that just failed).
|
|
4
|
+
* `random` is injectable so tests are deterministic.
|
|
5
|
+
*/
|
|
6
|
+
export declare function computeBackoffMs(attemptNo: number, random?: () => number): number;
|
|
7
|
+
//# sourceMappingURL=backoff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backoff.d.ts","sourceRoot":"","sources":["../src/backoff.ts"],"names":[],"mappings":"AACA;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,EACjB,MAAM,GAAE,MAAM,MAAoB,GACjC,MAAM,CAUR"}
|
package/dist/backoff.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Original work by the same author.
|
|
2
|
+
/**
|
|
3
|
+
* Retry backoff: 2s x 3^(n-1) with +/-20% jitter, floored at 1s and
|
|
4
|
+
* capped at 1h. attemptNo is 1-based (the attempt that just failed).
|
|
5
|
+
* `random` is injectable so tests are deterministic.
|
|
6
|
+
*/
|
|
7
|
+
export function computeBackoffMs(attemptNo, random = Math.random) {
|
|
8
|
+
if (!Number.isInteger(attemptNo) || attemptNo < 1) {
|
|
9
|
+
throw new Error(`attempt number must be a positive integer, got ${attemptNo}`);
|
|
10
|
+
}
|
|
11
|
+
const base = Math.min(2_000 * 3 ** (attemptNo - 1), 3_600_000);
|
|
12
|
+
const drawn = random();
|
|
13
|
+
// A broken custom RNG must not collapse delays into a retry storm.
|
|
14
|
+
const factor = Number.isFinite(drawn) ? 0.8 + 0.4 * drawn : 1;
|
|
15
|
+
const jittered = Math.round(base * factor);
|
|
16
|
+
return Math.max(1_000, Math.min(jittered, 3_600_000));
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=backoff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backoff.js","sourceRoot":"","sources":["../src/backoff.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC9B,SAAiB,EACjB,SAAuB,IAAI,CAAC,MAAM;IAElC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CAAC,kDAAkD,SAAS,EAAE,CAAC,CAAC;IACjF,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IAC/D,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC;IACvB,mEAAmE;IACnE,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9D,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;IAC3C,OAAO,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC;AACxD,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { pgErrorCode } from "./pg-error.js";
|
|
2
|
+
export { MemoryRateLimiter } from "./rate-limit.js";
|
|
3
|
+
export { redactValue } from "./redaction.js";
|
|
4
|
+
export { computeBackoffMs } from "./backoff.js";
|
|
5
|
+
export { verifyWebhookSignatureHeader, type VerifyWebhookOptions, webhookSignatureHeader, } from "./webhook-signature.js";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EACL,4BAA4B,EAC5B,KAAK,oBAAoB,EACzB,sBAAsB,GACvB,MAAM,wBAAwB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { pgErrorCode } from "./pg-error.js";
|
|
2
|
+
export { MemoryRateLimiter } from "./rate-limit.js";
|
|
3
|
+
export { redactValue } from "./redaction.js";
|
|
4
|
+
export { computeBackoffMs } from "./backoff.js";
|
|
5
|
+
export { verifyWebhookSignatureHeader, webhookSignatureHeader, } from "./webhook-signature.js";
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EACL,4BAA4B,EAE5B,sBAAsB,GACvB,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads the Postgres error code (`23505` unique, `23503` FK, ...) from a
|
|
3
|
+
* thrown value. Lets callers decide conflict-vs-bug without each
|
|
4
|
+
* keeping its own shape check.
|
|
5
|
+
*/
|
|
6
|
+
export declare function pgErrorCode(error: unknown): string | null;
|
|
7
|
+
//# sourceMappingURL=pg-error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pg-error.d.ts","sourceRoot":"","sources":["../src/pg-error.ts"],"names":[],"mappings":"AACA;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAMzD"}
|
package/dist/pg-error.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Original work by the same author.
|
|
2
|
+
/**
|
|
3
|
+
* Reads the Postgres error code (`23505` unique, `23503` FK, ...) from a
|
|
4
|
+
* thrown value. Lets callers decide conflict-vs-bug without each
|
|
5
|
+
* keeping its own shape check.
|
|
6
|
+
*/
|
|
7
|
+
export function pgErrorCode(error) {
|
|
8
|
+
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const code = error.code;
|
|
12
|
+
return typeof code === "string" ? code : null;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=pg-error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pg-error.js","sourceRoot":"","sources":["../src/pg-error.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,IAAI,KAAK,CAAC,EAAE,CAAC;QACtE,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,IAAI,GAAI,KAA2B,CAAC,IAAI,CAAC;IAC/C,OAAO,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AAChD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AASA,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuD;IAC/E,OAAO,CAAC,MAAM,CAAK;IAEnB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO;IAiBnD,OAAO,CAAC,OAAO;CAQhB"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Original work by the same author.
|
|
2
|
+
/**
|
|
3
|
+
* Fixed-window rate limiter, in-process. Correct for single-instance
|
|
4
|
+
* deployments; multi-instance enforcement needs a shared store behind the
|
|
5
|
+
* same allow() shape.
|
|
6
|
+
*/
|
|
7
|
+
const WINDOW_MS = 60_000;
|
|
8
|
+
const MAX_KEYS_BEFORE_GC = 10_000;
|
|
9
|
+
export class MemoryRateLimiter {
|
|
10
|
+
windows = new Map();
|
|
11
|
+
lastGc = 0;
|
|
12
|
+
allow(key, limitPerMinute) {
|
|
13
|
+
// Fail closed on misconfiguration: a non-positive limit denies.
|
|
14
|
+
if (!(limitPerMinute >= 1))
|
|
15
|
+
return false;
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
const current = this.windows.get(key);
|
|
18
|
+
if (!current || now - current.start >= WINDOW_MS) {
|
|
19
|
+
this.maybeGc(now);
|
|
20
|
+
this.windows.set(key, { start: now, count: 1 });
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
if (current.count >= limitPerMinute)
|
|
24
|
+
return false;
|
|
25
|
+
current.count += 1;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
// Drops expired windows, but at most once per window: past the key
|
|
29
|
+
// cap, every insert would otherwise pay a full-map scan.
|
|
30
|
+
maybeGc(now) {
|
|
31
|
+
if (this.windows.size <= MAX_KEYS_BEFORE_GC)
|
|
32
|
+
return;
|
|
33
|
+
if (now - this.lastGc < WINDOW_MS)
|
|
34
|
+
return;
|
|
35
|
+
this.lastGc = now;
|
|
36
|
+
for (const [k, v] of this.windows) {
|
|
37
|
+
if (now - v.start >= WINDOW_MS)
|
|
38
|
+
this.windows.delete(k);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC;;;;GAIG;AACH,MAAM,SAAS,GAAG,MAAM,CAAC;AACzB,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAElC,MAAM,OAAO,iBAAiB;IACX,OAAO,GAAG,IAAI,GAAG,EAA4C,CAAC;IACvE,MAAM,GAAG,CAAC,CAAC;IAEnB,KAAK,CAAC,GAAW,EAAE,cAAsB;QACvC,gEAAgE;QAChE,IAAI,CAAC,CAAC,cAAc,IAAI,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;QACzC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,CAAC,OAAO,IAAI,GAAG,GAAG,OAAO,CAAC,KAAK,IAAI,SAAS,EAAE,CAAC;YACjD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAClB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;YAChD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,OAAO,CAAC,KAAK,IAAI,cAAc;YAAE,OAAO,KAAK,CAAC;QAClD,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,mEAAmE;IACnE,yDAAyD;IACjD,OAAO,CAAC,GAAW;QACzB,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,kBAAkB;YAAE,OAAO;QACpD,IAAI,GAAG,GAAG,IAAI,CAAC,MAAM,GAAG,SAAS;YAAE,OAAO;QAC1C,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC;QAClB,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClC,IAAI,GAAG,GAAG,CAAC,CAAC,KAAK,IAAI,SAAS;gBAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redaction.d.ts","sourceRoot":"","sources":["../src/redaction.ts"],"names":[],"mappings":"AAkCA,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAEjE"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Original work by the same author. Generalized from a logger helper: the
|
|
2
|
+
// private key-material pattern was dropped; only generic secret shapes
|
|
3
|
+
// (secret-like keys, bearer/basic tokens) and long-value truncation remain.
|
|
4
|
+
/**
|
|
5
|
+
* Recursively replace credential-looking material with placeholders, so a
|
|
6
|
+
* structure is safe to log. Objects keep their shape and prototype; values
|
|
7
|
+
* under secret-like keys become "[redacted]", bearer/basic tokens become
|
|
8
|
+
* "<scheme> [redacted]", and cyclic references become "[circular]".
|
|
9
|
+
* Scrubbing applies to every field; truncation is volume control only (a
|
|
10
|
+
* cut secret still leaks its head), so human-text fields (`msg`,
|
|
11
|
+
* `message`, `stack`) are never truncated and everything else is bounded
|
|
12
|
+
* at 512 code points. Dates and regexes pass through untouched; Maps,
|
|
13
|
+
* Sets, and symbol keys are preserved.
|
|
14
|
+
*/
|
|
15
|
+
const SECRET_KEY_PATTERN = /(api[_-]?key|secret|token|password|passwd|signature|authorization|cookie|set-cookie|private[_-]?key|credentials)/i;
|
|
16
|
+
const AUTH_TOKEN_PATTERN = /\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi;
|
|
17
|
+
const HUMAN_TEXT_KEYS = new Set(["msg", "message", "stack"]);
|
|
18
|
+
const LONG_VALUE_MAX = 512;
|
|
19
|
+
function redactString(value, key) {
|
|
20
|
+
const scrubbed = value.replace(AUTH_TOKEN_PATTERN, (match) => `${match.split(/\s/, 1)[0]} [redacted]`);
|
|
21
|
+
if (key !== undefined && HUMAN_TEXT_KEYS.has(key.toLowerCase()))
|
|
22
|
+
return scrubbed;
|
|
23
|
+
// Count in code points so truncation never splits a surrogate pair.
|
|
24
|
+
const chars = Array.from(scrubbed);
|
|
25
|
+
if (chars.length > LONG_VALUE_MAX) {
|
|
26
|
+
return `${chars.slice(0, LONG_VALUE_MAX).join("")}…`;
|
|
27
|
+
}
|
|
28
|
+
return scrubbed;
|
|
29
|
+
}
|
|
30
|
+
export function redactValue(value, key) {
|
|
31
|
+
return redactInner(value, key, new Set());
|
|
32
|
+
}
|
|
33
|
+
function redactInner(value, key, seen) {
|
|
34
|
+
if (key !== undefined && SECRET_KEY_PATTERN.test(key)) {
|
|
35
|
+
return "[redacted]";
|
|
36
|
+
}
|
|
37
|
+
if (typeof value === "string") {
|
|
38
|
+
return redactString(value, key);
|
|
39
|
+
}
|
|
40
|
+
if (value === null || typeof value !== "object") {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
if (seen.has(value)) {
|
|
44
|
+
return "[circular]";
|
|
45
|
+
}
|
|
46
|
+
seen.add(value);
|
|
47
|
+
if (value instanceof Date || value instanceof RegExp) {
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
if (value instanceof Map) {
|
|
51
|
+
return new Map([...value].map(([k, v]) => [
|
|
52
|
+
k,
|
|
53
|
+
redactInner(v, typeof k === "string" ? k : undefined, seen),
|
|
54
|
+
]));
|
|
55
|
+
}
|
|
56
|
+
if (value instanceof Set) {
|
|
57
|
+
return new Set([...value].map((v) => redactInner(v, undefined, seen)));
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
return value.map((v) => redactInner(v, undefined, seen));
|
|
61
|
+
}
|
|
62
|
+
const source = value;
|
|
63
|
+
const out = Object.create(Object.getPrototypeOf(value));
|
|
64
|
+
for (const k of Reflect.ownKeys(source)) {
|
|
65
|
+
out[k] = redactInner(source[k], typeof k === "string" ? k : undefined, seen);
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=redaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redaction.js","sourceRoot":"","sources":["../src/redaction.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stripe-style signature header binding timestamp and exact body bytes:
|
|
3
|
+
* `t=<unix-seconds>,v1=<hex hmac-sha256(secret, "<ts>.<body>")>`.
|
|
4
|
+
* Receivers recompute over the raw bytes they received. Bring your own
|
|
5
|
+
* secret (32+ random bytes); version rotation stays in the consumer.
|
|
6
|
+
*/
|
|
7
|
+
export declare function webhookSignatureHeader(secret: Buffer, body: Buffer, timestamp: string): string;
|
|
8
|
+
export interface VerifyWebhookOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Reject signatures older than this, in seconds. Omit for no freshness
|
|
11
|
+
* check. Fold clock skew into the value; future timestamps are rejected.
|
|
12
|
+
*/
|
|
13
|
+
maxAgeSeconds?: number;
|
|
14
|
+
/** Clock for the freshness check (injectable for tests). Defaults to Date.now. */
|
|
15
|
+
now?: () => number;
|
|
16
|
+
}
|
|
17
|
+
/** Recompute the header over the received bytes and compare timing-safely. */
|
|
18
|
+
export declare function verifyWebhookSignatureHeader(secret: Buffer, body: Buffer, header: string | null | undefined, options?: VerifyWebhookOptions): boolean;
|
|
19
|
+
//# sourceMappingURL=webhook-signature.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-signature.d.ts","sourceRoot":"","sources":["../src/webhook-signature.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,MAAM,CAMR;AAED,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kFAAkF;IAClF,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,8EAA8E;AAC9E,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACjC,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAwBT"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Sign ported from production delivery code; verify mirrors it with a
|
|
2
|
+
// timing-safe compare. Original work by the same author.
|
|
3
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
4
|
+
/**
|
|
5
|
+
* Stripe-style signature header binding timestamp and exact body bytes:
|
|
6
|
+
* `t=<unix-seconds>,v1=<hex hmac-sha256(secret, "<ts>.<body>")>`.
|
|
7
|
+
* Receivers recompute over the raw bytes they received. Bring your own
|
|
8
|
+
* secret (32+ random bytes); version rotation stays in the consumer.
|
|
9
|
+
*/
|
|
10
|
+
export function webhookSignatureHeader(secret, body, timestamp) {
|
|
11
|
+
const mac = createHmac("sha256", secret)
|
|
12
|
+
.update(`${timestamp}.`, "utf8")
|
|
13
|
+
.update(body)
|
|
14
|
+
.digest("hex");
|
|
15
|
+
return `t=${timestamp},v1=${mac}`;
|
|
16
|
+
}
|
|
17
|
+
/** Recompute the header over the received bytes and compare timing-safely. */
|
|
18
|
+
export function verifyWebhookSignatureHeader(secret, body, header, options = {}) {
|
|
19
|
+
if (!header)
|
|
20
|
+
return false;
|
|
21
|
+
if (secret == null || body == null)
|
|
22
|
+
return false;
|
|
23
|
+
const match = /^t=(\d+),v1=([0-9a-f]+)$/.exec(header);
|
|
24
|
+
if (!match)
|
|
25
|
+
return false;
|
|
26
|
+
const timestamp = match[1];
|
|
27
|
+
if (timestamp === undefined)
|
|
28
|
+
return false;
|
|
29
|
+
const expected = webhookSignatureHeader(secret, body, timestamp);
|
|
30
|
+
const a = Buffer.from(header, "utf8");
|
|
31
|
+
const b = Buffer.from(expected, "utf8");
|
|
32
|
+
if (a.length !== b.length || !timingSafeEqual(a, b))
|
|
33
|
+
return false;
|
|
34
|
+
if (options.maxAgeSeconds !== undefined) {
|
|
35
|
+
// A NaN or negative maxAge (e.g. from config parsing) must fail closed,
|
|
36
|
+
// never silently disable replay protection.
|
|
37
|
+
if (!Number.isFinite(options.maxAgeSeconds) || options.maxAgeSeconds < 0) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const now = options.now ? options.now() : Date.now();
|
|
41
|
+
const ageMs = now - Number(timestamp) * 1000;
|
|
42
|
+
if (!Number.isFinite(ageMs) || ageMs < 0 || ageMs > options.maxAgeSeconds * 1000) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=webhook-signature.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-signature.js","sourceRoot":"","sources":["../src/webhook-signature.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,yDAAyD;AACzD,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CACpC,MAAc,EACd,IAAY,EACZ,SAAiB;IAEjB,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC;SACrC,MAAM,CAAC,GAAG,SAAS,GAAG,EAAE,MAAM,CAAC;SAC/B,MAAM,CAAC,IAAI,CAAC;SACZ,MAAM,CAAC,KAAK,CAAC,CAAC;IACjB,OAAO,KAAK,SAAS,OAAO,GAAG,EAAE,CAAC;AACpC,CAAC;AAYD,8EAA8E;AAC9E,MAAM,UAAU,4BAA4B,CAC1C,MAAc,EACd,IAAY,EACZ,MAAiC,EACjC,UAAgC,EAAE;IAElC,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC1B,IAAI,MAAM,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;QAAE,OAAO,KAAK,CAAC;IACjD,MAAM,KAAK,GAAG,0BAA0B,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACtD,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAC3B,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IAC1C,MAAM,QAAQ,GAAG,sBAAsB,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;IACjE,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACxC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAClE,IAAI,OAAO,CAAC,aAAa,KAAK,SAAS,EAAE,CAAC;QACxC,wEAAwE;QACxE,4CAA4C;QAC5C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,OAAO,CAAC,aAAa,GAAG,CAAC,EAAE,CAAC;YACzE,OAAO,KAAK,CAAC;QACf,CAAC;QACD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;QACrD,MAAM,KAAK,GAAG,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;QAC7C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,EAAE,CAAC;YACjF,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zedtech/zedkit-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"registry": "https://registry.npmjs.org/"
|
|
7
7
|
},
|
|
8
|
-
"description": "
|
|
8
|
+
"description": "Harden any Node server in minutes: in-process rate limiting, secret/PII redaction for logs, HKDF crypto and HTTP helpers. Original work by the same author.",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"nodejs",
|
|
11
|
+
"server",
|
|
12
|
+
"rate-limit",
|
|
13
|
+
"rate-limiting",
|
|
14
|
+
"redaction",
|
|
15
|
+
"pii",
|
|
16
|
+
"secrets",
|
|
17
|
+
"logging",
|
|
18
|
+
"hkdf",
|
|
19
|
+
"crypto",
|
|
20
|
+
"http",
|
|
21
|
+
"zero-dependency"
|
|
22
|
+
],
|
|
23
|
+
"homepage": "https://github.com/zedtech-fr/zedkit/tree/main/packages/server",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/zedtech-fr/zedkit/issues"
|
|
26
|
+
},
|
|
9
27
|
"license": "MIT",
|
|
10
28
|
"repository": {
|
|
11
29
|
"type": "git",
|