{"version":3,"sources":["../../src/stores/redis.ts","../../src/types.ts"],"sourcesContent":["import { type IdempotencyRecord, RECORD_STATUS_COMPLETED } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 86400; // 24 hours in seconds\n\n/** Minimal Redis client subset compatible with ioredis, node-redis, and @upstash/redis. */\nexport interface RedisClientLike {\n\tget(key: string): Promise<string | null>;\n\tset(key: string, value: string, options?: { NX?: boolean; EX?: number }): Promise<string | null>;\n\tdel(...keys: string[]): Promise<number>;\n}\n\nexport interface RedisStoreOptions {\n\t/** Redis client instance (ioredis, node-redis, or @upstash/redis). */\n\tclient: RedisClientLike;\n\t/** TTL in seconds (default: 86400 = 24h). Passed as Redis EX option. */\n\tttl?: number;\n}\n\nexport function redisStore(options: RedisStoreOptions): IdempotencyStore {\n\tconst { client, ttl = DEFAULT_TTL } = options;\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst raw = await client.get(key);\n\t\t\tif (!raw) return undefined;\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(raw) as IdempotencyRecord;\n\t\t\t} catch {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tlet serialized: string;\n\t\t\ttry {\n\t\t\t\tserialized = JSON.stringify(record);\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst result = await client.set(key, serialized, { NX: true, EX: ttl });\n\t\t\treturn result === \"OK\";\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst raw = await client.get(key);\n\t\t\tif (!raw) return;\n\t\t\tlet record: IdempotencyRecord;\n\t\t\ttry {\n\t\t\t\trecord = JSON.parse(raw) as IdempotencyRecord;\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\trecord.status = RECORD_STATUS_COMPLETED;\n\t\t\trecord.response = response;\n\t\t\tconst elapsed = Math.floor((Date.now() - record.createdAt) / 1000);\n\t\t\tconst remaining = Math.max(1, ttl - elapsed);\n\t\t\tlet serialized: string;\n\t\t\ttry {\n\t\t\t\tserialized = JSON.stringify(record);\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait client.set(key, serialized, { EX: remaining });\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait client.del(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\t// Redis handles expiration automatically via EX — no manual purge needed\n\t\t\treturn 0;\n\t\t},\n\t};\n}\n","import type { Context, Env } from \"hono\";\nimport type { ProblemDetail } from \"./errors.js\";\nimport type { IdempotencyStore } from \"./stores/types.js\";\n\nexport const RECORD_STATUS_PROCESSING = \"processing\" as const;\nexport const RECORD_STATUS_COMPLETED = \"completed\" as const;\n\nexport interface IdempotencyEnv extends Env {\n\tVariables: {\n\t\tidempotencyKey: string | undefined;\n\t};\n}\n\nexport interface StoredResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string;\n}\n\nexport interface IdempotencyRecord {\n\tkey: string;\n\tfingerprint: string;\n\tstatus: \"processing\" | \"completed\";\n\tresponse?: StoredResponse;\n\tcreatedAt: number;\n}\n\nexport interface IdempotencyOptions {\n\tstore: IdempotencyStore;\n\theaderName?: string;\n\tfingerprint?: (c: Context) => string | Promise<string>;\n\trequired?: boolean;\n\tmethods?: string[];\n\tmaxKeyLength?: number;\n\t/**\n\t * Maximum request body size in bytes. Pre-checked via Content-Length header,\n\t * then enforced against actual body byte length.\n\t * Only applies when an Idempotency-Key header is present.\n\t * Requests without the key bypass this check regardless of this setting.\n\t */\n\tmaxBodySize?: number;\n\t/** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */\n\tskipRequest?: (c: Context) => boolean | Promise<boolean>;\n\t/** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */\n\tonError?: (error: ProblemDetail, c: Context) => Response | Promise<Response>;\n\tcacheKeyPrefix?: string | ((c: Context) => string | Promise<string>);\n\t/**\n\t * Called when a cached response is about to be replayed.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t * `key` is the raw header value; sanitize before logging to prevent log injection.\n\t */\n\tonCacheHit?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Called when a new request acquires the lock (before the handler runs).\n\t * Fires on each lock acquisition, including retries after prior failures.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t */\n\tonCacheMiss?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Opt out of the multi-tenant safety warning.\n\t *\n\t * When `cacheKeyPrefix` is not set and `methods` includes any state-mutating\n\t * method (POST/PATCH/PUT/DELETE), the middleware emits a one-time\n\t * `console.warn` at factory construction time. Set this to `true` to\n\t * acknowledge that the deployment is single-tenant and silence the warning.\n\t *\n\t * @default false\n\t */\n\tdangerouslyAllowGlobalKeys?: boolean;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKO,IAAM,0BAA0B;;;ADFvC,IAAM,cAAc;AAgBb,SAAS,WAAW,SAA8C;AACxE,QAAM,EAAE,QAAQ,MAAM,YAAY,IAAI;AAEtC,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,MAAM,MAAM,OAAO,IAAI,GAAG;AAChC,UAAI,CAAC,IAAK,QAAO;AACjB,UAAI;AACH,eAAO,KAAK,MAAM,GAAG;AAAA,MACtB,QAAQ;AACP,eAAO;AAAA,MACR;AAAA,IACD;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,UAAI;AACJ,UAAI;AACH,qBAAa,KAAK,UAAU,MAAM;AAAA,MACnC,QAAQ;AACP,eAAO;AAAA,MACR;AACA,YAAM,SAAS,MAAM,OAAO,IAAI,KAAK,YAAY,EAAE,IAAI,MAAM,IAAI,IAAI,CAAC;AACtE,aAAO,WAAW;AAAA,IACnB;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,MAAM,MAAM,OAAO,IAAI,GAAG;AAChC,UAAI,CAAC,IAAK;AACV,UAAI;AACJ,UAAI;AACH,iBAAS,KAAK,MAAM,GAAG;AAAA,MACxB,QAAQ;AACP;AAAA,MACD;AACA,aAAO,SAAS;AAChB,aAAO,WAAW;AAClB,YAAM,UAAU,KAAK,OAAO,KAAK,IAAI,IAAI,OAAO,aAAa,GAAI;AACjE,YAAM,YAAY,KAAK,IAAI,GAAG,MAAM,OAAO;AAC3C,UAAI;AACJ,UAAI;AACH,qBAAa,KAAK,UAAU,MAAM;AAAA,MACnC,QAAQ;AACP;AAAA,MACD;AACA,YAAM,OAAO,IAAI,KAAK,YAAY,EAAE,IAAI,UAAU,CAAC;AAAA,IACpD;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,OAAO,IAAI,GAAG;AAAA,IACrB;AAAA,IAEA,MAAM,QAAQ;AAEb,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}