@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 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
- # Temporary Holding Version
1
+ # @zedtech/zedkit-server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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"}
@@ -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"}
@@ -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"}
@@ -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,7 @@
1
+ export declare class MemoryRateLimiter {
2
+ private readonly windows;
3
+ private lastGc;
4
+ allow(key: string, limitPerMinute: number): boolean;
5
+ private maybeGc;
6
+ }
7
+ //# sourceMappingURL=rate-limit.d.ts.map
@@ -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,2 @@
1
+ export declare function redactValue(value: unknown, key?: string): unknown;
2
+ //# sourceMappingURL=redaction.d.ts.map
@@ -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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
  }