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.
@@ -1,3 +1,5 @@
1
+ import { HazoError } from 'hazo_core';
2
+
1
3
  interface CsrfPolicy {
2
4
  cookieName?: string;
3
5
  headerName?: string;
@@ -14,7 +16,14 @@ interface CsrfPolicy {
14
16
  };
15
17
  }
16
18
  type CsrfErrorCode = "csrf_cookie_missing" | "csrf_header_missing" | "csrf_mismatch" | "csrf_malformed";
17
- declare class CsrfError extends Error {
19
+ /**
20
+ * Typed error raised by `/csrf`. Extends `HazoError` so it participates in
21
+ * the workspace's correlation-ID auto-attach + envelope shape, while
22
+ * preserving the legacy `(code, message)` constructor + `err.code` short
23
+ * discriminator that 1.0.x consumers already check. All CSRF rejections
24
+ * map to HTTP 403.
25
+ */
26
+ declare class CsrfError extends HazoError {
18
27
  readonly code: CsrfErrorCode;
19
28
  constructor(code: CsrfErrorCode, message: string);
20
29
  }
@@ -1,15 +1,20 @@
1
1
  // src/csrf/index.ts
2
+ import { HazoError } from "hazo_core";
2
3
  var DEFAULT_COOKIE_NAME = "hs_csrf";
3
4
  var DEFAULT_HEADER_NAME = "x-csrf-token";
4
5
  var DEFAULT_TOKEN_LENGTH = 32;
5
6
  var DEFAULT_PROTECTED = ["POST", "PUT", "PATCH", "DELETE"];
6
- var CsrfError = class extends Error {
7
+ var CsrfError = class extends HazoError {
7
8
  constructor(code, message) {
8
- super(message);
9
- this.code = code;
9
+ super({
10
+ code,
11
+ pkg: "hazo_secure",
12
+ message,
13
+ httpStatus: 403,
14
+ retryable: false
15
+ });
10
16
  this.name = "CsrfError";
11
17
  }
12
- code;
13
18
  };
14
19
  function resolved(policy) {
15
20
  return {
@@ -1,3 +1,24 @@
1
+ import { HazoError } from 'hazo_core';
2
+ import { Agent } from 'undici';
3
+
4
+ /**
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.
11
+ */
12
+ declare function validateConnectIp(ip: string): void;
13
+ /**
14
+ * Creates an undici Agent whose DNS lookup validates the resolved IP before
15
+ * the TCP socket is opened. This prevents DNS rebinding: even if the DNS
16
+ * record flips after the pre-flight check, the IP is re-validated here.
17
+ *
18
+ * Node 18+ ships undici as a built-in; we pin an explicit dep for stability.
19
+ */
20
+ declare function createSecureDispatcher(): Agent;
21
+
1
22
  interface SafeFetchPolicy {
2
23
  allowedHosts?: readonly string[];
3
24
  allowedHostPatterns?: readonly RegExp[];
@@ -15,10 +36,16 @@ interface SafeFetchOptions extends Omit<RequestInit, "redirect"> {
15
36
  deps?: SafeFetchDeps;
16
37
  }
17
38
  type SafeFetchErrorCode = "invalid_url" | "protocol_not_allowed" | "host_not_allowed" | "dns_failed" | "private_ip_blocked" | "redirect_limit" | "redirect_loop" | "timeout";
18
- 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 {
19
46
  readonly code: SafeFetchErrorCode;
20
47
  constructor(code: SafeFetchErrorCode, message: string);
21
48
  }
22
49
  declare function safeFetch(input: string | URL, options: SafeFetchOptions): Promise<Response>;
23
50
 
24
- export { type SafeFetchDeps, SafeFetchError, type SafeFetchErrorCode, type SafeFetchOptions, type SafeFetchPolicy, safeFetch };
51
+ export { type SafeFetchDeps, SafeFetchError, type SafeFetchErrorCode, type SafeFetchOptions, type SafeFetchPolicy, createSecureDispatcher, safeFetch, validateConnectIp };
@@ -1,5 +1,7 @@
1
1
  // src/fetch/index.ts
2
- import { lookup } from "dns/promises";
2
+ import { lookup as lookup2 } from "dns/promises";
3
+ import { fetch as undiciFetch } from "undici";
4
+ import { HazoError, REQUEST_ID_HEADER, getCorrelationId } from "hazo_core";
3
5
 
4
6
  // src/fetch/cidr.ts
5
7
  import { isIP } from "net";
@@ -70,20 +72,68 @@ function isPrivateIp(ip) {
70
72
  return false;
71
73
  }
72
74
 
75
+ // src/fetch/dispatcher.ts
76
+ import { Agent } from "undici";
77
+ import * as dns from "dns";
78
+ function validateConnectIp(ip) {
79
+ if (isPrivateIp(ip)) {
80
+ const err = new SafeFetchError(
81
+ "private_ip_blocked",
82
+ `SSRF: resolved IP ${ip} is a private/reserved address \u2014 blocked at connect time`
83
+ );
84
+ err.code = "private_ip_blocked";
85
+ err.ssrfCode = "SSRF_PRIVATE_IP";
86
+ throw err;
87
+ }
88
+ }
89
+ function createSecureDispatcher() {
90
+ return new Agent({
91
+ connect: {
92
+ lookup(hostname, _options, callback) {
93
+ dns.lookup(hostname, (err, address, family) => {
94
+ if (err) return callback(err, "", 4);
95
+ try {
96
+ validateConnectIp(address);
97
+ } catch (ssrfErr) {
98
+ return callback(ssrfErr, "", 4);
99
+ }
100
+ callback(null, address, family);
101
+ });
102
+ }
103
+ }
104
+ });
105
+ }
106
+
73
107
  // src/fetch/index.ts
74
- var SafeFetchError = class extends Error {
108
+ var secureDispatcher = createSecureDispatcher();
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 {
75
120
  constructor(code, message) {
76
- super(message);
77
- 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
+ });
78
129
  this.name = "SafeFetchError";
79
130
  }
80
- code;
81
131
  };
82
132
  var DEFAULT_TIMEOUT_MS = 1e4;
83
133
  var DEFAULT_MAX_REDIRECTS = 3;
84
134
  var DEFAULT_PROTOCOLS = ["https:", "http:"];
85
135
  async function defaultDnsLookup(host) {
86
- const records = await lookup(host, { all: true, verbatim: true });
136
+ const records = await lookup2(host, { all: true, verbatim: true });
87
137
  return records.map((r) => r.address);
88
138
  }
89
139
  function hostAllowed(host, policy) {
@@ -129,7 +179,8 @@ async function validateUrl(url, policy, deps) {
129
179
  }
130
180
  async function safeFetch(input, options) {
131
181
  const { policy, deps = {}, ...init } = options;
132
- const fetchImpl = deps.fetchImpl ?? fetch;
182
+ const fetchImpl = deps.fetchImpl ?? // eslint-disable-next-line @typescript-eslint/no-explicit-any
183
+ ((url, init2) => undiciFetch(url, { ...init2, dispatcher: secureDispatcher }));
133
184
  const maxRedirects = policy.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
134
185
  const timeoutMs = policy.timeoutMs ?? DEFAULT_TIMEOUT_MS;
135
186
  let currentUrl;
@@ -138,6 +189,14 @@ async function safeFetch(input, options) {
138
189
  } catch {
139
190
  throw new SafeFetchError("invalid_url", `Could not parse URL: ${String(input)}`);
140
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
+ }
141
200
  const visited = /* @__PURE__ */ new Set();
142
201
  let hopsRemaining = maxRedirects;
143
202
  while (true) {
@@ -185,5 +244,7 @@ async function safeFetch(input, options) {
185
244
  }
186
245
  export {
187
246
  SafeFetchError,
188
- safeFetch
247
+ createSecureDispatcher,
248
+ safeFetch,
249
+ validateConnectIp
189
250
  };
@@ -1,3 +1,5 @@
1
+ import { HazoError } from 'hazo_core';
2
+
1
3
  interface ExporterContext {
2
4
  userId: string;
3
5
  locale?: string;
@@ -13,12 +15,57 @@ interface DomainAnonymiser {
13
15
  domain: string;
14
16
  anonymise(ctx: ExporterContext): Promise<void>;
15
17
  }
18
+ type UserExportResult = Array<{
19
+ domain: string;
20
+ files: Array<{
21
+ filename: string;
22
+ content: Uint8Array | string;
23
+ }>;
24
+ }>;
25
+ type DryRunExportResult = Array<{
26
+ domain: string;
27
+ files: string[];
28
+ }>;
29
+ interface ValidateRegistryOptions {
30
+ /** Domain names that MUST each have both an exporter and an anonymiser registered. */
31
+ requiredDomains: string[];
32
+ }
33
+ /** Matches hazo_audit's auditIntent after partial application with an adapter. */
34
+ type AuditIntentFn = (name: string, callback: () => Promise<void>, opts?: {
35
+ subject_kind?: string;
36
+ subject_id?: string;
37
+ metadata?: Record<string, unknown>;
38
+ }) => Promise<void>;
39
+ interface GdprRegistryOptions {
40
+ /**
41
+ * Optional audit trail. Pass hazo_audit's auditIntent partially applied:
42
+ * createGdprRegistry({
43
+ * auditIntent: (name, cb, opts) => auditIntent(adapter, name, cb, opts),
44
+ * })
45
+ */
46
+ auditIntent?: AuditIntentFn;
47
+ }
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);
58
+ }
16
59
  interface GdprRegistry {
17
60
  registerExporter(exporter: DomainExporter): void;
18
61
  registerAnonymiser(anonymiser: DomainAnonymiser): void;
19
62
  listExporters(): readonly DomainExporter[];
20
63
  listAnonymisers(): readonly DomainAnonymiser[];
64
+ exportUser(ctx: ExporterContext): Promise<UserExportResult>;
65
+ anonymiseUser(ctx: ExporterContext): Promise<void>;
66
+ validateRegistry(opts: ValidateRegistryOptions): void;
67
+ dryRunExport(ctx: ExporterContext): Promise<DryRunExportResult>;
21
68
  }
22
- declare function createGdprRegistry(): GdprRegistry;
69
+ declare function createGdprRegistry(opts?: GdprRegistryOptions): GdprRegistry;
23
70
 
24
- export { type DomainAnonymiser, type DomainExporter, type ExporterContext, type GdprRegistry, 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,22 +1,107 @@
1
1
  // src/gdpr/index.ts
2
- function createGdprRegistry() {
2
+ import { HazoError } from "hazo_core";
3
+ var GdprRegistryError = class extends HazoError {
4
+ constructor(message, code) {
5
+ super({
6
+ code,
7
+ pkg: "hazo_secure",
8
+ message,
9
+ httpStatus: 500,
10
+ retryable: false
11
+ });
12
+ this.name = "GdprRegistryError";
13
+ }
14
+ };
15
+ function createGdprRegistry(opts = {}) {
3
16
  const exporters = [];
4
17
  const anonymisers = [];
18
+ const { auditIntent } = opts;
19
+ async function exportUser(ctx) {
20
+ const run = async () => {
21
+ const result = [];
22
+ for (const exporter of exporters) {
23
+ const files = [];
24
+ for await (const item of exporter.export(ctx)) {
25
+ files.push(item);
26
+ }
27
+ result.push({ domain: exporter.domain, files });
28
+ }
29
+ return result;
30
+ };
31
+ if (auditIntent) {
32
+ let result;
33
+ await auditIntent(
34
+ "gdpr.export",
35
+ async () => {
36
+ result = await run();
37
+ },
38
+ { subject_kind: "user", subject_id: ctx.userId }
39
+ );
40
+ return result;
41
+ }
42
+ return run();
43
+ }
44
+ async function anonymiseUser(ctx) {
45
+ const run = async () => {
46
+ for (const anonymiser of anonymisers) {
47
+ await anonymiser.anonymise(ctx);
48
+ }
49
+ };
50
+ if (auditIntent) {
51
+ await auditIntent(
52
+ "gdpr.anonymise",
53
+ run,
54
+ { subject_kind: "user", subject_id: ctx.userId, metadata: { domainCount: anonymisers.length } }
55
+ );
56
+ } else {
57
+ await run();
58
+ }
59
+ }
60
+ function validateRegistry(options) {
61
+ const exporterDomains = new Set(exporters.map((e) => e.domain));
62
+ const anonymiserDomains = new Set(anonymisers.map((a) => a.domain));
63
+ const gaps = [];
64
+ for (const domain of options.requiredDomains) {
65
+ const missing = [];
66
+ if (!exporterDomains.has(domain)) missing.push("exporter");
67
+ if (!anonymiserDomains.has(domain)) missing.push("anonymiser");
68
+ if (missing.length > 0) gaps.push(` ${domain}: missing ${missing.join(", ")}`);
69
+ }
70
+ if (gaps.length > 0) {
71
+ throw new GdprRegistryError(
72
+ `GDPR registry incomplete:
73
+ ${gaps.join("\n")}`,
74
+ "incomplete_registry"
75
+ );
76
+ }
77
+ }
78
+ async function dryRunExport(ctx) {
79
+ const result = [];
80
+ for (const exporter of exporters) {
81
+ const files = [];
82
+ for await (const item of exporter.export(ctx)) {
83
+ files.push(item.filename);
84
+ }
85
+ result.push({ domain: exporter.domain, files });
86
+ }
87
+ return result;
88
+ }
5
89
  return {
6
- registerExporter(e) {
90
+ registerExporter: (e) => {
7
91
  exporters.push(e);
8
92
  },
9
- registerAnonymiser(a) {
93
+ registerAnonymiser: (a) => {
10
94
  anonymisers.push(a);
11
95
  },
12
- listExporters() {
13
- return exporters;
14
- },
15
- listAnonymisers() {
16
- return anonymisers;
17
- }
96
+ listExporters: () => exporters,
97
+ listAnonymisers: () => anonymisers,
98
+ exportUser,
99
+ anonymiseUser,
100
+ validateRegistry,
101
+ dryRunExport
18
102
  };
19
103
  }
20
104
  export {
105
+ GdprRegistryError,
21
106
  createGdprRegistry
22
107
  };
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
  };
@@ -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;
@@ -8,7 +15,7 @@ declare class MemoryRateLimitStore implements RateLimitStore {
8
15
  private sweepTimer;
9
16
  constructor(opts?: MemoryStoreOptions);
10
17
  get(key: string): Promise<RateLimitState | null>;
11
- increment(key: string, windowMs: number): Promise<RateLimitState>;
18
+ increment(key: string, windowMs: number, _max?: number): Promise<RateLimitState>;
12
19
  reset(key: string): Promise<void>;
13
20
  size(): number;
14
21
  sweep(): void;
@@ -16,6 +23,50 @@ declare class MemoryRateLimitStore implements RateLimitStore {
16
23
  }
17
24
  declare function getDefaultMemoryStore(): MemoryRateLimitStore;
18
25
 
26
+ type BucketRow = {
27
+ id: string;
28
+ tokens: number;
29
+ last_refill_at: string;
30
+ max_tokens: number;
31
+ refill_rate_per_ms: number;
32
+ };
33
+ type SvcLike = {
34
+ findOneBy(criteria: Record<string, unknown>): Promise<BucketRow | null>;
35
+ insert(data: Partial<BucketRow> | Partial<BucketRow>[]): Promise<BucketRow[]>;
36
+ updateById(id: unknown, patch: Partial<BucketRow>): Promise<BucketRow[]>;
37
+ deleteById(id: unknown): Promise<void>;
38
+ };
39
+ interface ConnectRateLimitStoreOptions {
40
+ /**
41
+ * Factory that resolves to a hazo_connect HazoConnectAdapter. Used in production.
42
+ * Call createCrudService from hazo_connect/server before passing.
43
+ */
44
+ getAdapter?: () => unknown | Promise<unknown>;
45
+ /** For testing: inject a CrudService stub directly. */
46
+ _svc?: () => SvcLike | Promise<SvcLike>;
47
+ /** Injectable clock. Defaults to Date.now. */
48
+ now?: () => number;
49
+ }
50
+ /**
51
+ * Rate-limit store backed by hazo_connect (PostgreSQL or SQLite).
52
+ * Uses a token bucket algorithm — no boundary burst, continuous token refill.
53
+ *
54
+ * Requires the hazo_rl_buckets table from migrations/001_hazo_rl_buckets.sql.
55
+ *
56
+ * Token bucket → RateLimitState mapping:
57
+ * count = max - tokensAfterConsumption
58
+ * count <= max → allowed (tokensAfter >= 0)
59
+ * count > max → denied (tokensAfter < 0)
60
+ */
61
+ declare class ConnectRateLimitStore implements RateLimitStore {
62
+ #private;
63
+ constructor(opts: ConnectRateLimitStoreOptions);
64
+ private svc;
65
+ get(key: string): Promise<RateLimitState | null>;
66
+ increment(key: string, windowMs: number, max?: number): Promise<RateLimitState>;
67
+ reset(key: string): Promise<void>;
68
+ }
69
+
19
70
  interface RateLimitState {
20
71
  count: number;
21
72
  resetAt: number;
@@ -28,7 +79,8 @@ interface RateLimitDecision {
28
79
  }
29
80
  interface RateLimitStore {
30
81
  get(key: string): Promise<RateLimitState | null>;
31
- increment(key: string, windowMs: number): Promise<RateLimitState>;
82
+ /** max is optional for backward compat; ConnectRateLimitStore requires it. */
83
+ increment(key: string, windowMs: number, max?: number): Promise<RateLimitState>;
32
84
  reset(key: string): Promise<void>;
33
85
  }
34
86
  interface RateLimiterOptions {
@@ -51,4 +103,4 @@ interface WithRateLimitOptions extends RateLimiterOptions {
51
103
  type AnyRouteHandler = (...args: any[]) => Promise<Response>;
52
104
  declare function withRateLimit<H extends AnyRouteHandler>(handler: H, opts: WithRateLimitOptions): H;
53
105
 
54
- export { MemoryRateLimitStore, type RateLimitDecision, type RateLimitState, type RateLimitStore, type RateLimiter, type RateLimiterOptions, type WithRateLimitOptions, checkRateLimit, createRateLimiter, defaultKeyResolver, getDefaultMemoryStore, withRateLimit };
106
+ export { ConnectRateLimitStore, type ConnectRateLimitStoreOptions, MemoryRateLimitStore, type RateLimitDecision, type RateLimitState, type RateLimitStore, type RateLimiter, type RateLimiterOptions, type WithRateLimitOptions, checkRateLimit, createRateLimiter, defaultKeyResolver, getDefaultMemoryStore, withRateLimit };