hazo_secure 1.0.1 → 1.3.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.
@@ -1,8 +1,13 @@
1
+ import { HazoError } from 'hazo_core';
1
2
  import { Agent } from 'undici';
2
3
 
3
4
  /**
4
- * Validates a resolved IP at connect time. Throws if private/reserved.
5
- * Exported so it can be unit-tested directly.
5
+ * Validates a resolved IP at connect time. Throws `SafeFetchError("private_ip_blocked")`
6
+ * if private/reserved. Exported so it can be unit-tested directly.
7
+ *
8
+ * Carries the legacy `SSRF_PRIVATE_IP` ErrnoException-style code on the error
9
+ * for older consumers that grepped for it. New code should branch on the
10
+ * SafeFetchError discriminator instead.
6
11
  */
7
12
  declare function validateConnectIp(ip: string): void;
8
13
  /**
@@ -31,7 +36,13 @@ interface SafeFetchOptions extends Omit<RequestInit, "redirect"> {
31
36
  deps?: SafeFetchDeps;
32
37
  }
33
38
  type SafeFetchErrorCode = "invalid_url" | "protocol_not_allowed" | "host_not_allowed" | "dns_failed" | "private_ip_blocked" | "redirect_limit" | "redirect_loop" | "timeout";
34
- declare class SafeFetchError extends Error {
39
+ /**
40
+ * Typed error raised by `safeFetch`. Extends `HazoError` so it participates
41
+ * in the workspace's correlation-ID auto-attach + envelope JSON shape, while
42
+ * preserving the legacy `(code, message)` constructor + `err.code` short
43
+ * discriminator that 1.0.x consumers already check.
44
+ */
45
+ declare class SafeFetchError extends HazoError {
35
46
  readonly code: SafeFetchErrorCode;
36
47
  constructor(code: SafeFetchErrorCode, message: string);
37
48
  }
@@ -1,6 +1,7 @@
1
1
  // src/fetch/index.ts
2
2
  import { lookup as lookup2 } from "dns/promises";
3
3
  import { fetch as undiciFetch } from "undici";
4
+ import { HazoError, REQUEST_ID_HEADER, getCorrelationId } from "hazo_core";
4
5
 
5
6
  // src/fetch/cidr.ts
6
7
  import { isIP } from "net";
@@ -76,10 +77,12 @@ import { Agent } from "undici";
76
77
  import * as dns from "dns";
