hazo_secure 1.3.0 → 1.4.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/CHANGE_LOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Change Log
2
2
 
3
+ ## 1.4.0 — 2026-07-12
4
+
5
+ ### New: `hazo_secure/crypto` — generic HMAC sign/verify primitive
6
+
7
+ - **`signHmac(payload, secret)`** — signs an arbitrary UTF-8 payload string
8
+ with HMAC-SHA256, returning a `<payloadB64url>.<sigB64url>` token. No
9
+ expiry/claims logic — callers put their own JSON in the payload and handle
10
+ their own expiry/revocation.
11
+ - **`verifyHmac(token, secret)`** — constant-time verification via
12
+ `timingSafeEqual`, with a length pre-check on the signature against a
13
+ fixed constant before comparing, and a caught missing/empty-secret path
14
+ that fails closed with `{ valid: false, reason: "malformed" }` instead of
15
+ throwing. Returns `{ valid: true, payload }` with the decoded original
16
+ payload string on success.
17
+ - **`HmacVerifyResult`** type: `{ valid: true; payload: string } | { valid: false; reason: "malformed" | "signature_mismatch" }`.
18
+ - Dependency-free (only `node:crypto`); added to the existing `./crypto`
19
+ subpath export, no new subpath. Motivated by `hazo_embed`'s need for
20
+ domain-locked embed-key token signing, but kept fully generic.
21
+
3
22
  ## 1.3.0 — 2026-06-12
4
23
 
5
24
  ### New: `hazo_secure/secrets` — SecretsProvider
@@ -31,6 +31,30 @@ declare class HttpKeyProvider implements KeyProvider {
31
31
  clearCache(): void;
32
32
  }
33
33
 
34
+ /**
35
+ * Sign an arbitrary UTF-8 payload string with HMAC-SHA256, returning a
36
+ * `<payloadB64url>.<sigB64url>` token. The payload is opaque to this
37
+ * module — callers put their own JSON (or any string) in it and are
38
+ * responsible for their own expiry/revocation semantics; this primitive
39
+ * only proves the payload wasn't tampered with and was signed under `secret`.
40
+ */
41
+ declare function signHmac(payload: string, secret: string): string;
42
+ type HmacVerifyResult = {
43
+ valid: true;
44
+ payload: string;
45
+ } | {
46
+ valid: false;
47
+ reason: "malformed" | "signature_mismatch";
48
+ };
49
+ /**
50
+ * Verify a token produced by `signHmac`. Constant-time signature comparison
51
+ * via `timingSafeEqual`, with a length pre-check against a fixed constant
52
+ * (not secret-dependent) so short/garbage signatures never reach the
53
+ * timing-safe path. Fails closed — never throws, even for a missing/empty
54
+ * `secret` or a malformed token.
55
+ */
56
+ declare function verifyHmac(token: string, secret: string): HmacVerifyResult;
57
+
34
58
  /**
35
59
  * Source of symmetric keys for `encryptField` / `decryptField`. The current
36
60
  * key is used for encryption; `byId` resolves any key (current or rotated-out)
@@ -177,4 +201,4 @@ declare class LookupKeyProvider implements KeyProvider {
177
201
  * Convention: `<table>:<entityId>:<field>` — collision-free across tables. */
178
202
  declare function aadFor(table: string, entityId: string, field: string): string;
179
203
 
180
- export { CryptoError, type CryptoErrorCode, type EncryptedField, EnvKeyProvider, HttpKeyProvider, type HttpKeyProviderOptions, type KeyProvider, LookupKeyProvider, StaticKeyProvider, aadFor, decryptField, encryptField };
204
+ export { CryptoError, type CryptoErrorCode, type EncryptedField, EnvKeyProvider, type HmacVerifyResult, HttpKeyProvider, type HttpKeyProviderOptions, type KeyProvider, LookupKeyProvider, StaticKeyProvider, aadFor, decryptField, encryptField, signHmac, verifyHmac };
@@ -79,6 +79,55 @@ var HttpKeyProvider = class {
79
79
  }
80
80
  };
81
81
 
