@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.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/dist/errors.d.ts +26 -0
- package/dist/index.cjs +1250 -0
- package/dist/index.d.ts +252 -0
- package/dist/index.mjs +1245 -0
- package/dist/internal/guards.d.ts +8 -0
- package/dist/stores/index.d.ts +28 -0
- package/dist/stores/memory.d.ts +50 -0
- package/dist/stores/redis.d.ts +31 -0
- package/dist/stores.cjs +710 -0
- package/dist/stores.mjs +707 -0
- package/dist/token.d.ts +64 -0
- package/package.json +71 -0
|
@@ -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;
|