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 +19 -0
- package/dist/crypto/index.d.ts +25 -1
- package/dist/crypto/index.js +52 -1
- package/package.json +14 -14
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
|
package/dist/crypto/index.d.ts
CHANGED
|
@@ -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 };
|
package/dist/crypto/index.js
CHANGED
|
@@ -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
|
+
"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
|
|
80
|
-
"hazo_logs": "^2.
|
|
81
|
-
"hazo_connect": "^3.
|
|
82
|
-
"hazo_jobs": "^0.
|
|
83
|
-
"hazo_files": "^3.1.
|
|
84
|
-
"hazo_ui": "^
|
|
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
|
|
107
|
-
"hazo_logs": "^2.
|
|
108
|
-
"hazo_connect": "^3.
|
|
109
|
-
"hazo_jobs": "^0.
|
|
110
|
-
"hazo_files": "^3.1.
|
|
111
|
-
"hazo_ui": "^
|
|
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": "^
|
|
118
|
+
"zod": "^4.1.12"
|
|
119
119
|
},
|
|
120
120
|
"author": "Pubs Abayasiri",
|
|
121
121
|
"license": "MIT"
|