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.
@@ -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": "0.5.0",
4
- "description": "Security & compliance primitives — SSRF-safe fetch, rate limiting, field-level encryption, GDPR orchestration. Four subpath modules, pick the ones you need.",
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
- "hazo_logs": "^1.1.0",
59
- "hazo_jobs": "^0.7.0",
60
- "hazo_files": "^1.4.7"
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
- "hazo_jobs": { "optional": true },
64
- "hazo_files": { "optional": true }
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": "^29.0.0",
68
- "@types/node": "^22.0.0",
69
- "jest": "^29.0.0",
70
- "ts-jest": "^29.0.0",
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"