82
+ // src/crypto/hmac.ts
83
+ import { createHmac, timingSafeEqual } from "crypto";
84
+ var HMAC_B64URL_LEN = 43;
85
+ function b64url(input) {
86
+ const buf = typeof input === "string" ? Buffer.from(input, "utf8") : input;
87
+ return buf.toString("base64").replace(/=+$/g, "").replace(/\+/g, "-").replace(/\//g, "_");
88
+ }
89
+ function b64urlDecode(input) {
90
+ const pad = input.length % 4 === 0 ? "" : "=".repeat(4 - input.length % 4);
91
+ return Buffer.from(input.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64");
92
+ }
93
+ function sign(payloadB64, secret) {
94
+ if (!secret) throw new Error("hazo_secure/crypto: HMAC secret is not set");
95
+ return b64url(createHmac("sha256", secret).update(payloadB64).digest());
96
+ }
97
+ function signHmac(payload, secret) {
98
+ const payloadB64 = b64url(payload);
99
+ const sig = sign(payloadB64, secret);
100
+ return `${payloadB64}.${sig}`;
101
+ }
102
+ function verifyHmac(token, secret) {
103
+ if (!secret) return { valid: false, reason: "malformed" };
104
+ if (!token || typeof token !== "string") return { valid: false, reason: "malformed" };
105
+ const parts = token.split(".");
106
+ if (parts.length !== 2) return { valid: false, reason: "malformed" };
107
+ const [payloadB64, sig] = parts;
108
+ if (sig.length !== HMAC_B64URL_LEN) {
109
+ return { valid: false, reason: "signature_mismatch" };
110
+ }
111
+ let expectedSig;
112
+ try {
113
+ expectedSig = sign(payloadB64, secret);
114
+ } catch {
115
+ return { valid: false, reason: "malformed" };
116
+ }
117
+ const a = Buffer.from(sig);
118
+ const b = Buffer.from(expectedSig);
119
+ if (a.length !== b.length || !timingSafeEqual(a, b)) {
120
+ return { valid: false, reason: "signature_mismatch" };
121
+ }
122
+ let payload;
123
+ try {
124
+ payload = b64urlDecode(payloadB64).toString("utf8");
125
+ } catch {
126
+ return { valid: false, reason: "malformed" };
127
+ }
128
+ return { valid: true, payload };
129
+ }
130
+
82
131
  // src/crypto/index.ts
83
132
  var CryptoError = class extends HazoError {
84
133
  constructor(message, code) {
@@ -267,5 +316,7 @@ export {
267
316
  StaticKeyProvider,
268
317
  aadFor,
269
318
  decryptField,
270
- encryptField
319
+ encryptField,
320
+ signHmac,
321
+ verifyHmac
271
322
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_secure",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Security & compliance primitives — SSRF-safe fetch, rate limiting, field-level encryption, GDPR orchestration, CSRF. Five subpath modules, pick the ones you need.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -76,12 +76,12 @@
76
76
  "wink-eng-lite-web-model": "^1.7.0"
77
77
  },
78
78
  "peerDependencies": {
79
- "hazo_core": "^1.1.0",
80
- "hazo_logs": "^2.0.3",
81
- "hazo_connect": "^3.5.1",
82
- "hazo_jobs": "^0.12.0",
83
- "hazo_files": "^3.1.0",
84
- "hazo_ui": "^3.5.0"
79
+ "hazo_core": "^1.2.1",
80
+ "hazo_logs": "^2.1.1",
81
+ "hazo_connect": "^3.9.2",
82
+ "hazo_jobs": "^0.15.0",
83
+ "hazo_files": "^3.1.1",
84
+ "hazo_ui": "^6.1.0"
85
85
  },
86
86
  "peerDependenciesMeta": {
87
87
  "hazo_logs": {
@@ -103,19 +103,19 @@
103
103
  "devDependencies": {
104
104
  "@types/jest": "^30.0.0",
105
105
  "@types/node": "^22.10.0",
106
- "hazo_core": "^1.1.0",
107
- "hazo_logs": "^2.0.3",
108
- "hazo_connect": "^3.5.1",
109
- "hazo_jobs": "^0.12.0",
110
- "hazo_files": "^3.1.0",
111
- "hazo_ui": "^3.5.0",
106
+ "hazo_core": "^1.2.1",
107
+ "hazo_logs": "^2.1.1",
108
+ "hazo_connect": "^3.9.2",
109
+ "hazo_jobs": "^0.15.0",
110
+ "hazo_files": "^3.1.1",
111
+ "hazo_ui": "^6.1.0",
112
112
  "jest": "^30.2.0",
113
113
  "ts-jest": "^29.4.5",
114
114
  "tsup": "^8.5.1",
115
115
  "typescript": "^5.7.2",
116
116
  "wink-nlp": "^2.3.1",
117
117
  "wink-eng-lite-web-model": "^1.7.0",
118
- "zod": "^3.23.8"
118
+ "zod": "^4.1.12"
119
119
  },
120
120
  "author": "Pubs Abayasiri",
121
121
  "license": "MIT"