@exortek/challenge 1.0.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.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Argument guards bound to `ChallengeError` — the package-wide binding
3
+ * of `@exortek/shared/asserts`. Import guards from here, never from the
4
+ * shared module directly: every argument-shape failure must throw
5
+ * `ChallengeError` with `ErrorCode.INVALID_ARGUMENT` so callers get one
6
+ * error class for the whole package.
7
+ */
8
+ export declare const invalidArgument: (msg: string, extra?: import("@exortek/shared/asserts").WrapExtra) => Error, parse: (schema: import("@exortek/shared/asserts").ParseableSchema, input: unknown, path?: string) => unknown, assertObject: (value: unknown, name: string, opts?: import("@exortek/shared/asserts").AssertOptions) => void, assertNonEmptyString: (value: unknown, name: string, opts?: import("@exortek/shared/asserts").AssertOptions) => void, assertBytes: (value: unknown, name: string, opts?: import("@exortek/shared/asserts").AssertOptions) => void;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Store implementations shipped with `@exortek/challenge`. Any object
3
+ * exposing the {@link IncrStore} shape works — these are provided as
4
+ * a zero-config default (memory) and a cluster-safe default (Redis).
5
+ * `@exortek/security`'s rate-limit stores are wire-compatible.
6
+ */
7
+ export { memoryStore } from './memory.js';
8
+ export { redisStore } from './redis.js';
9
+ export type IncrStore = {
10
+ /**
11
+ * Atomic increment-with-expiry. First call for a fresh key returns
12
+ * `{ count: 1 }` and arms a TTL of `ttlMs`; subsequent calls before
13
+ * expiry return the incremented count. `verifyChallenge` reads
14
+ * `count > 1` as "already consumed".
15
+ */
16
+ incr: (key: string, ttlMs: number) => Promise<{
17
+ count: number;
18
+ expiresAt?: number;
19
+ }>;
20
+ };
21
+ /**
22
+ * @typedef {object} IncrStore
23
+ * @property {(key: string, ttlMs: number) => Promise<{ count: number, expiresAt?: number }>} incr
24
+ * Atomic increment-with-expiry. First call for a fresh key returns
25
+ * `{ count: 1 }` and arms a TTL of `ttlMs`; subsequent calls before
26
+ * expiry return the incremented count. `verifyChallenge` reads
27
+ * `count > 1` as "already consumed".
28
+ */
@@ -0,0 +1,50 @@
1
+ /**
2
+ * In-process memory store for challenge single-use enforcement.
3
+ *
4
+ * Not cluster-safe: every worker has its own state, so a token accepted
5
+ * on one worker could still be replayed on another. Use for dev,
6
+ * single-node deploys, sticky-session behind an LB, and tests. For
7
+ * multi-worker production, use {@link redisStore} or pass any object
8
+ * exposing `incr(key, ttlMs) → { count }` (e.g.
9
+ * `@exortek/security`'s rate-limit stores).
10
+ *
11
+ * Eviction: true LRU (least-recently-used). Every `incr` on an existing
12
+ * key re-inserts it so it becomes the newest entry;
13
+ * `map.keys().next().value` is then the least-recently-touched key and
14
+ * is dropped when `maxKeys` is exceeded. This matters for the
15
+ * replay-guard tombstone: a repeated verify attempt refreshes the
16
+ * `count > 1` entry so it stays authoritative until its TTL expires.
17
+ * FIFO would let an idle tombstone age out prematurely and a third
18
+ * replay attempt could see a fresh counter.
19
+ *
20
+ * Textbook pattern, popularised in the JS community by projects like
21
+ * `toad-cache` (MIT); no code copied, only the same ES2015
22
+ * iteration-order guarantee.
23
+ *
24
+ * Expired entries are pruned lazily on read, plus a `setInterval` sweep
25
+ * (unref'd so it never blocks process exit). The `maxKeys` cap
26
+ * protects against unbounded growth if the caller forgets to clean up.
27
+ */
28
+ export type MemoryStoreOptions = {
29
+ /**
30
+ * Hard cap; oldest entry dropped when exceeded.
31
+ */
32
+ maxKeys?: number;
33
+ /**
34
+ * Interval for the background TTL sweep.
35
+ */
36
+ sweepMs?: number;
37
+ };
38
+ /**
39
+ * @typedef {object} MemoryStoreOptions
40
+ * @property {number} [maxKeys=10000] Hard cap; oldest entry dropped when exceeded.
41
+ * @property {number} [sweepMs=60000] Interval for the background TTL sweep.
42
+ */
43
+ /**
44
+ * @param {MemoryStoreOptions} [options]
45
+ * @returns {import('./index.js').IncrStore & { _size: () => number, _stop: () => void }}
46
+ */
47
+ export declare function memoryStore(options?: MemoryStoreOptions): import('./index.js').IncrStore & {
48
+ _size: () => number;
49
+ _stop: () => void;
50
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Redis-backed store for challenge single-use enforcement.
3
+ *
4
+ * Cluster-safe: state lives in Redis, so a challenge accepted on one
5
+ * worker cannot be replayed on another. Works with any client exposing
6
+ * `eval(script, numkeys, ...args)` — verified against `ioredis`,
7
+ * `node-redis` v4+, and `@upstash/redis` (HTTP client, runs on
8
+ * Cloudflare Workers / Vercel Edge / Deno Deploy).
9
+ *
10
+ * Atomicity: `incr` runs a single Lua script that INCR's the key and
11
+ * PEXPIRE's it only when the key is fresh (count === 1). The TTL
12
+ * anchors to the first increment — exactly what single-use enforcement
13
+ * needs (the tombstone lives as long as the token could still verify,
14
+ * then rolls off).
15
+ */
16
+ export type RedisStoreOptions = {
17
+ /**
18
+ * Prepended to every key.
19
+ */
20
+ keyPrefix?: string;
21
+ };
22
+ /**
23
+ * @typedef {object} RedisStoreOptions
24
+ * @property {string} [keyPrefix='chall:'] Prepended to every key.
25
+ */
26
+ /**
27
+ * @param {any} client
28
+ * @param {RedisStoreOptions} [options]
29
+ * @returns {import('./index.js').IncrStore}
30
+ */
31
+ export declare function redisStore(client: any, options?: RedisStoreOptions): import('./index.js').IncrStore;