ioredis-toolkit 0.0.9 → 0.5.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 +67 -0
- package/LICENSE +1 -1
- package/README.md +68 -1058
- package/dist/cache/cache.d.ts +30 -0
- package/dist/cache/cache.d.ts.map +1 -0
- package/dist/cache/cache.js +59 -0
- package/dist/cache/cache.js.map +1 -0
- package/dist/cache/config.d.ts +12 -0
- package/dist/cache/config.d.ts.map +1 -0
- package/dist/cache/config.js +13 -0
- package/dist/cache/config.js.map +1 -0
- package/dist/cache/types.d.ts +32 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cache/types.js +5 -0
- package/dist/cache/types.js.map +1 -0
- package/dist/index.d.ts +43 -51
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -44
- package/dist/index.js.map +1 -0
- package/dist/lock/config.d.ts +12 -0
- package/dist/lock/config.d.ts.map +1 -0
- package/dist/lock/config.js +8 -0
- package/dist/lock/config.js.map +1 -0
- package/dist/lock/lock.d.ts +20 -0
- package/dist/lock/lock.d.ts.map +1 -0
- package/dist/lock/lock.js +44 -0
- package/dist/lock/lock.js.map +1 -0
- package/dist/lock/types.d.ts +19 -0
- package/dist/lock/types.d.ts.map +1 -0
- package/dist/lock/types.js +2 -0
- package/dist/lock/types.js.map +1 -0
- package/dist/modules-config.d.ts +3 -0
- package/dist/modules-config.d.ts.map +1 -0
- package/dist/modules-config.js +2 -0
- package/dist/modules-config.js.map +1 -0
- package/dist/pubsub/config.d.ts +11 -0
- package/dist/pubsub/config.d.ts.map +1 -0
- package/dist/pubsub/config.js +6 -0
- package/dist/pubsub/config.js.map +1 -0
- package/dist/pubsub/pubsub.d.ts +20 -0
- package/dist/pubsub/pubsub.d.ts.map +1 -0
- package/dist/pubsub/pubsub.js +54 -0
- package/dist/pubsub/pubsub.js.map +1 -0
- package/dist/pubsub/types.d.ts +24 -0
- package/dist/pubsub/types.d.ts.map +1 -0
- package/dist/pubsub/types.js +2 -0
- package/dist/pubsub/types.js.map +1 -0
- package/dist/rate-limit/config.d.ts +12 -0
- package/dist/rate-limit/config.d.ts.map +1 -0
- package/dist/rate-limit/config.js +6 -0
- package/dist/rate-limit/config.js.map +1 -0
- package/dist/rate-limit/rate-limiter.d.ts +18 -0
- package/dist/rate-limit/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limit/rate-limiter.js +37 -0
- package/dist/rate-limit/rate-limiter.js.map +1 -0
- package/dist/rate-limit/types.d.ts +27 -0
- package/dist/rate-limit/types.d.ts.map +1 -0
- package/dist/rate-limit/types.js +2 -0
- package/dist/rate-limit/types.js.map +1 -0
- package/dist/redis/client-facade.d.ts +77 -0
- package/dist/redis/client-facade.d.ts.map +1 -0
- package/dist/redis/client-facade.js +102 -0
- package/dist/redis/client-facade.js.map +1 -0
- package/dist/redis/client.d.ts +10 -0
- package/dist/redis/client.d.ts.map +1 -0
- package/dist/redis/client.js +29 -0
- package/dist/redis/client.js.map +1 -0
- package/dist/redis/cluster.d.ts +7 -0
- package/dist/redis/cluster.d.ts.map +1 -0
- package/dist/redis/cluster.js +47 -0
- package/dist/redis/cluster.js.map +1 -0
- package/dist/redis/config.d.ts +39 -0
- package/dist/redis/config.d.ts.map +1 -0
- package/dist/redis/config.js +52 -0
- package/dist/redis/config.js.map +1 -0
- package/dist/redis/errors.d.ts +5 -0
- package/dist/redis/errors.d.ts.map +1 -0
- package/dist/redis/errors.js +5 -0
- package/dist/redis/errors.js.map +1 -0
- package/dist/redis/types.d.ts +135 -0
- package/dist/redis/types.d.ts.map +1 -0
- package/dist/redis/types.js +2 -0
- package/dist/redis/types.js.map +1 -0
- package/dist/redis/wrapper.d.ts +88 -0
- package/dist/redis/wrapper.d.ts.map +1 -0
- package/dist/redis/wrapper.js +206 -0
- package/dist/redis/wrapper.js.map +1 -0
- package/dist/session/config.d.ts +47 -0
- package/dist/session/config.d.ts.map +1 -0
- package/dist/session/config.js +101 -0
- package/dist/session/config.js.map +1 -0
- package/dist/session/cookie.d.ts +16 -0
- package/dist/session/cookie.d.ts.map +1 -0
- package/dist/session/cookie.js +28 -0
- package/dist/session/cookie.js.map +1 -0
- package/dist/session/errors.d.ts +56 -0
- package/dist/session/errors.d.ts.map +1 -0
- package/dist/session/errors.js +58 -0
- package/dist/session/errors.js.map +1 -0
- package/dist/session/factory.d.ts +21 -0
- package/dist/session/factory.d.ts.map +1 -0
- package/dist/session/factory.js +30 -0
- package/dist/session/factory.js.map +1 -0
- package/dist/session/health.d.ts +12 -0
- package/dist/session/health.d.ts.map +1 -0
- package/dist/session/health.js +23 -0
- package/dist/session/health.js.map +1 -0
- package/dist/session/keys.d.ts +23 -0
- package/dist/session/keys.d.ts.map +1 -0
- package/dist/session/keys.js +27 -0
- package/dist/session/keys.js.map +1 -0
- package/dist/session/manager.d.ts +34 -0
- package/dist/session/manager.d.ts.map +1 -0
- package/dist/session/manager.js +31 -0
- package/dist/session/manager.js.map +1 -0
- package/dist/session/metrics.d.ts +11 -0
- package/dist/session/metrics.d.ts.map +1 -0
- package/dist/session/metrics.js +10 -0
- package/dist/session/metrics.js.map +1 -0
- package/dist/session/repository.d.ts +49 -0
- package/dist/session/repository.d.ts.map +1 -0
- package/dist/session/repository.js +203 -0
- package/dist/session/repository.js.map +1 -0
- package/dist/session/revocation.d.ts +22 -0
- package/dist/session/revocation.d.ts.map +1 -0
- package/dist/session/revocation.js +41 -0
- package/dist/session/revocation.js.map +1 -0
- package/dist/session/script-sources.d.ts +11 -0
- package/dist/session/script-sources.d.ts.map +1 -0
- package/dist/session/script-sources.js +140 -0
- package/dist/session/script-sources.js.map +1 -0
- package/dist/session/scripts.d.ts +15 -0
- package/dist/session/scripts.d.ts.map +1 -0
- package/dist/session/scripts.js +41 -0
- package/dist/session/scripts.js.map +1 -0
- package/dist/session/serializer.d.ts +12 -0
- package/dist/session/serializer.d.ts.map +1 -0
- package/dist/session/serializer.js +77 -0
- package/dist/session/serializer.js.map +1 -0
- package/dist/session/service.d.ts +48 -0
- package/dist/session/service.d.ts.map +1 -0
- package/dist/session/service.js +235 -0
- package/dist/session/service.js.map +1 -0
- package/dist/session/token.d.ts +16 -0
- package/dist/session/token.d.ts.map +1 -0
- package/dist/session/token.js +32 -0
- package/dist/session/token.js.map +1 -0
- package/dist/session/types.d.ts +134 -0
- package/dist/session/types.d.ts.map +1 -0
- package/dist/session/types.js +2 -0
- package/dist/session/types.js.map +1 -0
- package/dist/streams/config.d.ts +12 -0
- package/dist/streams/config.d.ts.map +1 -0
- package/dist/streams/config.js +6 -0
- package/dist/streams/config.js.map +1 -0
- package/dist/streams/streams.d.ts +24 -0
- package/dist/streams/streams.d.ts.map +1 -0
- package/dist/streams/streams.js +55 -0
- package/dist/streams/streams.js.map +1 -0
- package/dist/streams/types.d.ts +32 -0
- package/dist/streams/types.d.ts.map +1 -0
- package/dist/streams/types.js +2 -0
- package/dist/streams/types.js.map +1 -0
- package/docs/ACCEPTANCE-REPORT.md +70 -0
- package/docs/ARCHITECTURE.md +61 -0
- package/docs/CAPACITY.md +33 -0
- package/docs/DEPLOYMENT.md +22 -0
- package/docs/README-API.md +15 -0
- package/docs/STATE-MACHINE.md +38 -0
- package/docs/TESTING.md +37 -0
- package/docs/THREAT-MODEL.md +23 -0
- package/docs/TYPE-SAFETY.md +34 -0
- package/docs/modules/cache/README.md +7 -0
- package/docs/modules/cache/usage.md +156 -0
- package/docs/modules/lock/README.md +7 -0
- package/docs/modules/lock/usage.md +105 -0
- package/docs/modules/pubsub/README.md +7 -0
- package/docs/modules/pubsub/usage.md +106 -0
- package/docs/modules/rate-limit/README.md +7 -0
- package/docs/modules/rate-limit/usage.md +100 -0
- package/docs/modules/sessions/README.md +7 -0
- package/docs/modules/sessions/usage.md +262 -0
- package/docs/modules/streams/README.md +7 -0
- package/docs/modules/streams/usage.md +141 -0
- package/package.json +50 -60
- package/src/scripts/cleanup-index.lua +4 -0
- package/src/scripts/conditional-update.lua +21 -0
- package/src/scripts/consume-session.lua +21 -0
- package/src/scripts/create-session.lua +28 -0
- package/src/scripts/delete.lua +2 -0
- package/src/scripts/destroy-user.lua +13 -0
- package/src/scripts/enforce-limit.lua +17 -0
- package/src/scripts/revoke-session.lua +13 -0
- package/src/scripts/rotate.lua +24 -0
- package/src/scripts/touch-session.lua +28 -0
- package/src/scripts/update-session.lua +18 -0
- package/dist/cache.d.ts +0 -796
- package/dist/cache.js +0 -1120
- package/dist/client.d.ts +0 -284
- package/dist/client.js +0 -1114
- package/dist/cluster-slot.d.ts +0 -4
- package/dist/cluster-slot.js +0 -31
- package/dist/cluster.d.ts +0 -79
- package/dist/cluster.js +0 -156
- package/dist/errors.d.ts +0 -30
- package/dist/errors.js +0 -63
- package/dist/health.d.ts +0 -180
- package/dist/health.js +0 -239
- package/dist/lock.d.ts +0 -248
- package/dist/lock.js +0 -397
- package/dist/logger.d.ts +0 -12
- package/dist/logger.js +0 -40
- package/dist/pubsub.d.ts +0 -423
- package/dist/pubsub.js +0 -537
- package/dist/ratelimiter.d.ts +0 -441
- package/dist/ratelimiter.js +0 -539
- package/dist/session/index.d.ts +0 -23
- package/dist/session/index.js +0 -16
- package/dist/session/revocation-store.d.ts +0 -176
- package/dist/session/revocation-store.js +0 -318
- package/dist/session/scripts/cleanup-index.lua +0 -21
- package/dist/session/scripts/conditional-update-encrypted.lua +0 -60
- package/dist/session/scripts/conditional-update.lua +0 -63
- package/dist/session/scripts/create.lua +0 -83
- package/dist/session/scripts/delete-by-user.lua +0 -29
- package/dist/session/scripts/delete.lua +0 -15
- package/dist/session/scripts/enforce-limit.lua +0 -38
- package/dist/session/scripts/revoke.lua +0 -61
- package/dist/session/scripts/rotate-encrypted.lua +0 -110
- package/dist/session/scripts/rotate.lua +0 -122
- package/dist/session/scripts/touch-encrypted.lua +0 -89
- package/dist/session/scripts/touch.lua +0 -72
- package/dist/session/scripts/validate.lua +0 -90
- package/dist/session/session-circuit-breaker.d.ts +0 -42
- package/dist/session/session-circuit-breaker.js +0 -129
- package/dist/session/session-config.d.ts +0 -335
- package/dist/session/session-config.js +0 -162
- package/dist/session/session-cookie.d.ts +0 -72
- package/dist/session/session-cookie.js +0 -101
- package/dist/session/session-encryption.d.ts +0 -87
- package/dist/session/session-encryption.js +0 -139
- package/dist/session/session-errors.d.ts +0 -85
- package/dist/session/session-errors.js +0 -145
- package/dist/session/session-health.d.ts +0 -38
- package/dist/session/session-health.js +0 -60
- package/dist/session/session-keys.d.ts +0 -51
- package/dist/session/session-keys.js +0 -113
- package/dist/session/session-manager.d.ts +0 -73
- package/dist/session/session-manager.js +0 -94
- package/dist/session/session-metrics.d.ts +0 -33
- package/dist/session/session-metrics.js +0 -112
- package/dist/session/session-repository.d.ts +0 -161
- package/dist/session/session-repository.js +0 -683
- package/dist/session/session-scripts.d.ts +0 -36
- package/dist/session/session-scripts.js +0 -130
- package/dist/session/session-serializer.d.ts +0 -42
- package/dist/session/session-serializer.js +0 -248
- package/dist/session/session-service.d.ts +0 -104
- package/dist/session/session-service.js +0 -611
- package/dist/session/session-token.d.ts +0 -38
- package/dist/session/session-token.js +0 -86
- package/dist/session/session-types.d.ts +0 -253
- package/dist/session/session-types.js +0 -16
- package/dist/types.d.ts +0 -924
- package/dist/types.js +0 -151
- package/dist/utils/deepmerge.d.ts +0 -9
- package/dist/utils/deepmerge.js +0 -61
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/streams/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,sDAAsD;AACtD,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAC5R,sDAAsD;AACtD,MAAM,UAAU,kBAAkB,CAAC,KAAc,IAAmB,OAAO,mBAAmB,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAkB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { RedisClientWrapper } from '../redis/wrapper.js';
|
|
2
|
+
import type { StreamEntry, StreamReadOptions, StreamsConfig } from './types.js';
|
|
3
|
+
/** Redis Streams abstraction for append, consumer groups, reads and acknowledgements. */
|
|
4
|
+
export declare class RedisStreams {
|
|
5
|
+
private readonly redis;
|
|
6
|
+
private readonly config;
|
|
7
|
+
/** Creates a Streams module bound to the shared Redis client. */
|
|
8
|
+
constructor(redis: RedisClientWrapper, config: StreamsConfig);
|
|
9
|
+
/** Builds the physical stream key. */
|
|
10
|
+
key(name: string): string;
|
|
11
|
+
/** Appends a field map to a stream and optionally trims the stream length. */
|
|
12
|
+
add(name: string, fields: Record<string, string>, maxEntries?: number): Promise<string>;
|
|
13
|
+
/** Creates a consumer group; when startId is omitted, the group starts at '$'. */
|
|
14
|
+
createGroup(name: string, group: string, startId?: string, mkStream?: boolean): Promise<void>;
|
|
15
|
+
/** Reads entries from a stream, optionally through a consumer group. */
|
|
16
|
+
read(name: string, options?: StreamReadOptions): Promise<StreamEntry[]>;
|
|
17
|
+
/** Acknowledges one or more group messages. */
|
|
18
|
+
ack(name: string, group: string, ...ids: string[]): Promise<number>;
|
|
19
|
+
/** Removes entries by ID from a stream. */
|
|
20
|
+
delete(name: string, ...ids: string[]): Promise<number>;
|
|
21
|
+
/** Returns the stream length. */
|
|
22
|
+
length(name: string): Promise<number>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=streams.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streams.d.ts","sourceRoot":"","sources":["../../src/streams/streams.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhF,yFAAyF;AACzF,qBAAa,YAAY;IAEX,OAAO,CAAC,QAAQ,CAAC,KAAK;IAAsB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAD/E,iEAAiE;gBACpC,KAAK,EAAE,kBAAkB,EAAmB,MAAM,EAAE,aAAa;IAC9F,sCAAsC;IACtC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IACzB,8EAA8E;IACxE,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,UAAU,SAAyB,GAAG,OAAO,CAAC,MAAM,CAAC;IAC7G,kFAAkF;IAC5E,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,SAAM,EAAE,QAAQ,UAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAC7F,wEAAwE;IAClE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAWjF,+CAA+C;IACzC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IACzE,2CAA2C;IACrC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAC7D,iCAAiC;IAC3B,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAC5C"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Redis Streams abstraction for append, consumer groups, reads and acknowledgements. */
|
|
2
|
+
export class RedisStreams {
|
|
3
|
+
redis;
|
|
4
|
+
config;
|
|
5
|
+
/** Creates a Streams module bound to the shared Redis client. */
|
|
6
|
+
constructor(redis, config) {
|
|
7
|
+
this.redis = redis;
|
|
8
|
+
this.config = config;
|
|
9
|
+
}
|
|
10
|
+
/** Builds the physical stream key. */
|
|
11
|
+
key(name) { return `${this.config.keyPrefix}:${name}`; }
|
|
12
|
+
/** Appends a field map to a stream and optionally trims the stream length. */
|
|
13
|
+
async add(name, fields, maxEntries = this.config.maxEntries) { return this.redis.xadd(this.key(name), 'MAXLEN', '~', String(maxEntries), '*', ...Object.entries(fields).flat()); }
|
|
14
|
+
/** Creates a consumer group; when startId is omitted, the group starts at '$'. */
|
|
15
|
+
async createGroup(name, group, startId = '$', mkStream = true) { try {
|
|
16
|
+
await this.redis.xgroup('CREATE', this.key(name), group, startId, ...(mkStream ? ['MKSTREAM'] : []));
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (!(error instanceof Error) || !/BUSYGROUP/.test(error.message))
|
|
20
|
+
throw error;
|
|
21
|
+
} }
|
|
22
|
+
/** Reads entries from a stream, optionally through a consumer group. */
|
|
23
|
+
async read(name, options = {}) {
|
|
24
|
+
if (options.group !== undefined && options.consumer === undefined)
|
|
25
|
+
throw new RangeError('Stream reads via a consumer group require options.consumer');
|
|
26
|
+
const useGroup = options.group !== undefined && options.consumer !== undefined;
|
|
27
|
+
const count = options.count ?? 100;
|
|
28
|
+
const id = options.id ?? (useGroup ? '>' : '$');
|
|
29
|
+
const blockMs = options.blockMs ?? this.config.blockMs;
|
|
30
|
+
const result = useGroup
|
|
31
|
+
? await this.redis.xreadgroup('GROUP', options.group, options.consumer, 'COUNT', String(count), 'BLOCK', String(blockMs), 'STREAMS', this.key(name), id)
|
|
32
|
+
: await this.redis.xread('COUNT', String(count), 'BLOCK', String(blockMs), 'STREAMS', this.key(name), id);
|
|
33
|
+
return parseStreamResult(result);
|
|
34
|
+
}
|
|
35
|
+
/** Acknowledges one or more group messages. */
|
|
36
|
+
async ack(name, group, ...ids) { return this.redis.xack(this.key(name), group, ...ids); }
|
|
37
|
+
/** Removes entries by ID from a stream. */
|
|
38
|
+
async delete(name, ...ids) { return this.redis.xdel(this.key(name), ...ids); }
|
|
39
|
+
/** Returns the stream length. */
|
|
40
|
+
async length(name) { return this.redis.xlen(this.key(name)); }
|
|
41
|
+
}
|
|
42
|
+
function parseStreamResult(result) {
|
|
43
|
+
if (!Array.isArray(result) || result.length === 0)
|
|
44
|
+
return [];
|
|
45
|
+
const stream = result[0];
|
|
46
|
+
if (!stream || !Array.isArray(stream[1]))
|
|
47
|
+
return [];
|
|
48
|
+
return stream[1].map(([id, values]) => { const fields = {}; for (let i = 0; i < values.length; i += 2) {
|
|
49
|
+
const key = values[i];
|
|
50
|
+
const value = values[i + 1];
|
|
51
|
+
if (key !== undefined && value !== undefined)
|
|
52
|
+
fields[key] = value;
|
|
53
|
+
} return { id, fields }; });
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=streams.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streams.js","sourceRoot":"","sources":["../../src/streams/streams.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Normalized configuration for {@link RedisStreams}. */
|
|
2
|
+
export interface StreamsConfig {
|
|
3
|
+
/** Whether the application has enabled the module. The class remains directly constructible when false; {@link RedisClient}'s `streams` getter throws when disabled. */
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
/** Prefix used for physical stream keys. */
|
|
6
|
+
keyPrefix: string;
|
|
7
|
+
/** Approximate number of entries retained by the default `add()` trim operation. */
|
|
8
|
+
maxEntries: number;
|
|
9
|
+
/** Default configured blocking duration in milliseconds for application policy. */
|
|
10
|
+
blockMs: number;
|
|
11
|
+
}
|
|
12
|
+
/** One decoded Redis Stream entry. */
|
|
13
|
+
export interface StreamEntry {
|
|
14
|
+
/** Redis stream entry ID, for example `1750000000000-0`. */
|
|
15
|
+
id: string;
|
|
16
|
+
/** Field/value pairs returned by Redis. */
|
|
17
|
+
fields: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
/** Options controlling a stream read. */
|
|
20
|
+
export interface StreamReadOptions {
|
|
21
|
+
/** Consumer group name. Supplying this selects `XREADGROUP`. */
|
|
22
|
+
group?: string;
|
|
23
|
+
/** Consumer name. Required when `group` is supplied. */
|
|
24
|
+
consumer?: string;
|
|
25
|
+
/** Maximum number of entries requested from Redis. */
|
|
26
|
+
count?: number;
|
|
27
|
+
/** Optional blocking duration in milliseconds. */
|
|
28
|
+
blockMs?: number;
|
|
29
|
+
/** Starting/continuation ID. Group reads commonly use `>`. */
|
|
30
|
+
id?: string;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/streams/types.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,MAAM,WAAW,aAAa;IAC5B,wKAAwK;IACxK,OAAO,EAAE,OAAO,CAAC;IACjB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,oFAAoF;IACpF,UAAU,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,sCAAsC;AACtC,MAAM,WAAW,WAAW;IAC1B,4DAA4D;IAC5D,EAAE,EAAE,MAAM,CAAC;IACX,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED,yCAAyC;AACzC,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,EAAE,CAAC,EAAE,MAAM,CAAC;CACb"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/streams/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Acceptance Report — redis-session-kit 0.3.0
|
|
2
|
+
|
|
3
|
+
> **Update (0.5.0):** the package was subsequently renamed to `ioredis-toolkit`. The "Environment limitation" below is specific to the 0.3.0 acceptance pass and no longer applies: the 0.5.0 bug-fix pass ran `typecheck`, `lint`, `test`, and `build` against installed dependencies in this environment and all passed, including a full, verified-ESM `dist/` rebuild. See `CHANGELOG.md` and `docs/TYPE-SAFETY.md`'s "0.5.0 verification note" for what changed and what was actually run.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
This release adds and documents the Cache, Lock, Rate Limiting, Pub/Sub, and Streams modules alongside the existing session subsystem. All modules share one `RedisClient` and support global configuration plus fluent merge/replace overrides.
|
|
8
|
+
|
|
9
|
+
## Completed source-level checks
|
|
10
|
+
|
|
11
|
+
- All TypeScript source was regenerated into `dist/` using the TypeScript compiler's syntax-preserving transpilation APIs after source changes.
|
|
12
|
+
- Every generated JavaScript file passes `node --check`.
|
|
13
|
+
- Public declarations were regenerated from the current source.
|
|
14
|
+
- `npm pack --dry-run` succeeds and reports the expected source, distribution, documentation, and Lua-script files.
|
|
15
|
+
- Module usage documentation exists for Cache, Lock, Rate Limiting, Pub/Sub, Streams, and Sessions.
|
|
16
|
+
- Public module types and methods have JSDoc documentation in the source/declarations.
|
|
17
|
+
|
|
18
|
+
## Environment limitation
|
|
19
|
+
|
|
20
|
+
The execution environment could not download npm dependencies from `registry.npmjs.org` because external registry resolution returned `EAI_AGAIN`. Therefore a genuine dependency-backed `npm run typecheck`, Vitest run, and Redis integration run could not be completed here.
|
|
21
|
+
|
|
22
|
+
This limitation is explicitly recorded rather than treating missing dependency declarations as a successful typecheck.
|
|
23
|
+
|
|
24
|
+
## Required verification before publishing
|
|
25
|
+
|
|
26
|
+
Run in a normal networked development/CI environment:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bun install --frozen-lockfile
|
|
30
|
+
bun run typecheck
|
|
31
|
+
bun run lint
|
|
32
|
+
bun run test
|
|
33
|
+
bun run test:integration
|
|
34
|
+
bun run build
|
|
35
|
+
npm pack
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For integration/failover verification, also run the provided Docker Redis Standalone, Sentinel, and Cluster environments.
|
|
39
|
+
|
|
40
|
+
## Configuration API acceptance
|
|
41
|
+
|
|
42
|
+
The intended public configuration model is:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const redis = createRedisClient({
|
|
46
|
+
mode: 'standalone',
|
|
47
|
+
host: '127.0.0.1',
|
|
48
|
+
port: 6379,
|
|
49
|
+
cache: { enabled: true },
|
|
50
|
+
lock: { enabled: true },
|
|
51
|
+
rateLimit: { enabled: true },
|
|
52
|
+
pubsub: { enabled: true },
|
|
53
|
+
streams: { enabled: true },
|
|
54
|
+
sessions: { enabled: true },
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Each module can then be adjusted without constructing another Redis connection:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
redis
|
|
62
|
+
.withCache({ defaultTtl: 60 })
|
|
63
|
+
.withLock({ defaultTtl: 15 })
|
|
64
|
+
.withRateLimit({ maxRequests: 500 })
|
|
65
|
+
.withPubSub({ channelPrefix: 'critical' })
|
|
66
|
+
.withStreams({ maxEntries: 500_000 })
|
|
67
|
+
.withSessions({ idleTimeout: 3600 });
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Use `replace` when a module should return to defaults before applying the supplied override.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Architecture Decision Record
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted for the initial implementation.
|
|
6
|
+
|
|
7
|
+
## Decision: create the package and Redis infrastructure from scratch
|
|
8
|
+
|
|
9
|
+
The supplied specification described an existing Redis package, but this project started without one. Therefore the implementation owns the Redis connection factory, wrapper, cluster utilities, configuration validation, session domain, and public exports. No duplicate external authentication/core package is assumed.
|
|
10
|
+
|
|
11
|
+
## Decision: opaque token + token-hash locator
|
|
12
|
+
|
|
13
|
+
A token-only `validate(token)` operation requires a locator. The package uses SHA-256(token) as the lookup key and stores only the derived hash plus a user hash-tag pointer. The actual session record remains authoritative.
|
|
14
|
+
|
|
15
|
+
This is a deliberate secondary-index consistency boundary. A missing/stale index never authenticates anything.
|
|
16
|
+
|
|
17
|
+
## Decision: per-user Cluster hash tag
|
|
18
|
+
|
|
19
|
+
Session keys are shaped as:
|
|
20
|
+
|
|
21
|
+
`<namespace>:session:{<sha256(userId)>}:<sha256(token)>`
|
|
22
|
+
|
|
23
|
+
The user hash is deterministic and safe for Redis hash-tag syntax. It keeps one user's session records/index in one slot while distributing different users across slots.
|
|
24
|
+
|
|
25
|
+
## Decision: no global JTI index
|
|
26
|
+
|
|
27
|
+
JTI is an internal session identifier and is not needed to validate the opaque token. A global JTI index would add another cross-slot consistency boundary without improving the core validation path.
|
|
28
|
+
|
|
29
|
+
## Decision: session index as a negative authorization guard
|
|
30
|
+
|
|
31
|
+
The user sorted-set index is checked during validation. The index can deny access but cannot grant access: the session record must independently pass all checks.
|
|
32
|
+
|
|
33
|
+
Max-session enforcement (`create_session`) and revocation (`revoke()`, and rotation's `consume()`) all remove their affected member from the index eagerly. `create_session` additionally deletes the evicted session's own physical session and token-index keys — a bounded operation (at most one evicted session per `create()` call, given the per-user cap is enforced on every write), not the unbounded cluster-wide cleanup the "bounded maintenance" decision below rules out. Earlier revisions left the physical keys behind to expire on their own TTL; that left orphaned Redis memory and a token-index entry that stayed briefly resolvable past the point the session was actually evicted, so the index-only approach was deliberately narrowed to also delete the record it is evicting.
|
|
34
|
+
|
|
35
|
+
## Decision: rotation is two-phase
|
|
36
|
+
|
|
37
|
+
A predecessor is consumed atomically. A successor is created separately because the token-hash key is unrelated to the predecessor's key slot. This can produce a safe availability failure after consumption, but cannot produce two valid successors.
|
|
38
|
+
|
|
39
|
+
The package does not claim rotation idempotency. Network-timeout retry must be handled by a higher-level protocol if required.
|
|
40
|
+
|
|
41
|
+
## Decision: server time
|
|
42
|
+
|
|
43
|
+
Redis TIME is used for authentication/session timestamps to reduce application-clock ordering problems. TTLs remain Redis storage cleanup boundaries; application expiration fields remain authoritative for session validity.
|
|
44
|
+
|
|
45
|
+
## Decision: bounded maintenance
|
|
46
|
+
|
|
47
|
+
No request path scans the entire cluster or performs unbounded Lua loops. Batch/list/revoke-all calls are capped by configuration.
|
|
48
|
+
|
|
49
|
+
## Decision: no built-in circuit breaker
|
|
50
|
+
|
|
51
|
+
The low-level Redis client already owns connection/reconnect behavior. A circuit breaker in this first package would add another failure-state machine without changing the required fail-closed authentication semantics. Applications may wrap the package with an operation-class breaker if their deployment needs one.
|
|
52
|
+
|
|
53
|
+
`SessionConfig.circuitBreaker` exists as a reserved, fully-validated/defaulted configuration section for a future breaker, but no repository or service code currently reads it — it is not yet wired to any actual circuit-breaking behavior. Do not infer working circuit-breaker semantics from its presence in the config schema.
|
|
54
|
+
|
|
55
|
+
## Decision: every Redis connection gets a default `'error'` listener
|
|
56
|
+
|
|
57
|
+
`Redis`/`Cluster` instances are Node `EventEmitter`s that throw an uncaught exception and crash the process when an `'error'` event fires with zero listeners. `createRedisConnection()` and `RedisPubSub`'s dedicated subscriber connection now both attach a no-op default listener so a connection drop can never crash the process by itself. This does not replace real observability: applications that need alerting on connection errors should still attach their own `'error'` listener on the connection object.
|
|
58
|
+
|
|
59
|
+
## Decision: module `enabled` flags are enforced uniformly
|
|
60
|
+
|
|
61
|
+
Every module config (`cache`, `lock`, `rateLimit`, `pubsub`, `streams`, `sessions`) exposes an `enabled` flag defaulting to `false`. `RedisClient`'s lazy getters (`.cache`, `.lock`, `.rateLimiter`, `.pubsub`, `.streams`, `.sessions`) throw a configuration error while the corresponding module is disabled. Each module class remains directly constructible regardless of `enabled` — the flag only gates access through the shared client facade, matching how `sessions` already behaved before the other four modules were brought in line with it.
|
package/docs/CAPACITY.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Capacity Planning
|
|
2
|
+
|
|
3
|
+
Exact Redis memory depends heavily on allocator overhead, key length, metadata, replication, and encryption envelope size. The following is an engineering sizing method rather than a promise of exact bytes.
|
|
4
|
+
|
|
5
|
+
## Per-session components
|
|
6
|
+
|
|
7
|
+
Approximate:
|
|
8
|
+
|
|
9
|
+
- Session key: namespace + fixed prefixes + 64-byte token hash + user hash tag.
|
|
10
|
+
- User-index member: 64-byte token hash plus sorted-set overhead.
|
|
11
|
+
- Serialized record: fixed fields plus bounded metadata.
|
|
12
|
+
- Token locator: token hash key + 64-byte token hash + user tag value.
|
|
13
|
+
- Redis object/allocator overhead: measure with the target Redis version and representative payloads.
|
|
14
|
+
- Replication: multiply by replica count.
|
|
15
|
+
- Fragmentation/headroom: reserve operational margin; do not size to `maxmemory` exactly.
|
|
16
|
+
|
|
17
|
+
## Required benchmark
|
|
18
|
+
|
|
19
|
+
Before production sizing, create representative sessions with the largest expected metadata, then measure `MEMORY USAGE` for each key family and extrapolate to:
|
|
20
|
+
|
|
21
|
+
- 1 million sessions
|
|
22
|
+
- 10 million sessions
|
|
23
|
+
- 50 million sessions
|
|
24
|
+
|
|
25
|
+
Include token indexes, user indexes, replication, AOF/RDB overhead, allocator fragmentation, and peak write amplification.
|
|
26
|
+
|
|
27
|
+
## Operational limits
|
|
28
|
+
|
|
29
|
+
The package defaults to 8 KiB session metadata, 200-item batches, concurrency 8, and 20 sessions per user. These are conservative defaults, not universal production limits. Increase them only after load testing.
|
|
30
|
+
|
|
31
|
+
A per-user hash tag intentionally concentrates one user's session state on one slot. Extremely active users can become hot keys/slots; solving that requires a separate sharded-user design and would weaken the simple same-user atomicity model.
|
|
32
|
+
|
|
33
|
+
Sessions evicted by `maxSessionsPerUser` now have their physical session and token-index keys deleted immediately at eviction time, rather than only being removed from the user index and left to expire on their own TTL. Under high per-user session churn this makes actual Redis memory usage track the configured cap more closely instead of transiently overshooting it by up to one full session TTL's worth of orphaned keys per evicted session.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Deployment and Migration
|
|
2
|
+
|
|
3
|
+
## Rolling deployment
|
|
4
|
+
|
|
5
|
+
The serializer envelope contains a schema version. New application versions should continue reading the previous supported version during a rolling deployment. Add new fields rather than changing the meaning of existing fields in place — for example, `SessionRecord.idleTimeoutSeconds` (0.5.0) was added as a new optional field to preserve rotation/touch idle-window behavior, rather than repurposing an existing field. A record written by an older version simply omits it, and the serializer's schema validation treats it as optional.
|
|
6
|
+
|
|
7
|
+
Lua scripts are embedded and versioned with the package build. `EVALSHA` is attempted first; after a `NOSCRIPT` response the exact source is sent with `EVAL`, allowing Redis restart/failover to recover without an external script preload step.
|
|
8
|
+
|
|
9
|
+
## Rollback
|
|
10
|
+
|
|
11
|
+
1. Keep old application binaries available until all new sessions are compatible with the old reader.
|
|
12
|
+
2. Do not remove old serializer support until the compatibility window closes.
|
|
13
|
+
3. Roll back application code before removing Redis key formats.
|
|
14
|
+
4. Clean obsolete indexes only with bounded maintenance jobs.
|
|
15
|
+
|
|
16
|
+
## Disaster recovery
|
|
17
|
+
|
|
18
|
+
Redis authentication state is security-sensitive. Restoring an old RDB/AOF snapshot can resurrect sessions or undo revocations that happened after the snapshot. Treat restore as a security event and invalidate the affected authentication state using an authoritative security-version mechanism at the application/platform level.
|
|
19
|
+
|
|
20
|
+
## Memory policy
|
|
21
|
+
|
|
22
|
+
Authentication state should not use an arbitrary eviction policy. `noeviction` is used in the included disposable test environments so memory pressure fails writes instead of silently evicting authentication records. Production capacity and failover requirements must be validated independently.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# API Notes
|
|
2
|
+
|
|
3
|
+
## Authorization boundary
|
|
4
|
+
|
|
5
|
+
The package does not know whether the caller is an administrator or the owner of a user account. `revokeAll(userId)` and `list(userId)` therefore must be called only after the host authentication/authorization layer has established permission. The package intentionally does not accept a caller-supplied user ID as proof of authorization.
|
|
6
|
+
|
|
7
|
+
## Token lookup
|
|
8
|
+
|
|
9
|
+
`validate(token)` performs a token-hash lookup, then loads and validates the authoritative session record and checks the per-user authorization index. No raw token is used as a Redis key.
|
|
10
|
+
|
|
11
|
+
## Revocation store
|
|
12
|
+
|
|
13
|
+
Use `RedisRevocationStore` for credentials whose authorization model is JTI-based, such as externally issued JWTs. For the opaque stateful sessions in this package, session state itself is the source of truth and a separate revocation key is intentionally not consulted on every request.
|
|
14
|
+
|
|
15
|
+
`RedisRevocationStore.revoke(jti, now, expiresAt, reason?)` takes an explicit `now` (Unix seconds) rather than reading `Date.now()` internally, so callers should source `now` from the same clock used elsewhere in their system (e.g. Redis `TIME`, as `SessionService` does) to avoid clock-skew affecting the tombstone TTL.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Session State Machine
|
|
2
|
+
|
|
3
|
+
```text
|
|
4
|
+
ACTIVE ───────► CONSUMED
|
|
5
|
+
│ │
|
|
6
|
+
│ ▼
|
|
7
|
+
└────────────► REVOKED
|
|
8
|
+
│
|
|
9
|
+
▼
|
|
10
|
+
EXPIRED (derived by time)
|
|
11
|
+
│
|
|
12
|
+
▼
|
|
13
|
+
DELETED (physical cleanup)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Rules
|
|
17
|
+
|
|
18
|
+
- `ACTIVE` is the only state that can authenticate.
|
|
19
|
+
- `CONSUMED` cannot authenticate and is retained only for bounded replay detection.
|
|
20
|
+
- `REVOKED` cannot authenticate.
|
|
21
|
+
- Redis TTL may physically delete any state after its security usefulness ends.
|
|
22
|
+
- `destroy` is physical deletion and idempotent.
|
|
23
|
+
- `revoke` is logical denial and idempotent.
|
|
24
|
+
- `rotate` atomically consumes the predecessor before successor creation.
|
|
25
|
+
- A stale user-index member cannot authenticate because validation requires active session state and current index membership.
|
|
26
|
+
|
|
27
|
+
## Operation semantics
|
|
28
|
+
|
|
29
|
+
| Operation | Source | Result | Atomicity | Retry |
|
|
30
|
+
|---|---|---|---|---|
|
|
31
|
+
| create | none | active | session + user index same-slot Lua; on eviction, also same-slot `DEL` of the evicted session/token-index keys; token locator is a separate, cross-slot secondary write | not idempotent |
|
|
32
|
+
| validate | active candidate | read-only decision | no transaction needed | safe read retry by application |
|
|
33
|
+
| touch | active | active | same-key Lua | monotonic/idempotent effect |
|
|
34
|
+
| update | active | active | same-key Lua + version | safe only with version semantics |
|
|
35
|
+
| rotate | active | predecessor consumed + new active session | predecessor Lua consume is atomic; a same-slot but separate `ZREM` then removes it from the user index (self-heals via `list()` if interrupted); successor cross-slot write separate | not inherently idempotent |
|
|
36
|
+
| revoke | active/revoked | revoked/deleted | same-key Lua marks the record; a same-slot but separate `ZREM` then removes it from the user index (self-heals via `list()` if interrupted) | idempotent |
|
|
37
|
+
| destroy | any | deleted | individual writes | idempotent |
|
|
38
|
+
| revokeAll | user index | bounded deletion | bounded per-user pipeline | idempotent per batch |
|
package/docs/TESTING.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Testing Matrix
|
|
2
|
+
|
|
3
|
+
The project separates deterministic unit/security tests from real Redis tests.
|
|
4
|
+
|
|
5
|
+
## Required CI layers
|
|
6
|
+
|
|
7
|
+
1. TypeScript strict typecheck.
|
|
8
|
+
2. ESLint.
|
|
9
|
+
3. Unit tests for token generation, hashing, key hashing, serializer, configuration, cookie serialization, and Pub/Sub message/error handling (connection error resilience, malformed-message dropping, subscribe/unsubscribe deduplication).
|
|
10
|
+
4. Security tests ensuring no raw-token persistence is part of the repository contract and invalid credentials fail closed.
|
|
11
|
+
5. Concurrency tests against real Redis for simultaneous touch, update, rotate, revoke, and max-session operations.
|
|
12
|
+
6. Standalone integration tests.
|
|
13
|
+
7. Sentinel integration tests with master/replica/Sentinel failover.
|
|
14
|
+
8. Cluster integration tests with multiple primaries/replicas, MOVED/ASK handling, resharding, and node failure.
|
|
15
|
+
9. Load/performance tests using bounded concurrency.
|
|
16
|
+
|
|
17
|
+
## Shared Redis safety
|
|
18
|
+
|
|
19
|
+
Tests must use a unique namespace per run. Never use `FLUSHALL` or `FLUSHDB` against a shared Redis deployment.
|
|
20
|
+
|
|
21
|
+
## Failure injection
|
|
22
|
+
|
|
23
|
+
Production verification should include:
|
|
24
|
+
|
|
25
|
+
- connection loss during reads;
|
|
26
|
+
- connection loss after a server-side write;
|
|
27
|
+
- primary failure and promotion;
|
|
28
|
+
- cluster slot migration;
|
|
29
|
+
- command-level pipeline errors;
|
|
30
|
+
- malformed session payloads;
|
|
31
|
+
- malformed cache/Pub-Sub payloads (non-JSON values written or published by a non-conforming client);
|
|
32
|
+
- stale token indexes;
|
|
33
|
+
- stale user-index members;
|
|
34
|
+
- Redis memory pressure;
|
|
35
|
+
- old backup restore scenarios.
|
|
36
|
+
|
|
37
|
+
The current archive includes the standalone real-Redis integration harness. Sentinel and Cluster environments are supplied as disposable Docker definitions; their failover/resharding tests should be exercised in CI where Docker networking and Redis administration are available.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Threat Model
|
|
2
|
+
|
|
3
|
+
| Threat | Asset | Mitigation | Residual risk / test target |
|
|
4
|
+
|---|---|---|---|
|
|
5
|
+
| Stolen session token | Authentication state | 256-bit secret; SHA-256 storage; TLS expected; revocation/rotation | Bearer token remains a bearer credential; test token theft/replay |
|
|
6
|
+
| Token brute force | Session lookup | 256-bit random secret | Computationally infeasible; test format rejection |
|
|
7
|
+
| Token enumeration | Authentication existence | Opaque token; safe invalid results | Timing differences should be monitored; public APIs should not expose internal reasons |
|
|
8
|
+
| Session fixation | Login/session identity | Fresh random token creation and rotation | Host auth flow must replace pre-auth session after authentication |
|
|
9
|
+
| Replay after rotation | Refresh/session credential | Atomic predecessor consumption | Rotation is non-idempotent; test concurrent rotate |
|
|
10
|
+
| Replay after logout | Session state | revoke/destroy + index checks | A request already authorized before logout may complete; authorization transaction boundaries remain application-specific |
|
|
11
|
+
| Concurrent attacker requests | Session state | Lua state transitions and version checks | Test simultaneous touch/update/rotate |
|
|
12
|
+
| Redis outage | Authentication state | Fail closed; storage errors distinct from invalid sessions | Application decides 401 vs 503; never fail open |
|
|
13
|
+
| Uncaught connection-error crash | Availability (whole process) | Default no-op `'error'` listener attached to every Redis connection, including the Pub/Sub subscriber connection | The default listener only prevents the crash; applications needing alerting must still attach their own `'error'` listener on the connection |
|
|
14
|
+
| Malformed cached/published payload | Availability | `RedisCache.get()` throws a typed `CacheError`; `RedisPubSub` silently drops an unparseable message | Neither crashes the process; a cache read failure still surfaces as an error to the caller rather than a silent miss |
|
|
15
|
+
| Redis data exposure | Session metadata | Optional AES-256-GCM; TLS/ACLs/private network | Compromised app with key can decrypt |
|
|
16
|
+
| Stale secondary index | Authorization | Session record independently validated; index only denies | `create()` eviction, `revoke()`, and rotation's `consume()` now remove their affected index member eagerly (not only via `list()`'s lazy self-heal), shrinking the window in which a stale entry can exist and be counted toward `maxSessionsPerUser` |
|
|
17
|
+
| Redis eviction | Session state | TTL + index checks; operational memory policy required | Eviction can log users out; it must never authenticate them |
|
|
18
|
+
| Old backup restore | Revocation state | Document restore as security event | Requires deployment-level security-version invalidation strategy |
|
|
19
|
+
| CSRF | Cookie credential | Cookie flags + framework-level CSRF strategy | SameSite is not universal CSRF protection |
|
|
20
|
+
| XSS | Browser auth state | HttpOnly cookie option | XSS can still act as user in browser; application CSP/output encoding required |
|
|
21
|
+
| CORS misconfiguration | Credentialed browser requests | No framework CORS policy inside core | Host application must whitelist origins and never wildcard credentialed requests |
|
|
22
|
+
| Hot user slot | Availability | Per-user hash tag; bounded operations | A single extremely active user can concentrate load |
|
|
23
|
+
| Memory exhaustion | Redis availability | Metadata/pipeline/batch bounds | Deployment still needs capacity planning |
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Type Safety and Verification
|
|
2
|
+
|
|
3
|
+
This package is authored for TypeScript strict mode with ESM and Node.js 22+.
|
|
4
|
+
|
|
5
|
+
## What is guaranteed by the source
|
|
6
|
+
|
|
7
|
+
- No `any` is used in package source modules.
|
|
8
|
+
- Public module APIs use explicit exported types.
|
|
9
|
+
- Redis internals are hidden behind `RedisClientWrapper`.
|
|
10
|
+
- Pub/Sub uses a type-only import for `Redis`, which is required when `verbatimModuleSyntax` is enabled.
|
|
11
|
+
- Optional properties are authored to work with `exactOptionalPropertyTypes`.
|
|
12
|
+
- Runtime configuration is validated before it reaches module constructors.
|
|
13
|
+
|
|
14
|
+
## Required verification
|
|
15
|
+
|
|
16
|
+
Run the following after installing dependencies:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bun install
|
|
20
|
+
bun run typecheck
|
|
21
|
+
bun run build
|
|
22
|
+
bun run lint
|
|
23
|
+
bun run test
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Integration tests require Redis and are intentionally separated:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bun run test:integration
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 0.5.0 verification note
|
|
33
|
+
|
|
34
|
+
Earlier releases of this document recorded that dependency-backed `tsc`/Vitest execution could not be performed in the environment used to prepare the archive. That limitation does not apply to the 0.5.0 bug-fix pass: `bun run typecheck` (equivalently `tsc -p tsconfig.json --noEmit`), `bun run lint`, `bun run test`, and `bun run build` were all executed against the installed dependencies and passed, including a full rebuild of `dist/` verified to emit real ESM (the previously committed `dist/` had drifted to stale CommonJS output — see the Changelog). Do not treat a structural audit as a substitute for actually running these commands; the note above only documents that, for this pass, they were run.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Cache module
|
|
2
|
+
|
|
3
|
+
This directory contains the complete usage documentation for the cache module.
|
|
4
|
+
|
|
5
|
+
**Full guide:** [usage.md](./usage.md)
|
|
6
|
+
|
|
7
|
+
The usage guide documents configuration, public types, every public method, arguments, return values, semantics, error/edge-case behavior, and multiple examples.
|