@zedtech/zedkit-server 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +50 -2
- 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 +40 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zedtech-fr
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,51 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @zedtech/zedkit-server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Server-side helpers with a stable public surface. Original work by the
|
|
4
|
+
same author. Zero runtime dependencies.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
pnpm add @zedtech/zedkit-server
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { redactValue } from "@zedtech/zedkit-server";
|
|
12
|
+
|
|
13
|
+
logger.info({ event: "login", user: redactValue(user) });
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## API
|
|
17
|
+
|
|
18
|
+
- `MemoryRateLimiter` — fixed-window, in-process rate limiting:
|
|
19
|
+
`allow(key, limitPerMinute)`. Correct for single-instance deployments;
|
|
20
|
+
multi-instance enforcement needs a shared store behind the same shape.
|
|
21
|
+
- `redactValue(value, key?)` — recursively replace credential-looking
|
|
22
|
+
material with placeholders before logging. Secret-like keys become
|
|
23
|
+
`"[redacted]"`, bearer/basic tokens become `"<scheme> [redacted]"`,
|
|
24
|
+
cyclic references become `"[circular]"`. Scrubbing applies to every
|
|
25
|
+
field; truncation is volume control only, so `msg`/`message`/`stack`
|
|
26
|
+
are never truncated and everything else is bounded at 512 code points.
|
|
27
|
+
Dates pass through; Maps, Sets, prototypes, and symbol keys are
|
|
28
|
+
preserved.
|
|
29
|
+
- `computeBackoffMs(attemptNo, random?)` — exponential retry backoff
|
|
30
|
+
(2s × 3^(n-1), ±20% jitter, 1s floor, 1h cap).
|
|
31
|
+
- `webhookSignatureHeader(secret, body, timestamp)` /
|
|
32
|
+
`verifyWebhookSignatureHeader(secret, body, header, options?)` —
|
|
33
|
+
Stripe-style `t=…,v1=…` HMAC signatures over exact body bytes, with an
|
|
34
|
+
optional freshness check. Bring your own secret.
|
|
35
|
+
- `pgErrorCode(error)` — read the Postgres error code (`23505`
|
|
36
|
+
unique, `23503` FK, ...) from a thrown value, or `null` when it
|
|
37
|
+
has none. Lets callers decide conflict-vs-bug.
|
|
38
|
+
|
|
39
|
+
Find helpers by need in [CATALOG.md](CATALOG.md).
|
|
40
|
+
|
|
41
|
+
## Install or copy?
|
|
42
|
+
|
|
43
|
+
These modules are small and dependency-free: source-copying the files
|
|
44
|
+
is a sane alternative to installing (MIT allows it). Install the package
|
|
45
|
+
for versioned updates.
|
|
46
|
+
|
|
47
|
+
## Verify
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
pnpm --filter @zedtech/zedkit-server typecheck && pnpm --filter @zedtech/zedkit-server lint && pnpm --filter @zedtech/zedkit-server build && pnpm --filter @zedtech/zedkit-server test
|
|
51
|
+
```
|
|
@@ -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,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zedtech/zedkit-server",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public",
|
|
6
|
+
"registry": "https://registry.npmjs.org/"
|
|
7
|
+
},
|
|
8
|
+
"description": "Server helpers: in-process rate limiting, secret redaction. Original work by the same author.",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/zedtech-fr/zedkit",
|
|
13
|
+
"directory": "packages/server"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"imports": {
|
|
18
|
+
"#*": "./src/*"
|
|
19
|
+
},
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "22.20.5",
|
|
34
|
+
"typescript": "5.9.3",
|
|
35
|
+
"vitest": "2.1.9"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc -p tsconfig.json",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"typecheck": "tsc --noEmit -p tsconfig.tests.json",
|
|
41
|
+
"lint": "eslint src tests"
|
|
42
|
+
}
|
|
6
43
|
}
|