77
78
  function validateConnectIp(ip) {
78
79
  if (isPrivateIp(ip)) {
79
- const err = new Error(
80
+ const err = new SafeFetchError(
81
+ "private_ip_blocked",
80
82
  `SSRF: resolved IP ${ip} is a private/reserved address \u2014 blocked at connect time`
81
83
  );
82
- err.code = "SSRF_PRIVATE_IP";
84
+ err.code = "private_ip_blocked";
85
+ err.ssrfCode = "SSRF_PRIVATE_IP";
83
86
  throw err;
84
87
  }
85
88
  }
@@ -103,13 +106,28 @@ function createSecureDispatcher() {
103
106
 
104
107
  // src/fetch/index.ts
105
108
  var secureDispatcher = createSecureDispatcher();
106
- var SafeFetchError = class extends Error {
109
+ var SAFE_FETCH_STATUS_MAP = {
110
+ invalid_url: { httpStatus: 400, retryable: false },
111
+ protocol_not_allowed: { httpStatus: 400, retryable: false },
112
+ host_not_allowed: { httpStatus: 403, retryable: false },
113
+ dns_failed: { httpStatus: 502, retryable: true },
114
+ private_ip_blocked: { httpStatus: 403, retryable: false },
115
+ redirect_limit: { httpStatus: 502, retryable: false },
116
+ redirect_loop: { httpStatus: 502, retryable: false },
117
+ timeout: { httpStatus: 504, retryable: true }
118
+ };
119
+ var SafeFetchError = class extends HazoError {
107
120
  constructor(code, message) {
108
- super(message);
109
- this.code = code;
121
+ const { httpStatus, retryable } = SAFE_FETCH_STATUS_MAP[code];
122
+ super({
123
+ code,
124
+ pkg: "hazo_secure",
125
+ message,
126
+ httpStatus,
127
+ retryable
128
+ });
110
129
  this.name = "SafeFetchError";
111
130
  }
112
- code;
113
131
  };
114
132
  var DEFAULT_TIMEOUT_MS = 1e4;
115
133
  var DEFAULT_MAX_REDIRECTS = 3;
@@ -171,6 +189,14 @@ async function safeFetch(input, options) {
171
189
  } catch {
172
190
  throw new SafeFetchError("invalid_url", `Could not parse URL: ${String(input)}`);
173
191
  }
192
+ const correlationId = getCorrelationId();
193
+ if (correlationId) {
194
+ const existingHeaders = new Headers(init.headers);
195
+ if (!existingHeaders.has(REQUEST_ID_HEADER)) {
196
+ existingHeaders.set(REQUEST_ID_HEADER, correlationId);
197
+ init.headers = existingHeaders;
198
+ }
199
+ }
174
200
  const visited = /* @__PURE__ */ new Set();
175
201
  let hopsRemaining = maxRedirects;
176
202
  while (true) {
@@ -1,3 +1,5 @@
1
+ import { HazoError } from 'hazo_core';
2
+
1
3
  interface ExporterContext {
2
4
  userId: string;
3
5
  locale?: string;
@@ -43,9 +45,16 @@ interface GdprRegistryOptions {
43
45
  */
44
46
  auditIntent?: AuditIntentFn;
45
47
  }
46
- declare class GdprRegistryError extends Error {
47
- readonly code: 'incomplete_registry';
48
- constructor(message: string, code: 'incomplete_registry');
48
+ type GdprRegistryErrorCode = 'incomplete_registry';
49
+ /**
50
+ * Typed error raised by `/gdpr`. Extends `HazoError` so registry-incomplete
51
+ * gaps surface through the workspace's correlation-ID + envelope shape, while
52
+ * preserving the legacy `(message, code)` constructor + `err.code` short
53
+ * discriminator that 1.0.x consumers already check.
54
+ */
55
+ declare class GdprRegistryError extends HazoError {
56
+ readonly code: GdprRegistryErrorCode;
57
+ constructor(message: string, code: GdprRegistryErrorCode);
49
58
  }
50
59
  interface GdprRegistry {
51
60
  registerExporter(exporter: DomainExporter): void;
@@ -59,4 +68,4 @@ interface GdprRegistry {
59
68
  }
60
69
  declare function createGdprRegistry(opts?: GdprRegistryOptions): GdprRegistry;
61
70
 
62
- export { type AuditIntentFn, type DomainAnonymiser, type DomainExporter, type DryRunExportResult, type ExporterContext, type GdprRegistry, GdprRegistryError, type GdprRegistryOptions, type UserExportResult, type ValidateRegistryOptions, createGdprRegistry };
71
+ export { type AuditIntentFn, type DomainAnonymiser, type DomainExporter, type DryRunExportResult, type ExporterContext, type GdprRegistry, GdprRegistryError, type GdprRegistryErrorCode, type GdprRegistryOptions, type UserExportResult, type ValidateRegistryOptions, createGdprRegistry };
@@ -1,11 +1,16 @@
1
1
  // src/gdpr/index.ts
2
- var GdprRegistryError = class extends Error {
2
+ import { HazoError } from "hazo_core";
3
+ var GdprRegistryError = class extends HazoError {
3
4
  constructor(message, code) {
4
- super(message);
5
- this.code = code;
5
+ super({
6
+ code,
7
+ pkg: "hazo_secure",
8
+ message,
9
+ httpStatus: 500,
10
+ retryable: false
11
+ });
6
12
  this.name = "GdprRegistryError";
7
13
  }
8
- code;
9
14
  };
10
15
  function createGdprRegistry(opts = {}) {
11
16
  const exporters = [];
package/dist/index.d.ts CHANGED
@@ -1,10 +1,43 @@
1
+ /**
2
+ * Logger utility for hazo_secure.
3
+ *
4
+ * Default: delegates to hazo_core's createLogger('hazo_secure'), which
5
+ * auto-injects correlationId, env, and writes structured JSON via hazo_logs.
6
+ * Consumers can still override with set_logger() — useful for tests, or for
7
+ * apps that want to route hazo_secure logs through their own sink.
8
+ */
1
9
  interface Logger {
2
- debug(msg: string, meta?: Record<string, unknown>): void;
3
- info(msg: string, meta?: Record<string, unknown>): void;
4
- warn(msg: string, meta?: Record<string, unknown>): void;
5
- error(msg: string, meta?: Record<string, unknown>): void;
10
+ info: (message: string, data?: Record<string, unknown>) => void;
11
+ debug: (message: string, data?: Record<string, unknown>) => void;
12
+ warn: (message: string, data?: Record<string, unknown>) => void;
13
+ error: (message: string, data?: Record<string, unknown>) => void;
6
14
  }
7
- declare const NoopLogger: Logger;
8
- declare const PACKAGE_VERSION = "0.1.0";
15
+ /**
16
+ * Set the logger instance used throughout hazo_secure.
17
+ * Pass `undefined` to reset to the hazo_core default.
18
+ */
19
+ declare function set_logger(logger: Logger | undefined): void;
20
+ /**
21
+ * Get the current logger instance — defaults to hazo_core's createLogger
22
+ * on first access if no consumer override has been set.
23
+ */
24
+ declare function get_logger(): Logger;
25
+ /**
26
+ * Re-exported for backwards compatibility with hazo_secure < 1.1.0, which
27
+ * shipped a `NoopLogger` constant for consumers that wanted to silence
28
+ * package logs. Prefer `set_logger(noop_logger)` going forward.
29
+ */
30
+ declare const noop_logger: Logger;
9
31
 
10
- export { type Logger, NoopLogger, PACKAGE_VERSION };
32
+ /**
33
+ * Root entry — shared types only. Functional surface lives in the subpath
34
+ * modules (`hazo_secure/fetch`, `/ratelimit`, `/crypto`, `/gdpr`, `/csrf`).
35
+ *
36
+ * Logger helpers re-exported here so consumers can override the in-package
37
+ * logger without reaching into a subpath: `import { set_logger } from
38
+ * 'hazo_secure'`.
39
+ */
40
+
41
+ declare const PACKAGE_VERSION = "1.1.0";
42
+
43
+ export { type Logger, noop_logger as NoopLogger, PACKAGE_VERSION, get_logger, noop_logger, set_logger };
package/dist/index.js CHANGED
@@ -1,16 +1,45 @@
1
- // src/index.ts
2
- var NoopLogger = {
3
- debug() {
1
+ // src/utils/logger.ts
2
+ import { createLogger } from "hazo_core";
3
+ var console_logger = {
4
+ info: (m, d) => d ? console.log(`[hazo_secure] ${m}`, d) : console.log(`[hazo_secure] ${m}`),
5
+ debug: (m, d) => d ? console.debug(`[hazo_secure] ${m}`, d) : console.debug(`[hazo_secure] ${m}`),
6
+ warn: (m, d) => d ? console.warn(`[hazo_secure] ${m}`, d) : console.warn(`[hazo_secure] ${m}`),
7
+ error: (m, d) => d ? console.error(`[hazo_secure] ${m}`, d) : console.error(`[hazo_secure] ${m}`)
8
+ };
9
+ function build_default_logger() {
10
+ try {
11
+ return createLogger("hazo_secure");
12
+ } catch {
13
+ return console_logger;
14
+ }
15
+ }
16
+ var current_logger = null;
17
+ function set_logger(logger) {
18
+ current_logger = logger ?? null;
19
+ }
20
+ function get_logger() {
21
+ if (!current_logger) {
22
+ current_logger = build_default_logger();
23
+ }
24
+ return current_logger;
25
+ }
26
+ var noop_logger = {
27
+ debug: () => {
4
28
  },
5
- info() {
29
+ info: () => {
6
30
  },
7
- warn() {
31
+ warn: () => {
8
32
  },
9
- error() {
33
+ error: () => {
10
34
  }
11
35
  };
12
- var PACKAGE_VERSION = "0.1.0";
36
+
37
+ // src/index.ts
38
+ var PACKAGE_VERSION = "1.1.0";
13
39
  export {
14
- NoopLogger,
15
- PACKAGE_VERSION
40
+ noop_logger as NoopLogger,
41
+ PACKAGE_VERSION,
42
+ get_logger,
43
+ noop_logger,
44
+ set_logger
16
45
  };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * mask_email — replace with deterministic masked email.
3
+ * alice@example.com → a3f9b12c1d2e@masked.example
4
+ * Same source → same masked value (FK survival).
5
+ */
6
+ declare function mask_email(value: unknown, _col: string, maskKey: string): unknown;
7
+ /**
8
+ * mask_phone — replace with deterministic masked phone.
9
+ * Preserves leading + and length pattern. +1-555-0001 → +1-XXX-a3f9b1
10
+ */
11
+ declare function mask_phone(value: unknown, _col: string, maskKey: string): unknown;
12
+ declare function fake_name(value: unknown, _col: string, maskKey: string): unknown;
13
+ /**
14
+ * jitter_date — add a deterministic date offset (±0–180 days) to a date string.
15
+ * Preserves approximate era; different source values → different offsets.
16
+ */
17
+ declare function jitter_date(value: unknown, _col: string, maskKey: string): unknown;
18
+ /**
19
+ * hash — replace with HMAC-SHA256 hex hash of value.
20
+ * Fully opaque; same source → same hash.
21
+ */
22
+ declare function hash(value: unknown, col: string, maskKey: string): unknown;
23
+ /**
24
+ * tokenize — replace with a stable short token (FK-safe).
25
+ * tok_<12 hex chars> — same source → same token.
26
+ */
27
+ declare function tokenize(value: unknown, col: string, maskKey: string): unknown;
28
+ /**
29
+ * drop — always returns null (secret dropped, not masked).
30
+ * Used for passwords, tokens, etc.
31
+ */
32
+ declare function drop(_value: unknown, _col: string, _maskKey: string): unknown;
33
+ /**
34
+ * nullify — replace with null (less aggressive than drop; preserves column semantics).
35
+ */
36
+ declare function nullify(_value: unknown, _col: string, _maskKey: string): unknown;
37
+ /**
38
+ * redact_pii — NER-based PII redaction (wink-nlp lite model).
39
+ * Detects named entities (dates, cardinals, etc.) and replaces them with [REDACTED_TYPE].
40
+ * Deterministic: same input + same model → same output.
41
+ * maskKey is accepted but not used (redaction is model-driven, not key-driven).
42
+ */
43
+ declare function redact_pii(value: unknown, _col: string, _maskKey: string): unknown;
44
+ /** Registry of all built-in transforms */
45
+ declare const BUILTIN_TRANSFORMS: Record<string, (value: unknown, col: string, maskKey: string) => unknown>;
46
+ type MaskTransformFn = (value: unknown, col: string, maskKey: string) => unknown;
47
+
48
+ export { BUILTIN_TRANSFORMS, type MaskTransformFn, drop, fake_name, hash, jitter_date, mask_email, mask_phone, nullify, redact_pii, tokenize };
@@ -0,0 +1,115 @@
1
+ // src/mask/index.ts
2
+ import { createHmac } from "crypto";
3
+ import { createRequire } from "module";
4
+ function hmacHex(value, maskKey, context) {
5
+ return createHmac("sha256", maskKey).update(`${context}:${value}`).digest("hex");
6
+ }
7
+ function hmacToken(value, maskKey, context) {
8
+ return hmacHex(value, maskKey, context).slice(0, 12);
9
+ }
10
+ function mask_email(value, _col, maskKey) {
11
+ if (value === null || value === void 0) return value;
12
+ const str = String(value);
13
+ const atIdx = str.indexOf("@");
14
+ const domain = atIdx >= 0 ? str.slice(atIdx + 1) : "example.com";
15
+ const token = hmacToken(str, maskKey, "email");
16
+ return `${token}@masked.${domain}`;
17
+ }
18
+ function mask_phone(value, _col, maskKey) {
19
+ if (value === null || value === void 0) return value;
20
+ const str = String(value);
21
+ const token = hmacToken(str, maskKey, "phone").slice(0, 6);
22
+ const hasPlus = str.startsWith("+");
23
+ const digits = str.replace(/[^\d]/g, "");
24
+ const prefix = digits.length > 4 ? digits.slice(0, 2) : "00";
25
+ return `${hasPlus ? "+" : ""}${prefix}-XXX-${token}`;
26
+ }
27
+ var FIRST_NAMES = ["Alex", "Blake", "Casey", "Dana", "Ellis", "Finley", "Gray", "Harper", "Indigo", "Jordan"];
28
+ var LAST_NAMES = ["Smith", "Jones", "Williams", "Brown", "Davis", "Miller", "Wilson", "Moore", "Taylor", "Anderson"];
29
+ function fake_name(value, _col, maskKey) {
30
+ if (value === null || value === void 0) return value;
31
+ const str = String(value);
32
+ const h = hmacHex(str, maskKey, "name");
33
+ const firstIdx = parseInt(h.slice(0, 2), 16) % FIRST_NAMES.length;
34
+ const lastIdx = parseInt(h.slice(2, 4), 16) % LAST_NAMES.length;
35
+ return `${FIRST_NAMES[firstIdx]} ${LAST_NAMES[lastIdx]}`;
36
+ }
37
+ function jitter_date(value, _col, maskKey) {
38
+ if (value === null || value === void 0) return value;
39
+ const str = String(value);
40
+ const h = hmacHex(str, maskKey, "date");
41
+ const jitterDays = parseInt(h.slice(0, 4), 16) % 361 - 180;
42
+ const date = new Date(str);
43
+ if (isNaN(date.getTime())) return value;
44
+ date.setDate(date.getDate() + jitterDays);
45
+ return date.toISOString().slice(0, 10);
46
+ }
47
+ function hash(value, col, maskKey) {
48
+ if (value === null || value === void 0) return value;
49
+ return hmacHex(String(value), maskKey, col);
50
+ }
51
+ function tokenize(value, col, maskKey) {
52
+ if (value === null || value === void 0) return value;
53
+ return `tok_${hmacToken(String(value), maskKey, col)}`;
54
+ }
55
+ function drop(_value, _col, _maskKey) {
56
+ return null;
57
+ }
58
+ function nullify(_value, _col, _maskKey) {
59
+ return null;
60
+ }
61
+ var _esmReq = createRequire(import.meta.url);
62
+ var _nlpInstance = null;
63
+ var _nlpIts = null;
64
+ function getNlp() {
65
+ if (_nlpInstance) return { nlp: _nlpInstance, its: _nlpIts };
66
+ try {
67
+ const winkNLP = _esmReq("wink-nlp");
68
+ const model = _esmReq("wink-eng-lite-web-model");
69
+ const nlp = winkNLP(model, ["ner"]);
70
+ _nlpInstance = nlp;
71
+ _nlpIts = nlp.its;
72
+ return { nlp, its: nlp.its };
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+ function redact_pii(value, _col, _maskKey) {
78
+ if (value === null || value === void 0) return value;
79
+ const text = String(value);
80
+ const ctx = getNlp();
81
+ if (!ctx) return value;
82
+ const doc = ctx.nlp.readDoc(text);
83
+ const replacements = [];
84
+ doc.entities().each((e) => {
85
+ replacements.push({ text: e.out(), type: e.out(ctx.its.type) });
86
+ });
87
+ let result = text;
88
+ for (const { text: entityText, type } of replacements) {
89
+ result = result.replaceAll(entityText, `[REDACTED_${type}]`);
90
+ }
91
+ return result;
92
+ }
93
+ var BUILTIN_TRANSFORMS = {
94
+ mask_email,
95
+ mask_phone,
96
+ fake_name,
97
+ jitter_date,
98
+ hash,
99
+ tokenize,
100
+ drop,
101
+ nullify,
102
+ redact_pii
103
+ };
104
+ export {
105
+ BUILTIN_TRANSFORMS,
106
+ drop,
107
+ fake_name,
108
+ hash,
109
+ jitter_date,
110
+ mask_email,
111
+ mask_phone,
112
+ nullify,
113
+ redact_pii,
114
+ tokenize
115
+ };
@@ -1,3 +1,10 @@
1
+ /**
2
+ * @deprecated MemoryRateLimitStore has moved to `hazo_api/rate_limit` per D-014.
3
+ * This implementation remains in hazo_secure for one major version for backwards
4
+ * compatibility and will be removed in hazo_secure@2.0.0.
5
+ * Update your imports: `import { MemoryRateLimitStore } from 'hazo_api/rate_limit'`
6
+ */
7
+
1
8
  interface MemoryStoreOptions {
2
9
  now?: () => number;
3
10
  sweepIntervalMs?: number;
@@ -58,6 +58,7 @@ function getDefaultMemoryStore() {
58
58
  }
59
59
 
60
60
  // src/ratelimit/store-connect.ts
61
+ import { HazoConfigError, HazoValidationError, optional_import } from "hazo_core";
61
62
  var ConnectRateLimitStore = class {
62
63
  #getSvc;
63
64
  #now;
@@ -67,14 +68,27 @@ var ConnectRateLimitStore = class {
67
68
  } else if (opts.getAdapter) {
68
69
  const getAdapter = opts.getAdapter;
69
70
  this.#getSvc = async () => {
70
- const { createCrudService } = await import("hazo_connect/server");
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
+ }
71
81
  const adapter = await getAdapter();
72
- return createCrudService(adapter, "hazo_rl_buckets", {
82
+ return mod.createCrudService(adapter, "hazo_rl_buckets", {
73
83
  autoId: { enabled: false, column: "id" }
74
84
  });
75
85
  };
76
86
  } else {
77
- throw new Error("ConnectRateLimitStore: provide getAdapter or _svc");
87
+ throw new HazoConfigError({
88
+ code: "HAZO_SECURE_RATELIMIT_INVALID_STORE_OPTIONS",
89
+ pkg: "hazo_secure",
90
+ message: "ConnectRateLimitStore: provide getAdapter or _svc"
91
+ });
78
92
  }
79
93
  this.#now = opts.now ?? Date.now;
80
94
  }
@@ -94,9 +108,11 @@ var ConnectRateLimitStore = class {
94
108
  }
95
109
  async increment(key, windowMs, max) {
96
110
  if (max === void 0) {
97
- throw new Error(
98
- "ConnectRateLimitStore requires max in increment(). Ensure createRateLimiter / checkRateLimit receives { max }."
99
- );
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
+ });
100
116
  }
101
117
  const svc = await this.svc();
102
118
  const now = this.#now();
@@ -0,0 +1,41 @@
1
+ import { HazoError } from 'hazo_core';
2
+
3
+ type SecretsErrorCode = 'HAZO_SECURE_SECRET_NOT_FOUND';
4
+ declare class SecretsError extends HazoError {
5
+ readonly code: SecretsErrorCode;
6
+ constructor(code: SecretsErrorCode, message: string);
7
+ }
8
+ interface SecretsProvider {
9
+ /** Resolve multiple secret names at once. Returns all resolved values.
10
+ * Throws SecretsError (HAZO_SECURE_SECRET_NOT_FOUND) for any missing name. */
11
+ resolve(names: string[]): Promise<Record<string, string>>;
12
+ /** Resolve a single secret. Throws if absent. */
13
+ get(name: string): Promise<string>;
14
+ }
15
+ /**
16
+ * Reads secrets from environment variables.
17
+ * Variable name = `${prefix}_${NAME.toUpperCase()}` (underscores preserved).
18
+ * Default prefix: `HAZO_SECRET`
19
+ */
20
+ declare class EnvSecretsProvider implements SecretsProvider {
21
+ private readonly prefix;
22
+ constructor(prefix?: string);
23
+ get(name: string): Promise<string>;
24
+ resolve(names: string[]): Promise<Record<string, string>>;
25
+ }
26
+ /** In-memory map. Primary use: tests. */
27
+ declare class StaticSecretsProvider implements SecretsProvider {
28
+ private readonly map;
29
+ constructor(secrets: Record<string, string>);
30
+ get(name: string): Promise<string>;
31
+ resolve(names: string[]): Promise<Record<string, string>>;
32
+ }
33
+ /** Caller-supplied lookup function. Useful for custom secret backends. */
34
+ declare class LookupSecretsProvider implements SecretsProvider {
35
+ private readonly lookup;
36
+ constructor(lookup: (name: string) => string | undefined | Promise<string | undefined>);
37
+ get(name: string): Promise<string>;
38
+ resolve(names: string[]): Promise<Record<string, string>>;
39
+ }
40
+
41
+ export { EnvSecretsProvider, LookupSecretsProvider, SecretsError, type SecretsProvider, StaticSecretsProvider };
@@ -0,0 +1,84 @@
1
+ // src/secrets/index.ts
2
+ import { HazoError } from "hazo_core";
3
+ var SecretsError = class extends HazoError {
4
+ constructor(code, message) {
5
+ super({ code, pkg: "hazo_secure", message, httpStatus: 400, retryable: false });
6
+ this.name = "SecretsError";
7
+ }
8
+ };
9
+ var EnvSecretsProvider = class {
10
+ prefix;
11
+ constructor(prefix = "HAZO_SECRET") {
12
+ this.prefix = prefix;
13
+ }
14
+ async get(name) {
15
+ const envKey = `${this.prefix}_${name.toUpperCase()}`;
16
+ const val = process.env[envKey];
17
+ if (val === void 0) {
18
+ throw new SecretsError(
19
+ "HAZO_SECURE_SECRET_NOT_FOUND",
20
+ `Secret "${name}" not found (checked env var ${envKey})`
21
+ );
22
+ }
23
+ return val;
24
+ }
25
+ async resolve(names) {
26
+ const out = {};
27
+ for (const name of names) {
28
+ out[name] = await this.get(name);
29
+ }
30
+ return out;
31
+ }
32
+ };
33
+ var StaticSecretsProvider = class {
34
+ map;
35
+ constructor(secrets) {
36
+ this.map = new Map(Object.entries(secrets));
37
+ }
38
+ async get(name) {
39
+ const val = this.map.get(name);
40
+ if (val === void 0) {
41
+ throw new SecretsError(
42
+ "HAZO_SECURE_SECRET_NOT_FOUND",
43
+ `Secret "${name}" not found in static map`
44
+ );
45
+ }
46
+ return val;
47
+ }
48
+ async resolve(names) {
49
+ const out = {};
50
+ for (const name of names) {
51
+ out[name] = await this.get(name);
52
+ }
53
+ return out;
54
+ }
55
+ };
56
+ var LookupSecretsProvider = class {
57
+ lookup;
58
+ constructor(lookup) {
59
+ this.lookup = lookup;
60
+ }
61
+ async get(name) {
62
+ const val = await this.lookup(name);
63
+ if (val === void 0) {
64
+ throw new SecretsError(
65
+ "HAZO_SECURE_SECRET_NOT_FOUND",
66
+ `Secret "${name}" not found via lookup`
67
+ );
68
+ }
69
+ return val;
70
+ }
71
+ async resolve(names) {
72
+ const out = {};
73
+ for (const name of names) {
74
+ out[name] = await this.get(name);
75
+ }
76
+ return out;
77
+ }
78
+ };
79
+ export {
80
+ EnvSecretsProvider,
81
+ LookupSecretsProvider,
82
+ SecretsError,
83
+ StaticSecretsProvider
84
+ };
@@ -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
+ );