hazo_secure 0.5.0 → 1.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/CHANGE_LOG.md +171 -0
- package/README.md +87 -2
- package/SETUP_CHECKLIST.md +68 -0
- package/config/hazo_secure_config.ini.sample +71 -0
- package/db_setup_postgres.sql +20 -0
- package/db_setup_sqlite.sql +18 -0
- package/dist/crypto/index.d.ts +44 -4
- package/dist/crypto/index.js +94 -9
- package/dist/csrf/index.d.ts +10 -1
- package/dist/csrf/index.js +9 -4
- package/dist/fetch/index.d.ts +29 -2
- package/dist/fetch/index.js +69 -8
- package/dist/gdpr/index.d.ts +49 -2
- package/dist/gdpr/index.js +94 -9
- package/dist/index.d.ts +40 -7
- package/dist/index.js +38 -9
- package/dist/ratelimit/index.d.ts +55 -3
- package/dist/ratelimit/index.js +98 -2
- package/migrations/001_hazo_rl_buckets.sql +26 -0
- package/package.json +45 -16
package/dist/ratelimit/index.js
CHANGED
|
@@ -21,7 +21,7 @@ var MemoryRateLimitStore = class {
|
|
|
21
21
|
}
|
|
22
22
|
return { ...e.state };
|
|
23
23
|
}
|
|
24
|
-
async increment(key, windowMs) {
|
|
24
|
+
async increment(key, windowMs, _max) {
|
|
25
25
|
const t = this.now();
|
|
26
26
|
const existing = this.map.get(key);
|
|
27
27
|
if (!existing || existing.expiresAt <= t) {
|
|
@@ -57,6 +57,101 @@ function getDefaultMemoryStore() {
|
|
|
57
57
|
return defaultStore;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// src/ratelimit/store-connect.ts
|
|
61
|
+
import { HazoConfigError, HazoValidationError, optional_import } from "hazo_core";
|
|
62
|
+
var ConnectRateLimitStore = class {
|
|
63
|
+
#getSvc;
|
|
64
|
+
#now;
|
|
65
|
+
constructor(opts) {
|
|
66
|
+
if (opts._svc) {
|
|
67
|
+
this.#getSvc = opts._svc;
|
|
68
|
+
} else if (opts.getAdapter) {
|
|
69
|
+
const getAdapter = opts.getAdapter;
|
|
70
|
+
this.#getSvc = async () => {
|
|
71
|
+
const mod = await optional_import(
|
|
72
|
+
"hazo_connect/server"
|
|
73
|
+
);
|
|
74
|
+
if (!mod) {
|
|
75
|
+
throw new HazoConfigError({
|
|
76
|
+
code: "HAZO_SECURE_RATELIMIT_CONNECT_MISSING",
|
|
77
|
+
pkg: "hazo_secure",
|
|
78
|
+
message: "ConnectRateLimitStore requires hazo_connect to be installed. Install it or pass `_svc` directly."
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const adapter = await getAdapter();
|
|
82
|
+
return mod.createCrudService(adapter, "hazo_rl_buckets", {
|
|
83
|
+
autoId: { enabled: false, column: "id" }
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
} else {
|
|
87
|
+
throw new HazoConfigError({
|
|
88
|
+
code: "HAZO_SECURE_RATELIMIT_INVALID_STORE_OPTIONS",
|
|
89
|
+
pkg: "hazo_secure",
|
|
90
|
+
message: "ConnectRateLimitStore: provide getAdapter or _svc"
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
this.#now = opts.now ?? Date.now;
|
|
94
|
+
}
|
|
95
|
+
async svc() {
|
|
96
|
+
return this.#getSvc();
|
|
97
|
+
}
|
|
98
|
+
async get(key) {
|
|
99
|
+
const svc = await this.svc();
|
|
100
|
+
const row = await svc.findOneBy({ id: key });
|
|
101
|
+
if (!row) return null;
|
|
102
|
+
const now = this.#now();
|
|
103
|
+
const elapsedMs = now - new Date(row.last_refill_at).getTime();
|
|
104
|
+
const tokens = Math.min(row.max_tokens, row.tokens + elapsedMs * row.refill_rate_per_ms);
|
|
105
|
+
const storedTokens = Math.max(0, tokens);
|
|
106
|
+
const resetAt = now + Math.ceil(Math.max(0, 1 - storedTokens) / row.refill_rate_per_ms);
|
|
107
|
+
return { count: Math.max(0, row.max_tokens - tokens), resetAt };
|
|
108
|
+
}
|
|
109
|
+
async increment(key, windowMs, max) {
|
|
110
|
+
if (max === void 0) {
|
|
111
|
+
throw new HazoValidationError({
|
|
112
|
+
code: "HAZO_SECURE_RATELIMIT_MISSING_MAX",
|
|
113
|
+
pkg: "hazo_secure",
|
|
114
|
+
message: "ConnectRateLimitStore requires max in increment(). Ensure createRateLimiter / checkRateLimit receives { max }."
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const svc = await this.svc();
|
|
118
|
+
const now = this.#now();
|
|
119
|
+
const refillRatePerMs = max / windowMs;
|
|
120
|
+
const row = await svc.findOneBy({ id: key });
|
|
121
|
+
let tokensAfter;
|
|
122
|
+
if (!row) {
|
|
123
|
+
tokensAfter = max - 1;
|
|
124
|
+
await svc.insert({
|
|
125
|
+
id: key,
|
|
126
|
+
tokens: Math.max(0, tokensAfter),
|
|
127
|
+
last_refill_at: new Date(now).toISOString(),
|
|
128
|
+
max_tokens: max,
|
|
129
|
+
refill_rate_per_ms: refillRatePerMs
|
|
130
|
+
});
|
|
131
|
+
} else {
|
|
132
|
+
const elapsedMs = now - new Date(row.last_refill_at).getTime();
|
|
133
|
+
const refilledTokens = Math.min(max, row.tokens + elapsedMs * refillRatePerMs);
|
|
134
|
+
tokensAfter = refilledTokens - 1;
|
|
135
|
+
await svc.updateById(key, {
|
|
136
|
+
tokens: Math.max(0, tokensAfter),
|
|
137
|
+
last_refill_at: new Date(now).toISOString(),
|
|
138
|
+
max_tokens: max,
|
|
139
|
+
refill_rate_per_ms: refillRatePerMs
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const count = max - tokensAfter;
|
|
143
|
+
const storedTokens = Math.max(0, tokensAfter);
|
|
144
|
+
const refillNeeded = Math.max(0, 1 - storedTokens);
|
|
145
|
+
const resetAt = now + Math.ceil(refillNeeded / refillRatePerMs);
|
|
146
|
+
return { count, resetAt };
|
|
147
|
+
}
|
|
148
|
+
async reset(key) {
|
|
149
|
+
const svc = await this.svc();
|
|
150
|
+
const row = await svc.findOneBy({ id: key });
|
|
151
|
+
if (row) await svc.deleteById(key);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
60
155
|
// src/ratelimit/index.ts
|
|
61
156
|
function defaultKeyResolver(req) {
|
|
62
157
|
const headers = req.headers;
|
|
@@ -73,7 +168,7 @@ function createRateLimiter(opts) {
|
|
|
73
168
|
const keyResolver = opts.keyResolver ?? defaultKeyResolver;
|
|
74
169
|
const now = opts.now ?? Date.now;
|
|
75
170
|
async function checkKey(key) {
|
|
76
|
-
const state = await store.increment(key, opts.windowMs);
|
|
171
|
+
const state = await store.increment(key, opts.windowMs, opts.max);
|
|
77
172
|
const allowed = state.count <= opts.max;
|
|
78
173
|
const remaining = Math.max(0, opts.max - state.count);
|
|
79
174
|
const retryAfterSeconds = Math.max(0, Math.ceil((state.resetAt - now()) / 1e3));
|
|
@@ -113,6 +208,7 @@ function withRateLimit(handler, opts) {
|
|
|
113
208
|
return fn;
|
|
114
209
|
}
|
|
115
210
|
export {
|
|
211
|
+
ConnectRateLimitStore,
|
|
116
212
|
MemoryRateLimitStore,
|
|
117
213
|
checkRateLimit,
|
|
118
214
|
createRateLimiter,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
-- hazo_secure rate-limit token bucket table
|
|
2
|
+
-- Supports both PostgreSQL and SQLite
|
|
3
|
+
--
|
|
4
|
+
-- Column: id — rate-limit key (e.g. "1.2.3.4:/api/auth/login")
|
|
5
|
+
-- Column: tokens — current token count (REAL so partial tokens are tracked)
|
|
6
|
+
-- Column: last_refill_at — when tokens was last updated
|
|
7
|
+
-- Column: max_tokens — cap configured for this key (stored for introspection)
|
|
8
|
+
-- Column: refill_rate_per_ms — tokens added per millisecond
|
|
9
|
+
--
|
|
10
|
+
-- SQLite version (use this for local dev):
|
|
11
|
+
-- CREATE TABLE IF NOT EXISTS hazo_rl_buckets (
|
|
12
|
+
-- id TEXT NOT NULL PRIMARY KEY,
|
|
13
|
+
-- tokens REAL NOT NULL,
|
|
14
|
+
-- last_refill_at TEXT NOT NULL,
|
|
15
|
+
-- max_tokens REAL NOT NULL,
|
|
16
|
+
-- refill_rate_per_ms REAL NOT NULL
|
|
17
|
+
-- );
|
|
18
|
+
--
|
|
19
|
+
-- PostgreSQL version (active):
|
|
20
|
+
CREATE TABLE IF NOT EXISTS hazo_rl_buckets (
|
|
21
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
22
|
+
tokens DOUBLE PRECISION NOT NULL,
|
|
23
|
+
last_refill_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
24
|
+
max_tokens DOUBLE PRECISION NOT NULL,
|
|
25
|
+
refill_rate_per_ms DOUBLE PRECISION NOT NULL
|
|
26
|
+
);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hazo_secure",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Security & compliance primitives — SSRF-safe fetch, rate limiting, field-level encryption, GDPR orchestration.
|
|
3
|
+
"version": "1.1.0",
|
|
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",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -40,8 +40,14 @@
|
|
|
40
40
|
},
|
|
41
41
|
"files": [
|
|
42
42
|
"dist",
|
|
43
|
+
"config/hazo_secure_config.ini.sample",
|
|
44
|
+
"db_setup_postgres.sql",
|
|
45
|
+
"db_setup_sqlite.sql",
|
|
46
|
+
"migrations",
|
|
43
47
|
"LICENSE",
|
|
44
|
-
"README.md"
|
|
48
|
+
"README.md",
|
|
49
|
+
"SETUP_CHECKLIST.md",
|
|
50
|
+
"CHANGE_LOG.md"
|
|
45
51
|
],
|
|
46
52
|
"scripts": {
|
|
47
53
|
"build": "tsup",
|
|
@@ -54,25 +60,48 @@
|
|
|
54
60
|
"install:test-app": "cd test-app && npm install",
|
|
55
61
|
"prepublishOnly": "npm run build"
|
|
56
62
|
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"undici": "^7.3.0"
|
|
65
|
+
},
|
|
57
66
|
"peerDependencies": {
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
67
|
+
"hazo_core": "^1.0.0",
|
|
68
|
+
"hazo_logs": "^2.0.0",
|
|
69
|
+
"hazo_connect": "^3.0.0",
|
|
70
|
+
"hazo_jobs": "^0.12.0",
|
|
71
|
+
"hazo_files": "^3.0.0",
|
|
72
|
+
"hazo_ui": "^3.1.0"
|
|
61
73
|
},
|
|
62
74
|
"peerDependenciesMeta": {
|
|
63
|
-
"
|
|
64
|
-
|
|
75
|
+
"hazo_logs": {
|
|
76
|
+
"optional": true
|
|
77
|
+
},
|
|
78
|
+
"hazo_connect": {
|
|
79
|
+
"optional": true
|
|
80
|
+
},
|
|
81
|
+
"hazo_jobs": {
|
|
82
|
+
"optional": true
|
|
83
|
+
},
|
|
84
|
+
"hazo_files": {
|
|
85
|
+
"optional": true
|
|
86
|
+
},
|
|
87
|
+
"hazo_ui": {
|
|
88
|
+
"optional": true
|
|
89
|
+
}
|
|
65
90
|
},
|
|
66
91
|
"devDependencies": {
|
|
67
|
-
"@types/jest": "^
|
|
68
|
-
"@types/node": "^22.
|
|
69
|
-
"
|
|
70
|
-
"
|
|
92
|
+
"@types/jest": "^30.0.0",
|
|
93
|
+
"@types/node": "^22.10.0",
|
|
94
|
+
"hazo_core": "^1.0.0",
|
|
95
|
+
"hazo_logs": "^2.0.1",
|
|
96
|
+
"hazo_connect": "^3.0.0",
|
|
97
|
+
"hazo_jobs": "^0.12.0",
|
|
98
|
+
"hazo_files": "^3.0.0",
|
|
99
|
+
"hazo_ui": "^3.1.0",
|
|
100
|
+
"jest": "^30.2.0",
|
|
101
|
+
"ts-jest": "^29.4.5",
|
|
71
102
|
"tsup": "^8.5.1",
|
|
72
|
-
"typescript": "^5.7.2"
|
|
73
|
-
|
|
74
|
-
"engines": {
|
|
75
|
-
"node": ">=18.0.0"
|
|
103
|
+
"typescript": "^5.7.2",
|
|
104
|
+
"zod": "^3.23.8"
|
|
76
105
|
},
|
|
77
106
|
"author": "Pubs Abayasiri",
|
|
78
107
|
"license": "MIT"
|