{"version":3,"sources":["../../src/stores/cloudflare-d1.ts","../../src/types.ts"],"sourcesContent":["import {\n\ttype IdempotencyRecord,\n\tRECORD_STATUS_COMPLETED,\n\ttype RECORD_STATUS_PROCESSING,\n\ttype StoredResponse,\n} from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TABLE = \"idempotency_keys\";\nconst DEFAULT_TTL = 86400; // 24 hours in seconds\nconst TABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\n/** Minimal D1Database subset used by d1Store (avoids @cloudflare/workers-types dependency). */\nexport interface D1DatabaseLike {\n\tprepare(sql: string): D1PreparedStatementLike;\n}\n\nexport interface D1PreparedStatementLike {\n\tbind(...params: unknown[]): D1PreparedStatementLike;\n\trun(): Promise<{ success: boolean; meta: { changes: number } }>;\n\tfirst(): Promise<Record<string, unknown> | null>;\n}\n\nexport interface D1StoreOptions {\n\t/** Cloudflare D1 database binding. */\n\tdatabase: D1DatabaseLike;\n\t/** Table name (default: \"idempotency_keys\"). Must match /^[a-zA-Z_][a-zA-Z0-9_]*$/. */\n\ttableName?: string;\n\t/** TTL in seconds (default: 86400 = 24h). Expired rows are ignored by get/lock. */\n\tttl?: number;\n}\n\nexport function d1Store(options: D1StoreOptions): IdempotencyStore {\n\tconst { database: db, tableName = DEFAULT_TABLE, ttl = DEFAULT_TTL } = options;\n\n\tif (!TABLE_NAME_RE.test(tableName)) {\n\t\tthrow new Error(`Invalid table name: \"${tableName}\". Must match ${TABLE_NAME_RE}`);\n\t}\n\n\tlet initialized = false;\n\n\tconst ensureTable = async (): Promise<void> => {\n\t\tif (initialized) return;\n\t\tawait db\n\t\t\t.prepare(\n\t\t\t\t`CREATE TABLE IF NOT EXISTS ${tableName} (\n\t\t\t\tkey TEXT PRIMARY KEY,\n\t\t\t\tfingerprint TEXT NOT NULL,\n\t\t\t\tstatus TEXT NOT NULL,\n\t\t\t\tresponse TEXT,\n\t\t\t\tcreated_at INTEGER NOT NULL\n\t\t\t)`,\n\t\t\t)\n\t\t\t.run();\n\t\tinitialized = true;\n\t};\n\n\tconst ttlThreshold = (): number => {\n\t\treturn Date.now() - ttl * 1000;\n\t};\n\n\tconst toRecord = (row: Record<string, unknown>): IdempotencyRecord => {\n\t\tlet response: StoredResponse | undefined;\n\t\tif (row.response) {\n\t\t\ttry {\n\t\t\t\tresponse = JSON.parse(row.response as string) as StoredResponse;\n\t\t\t} catch {\n\t\t\t\t// Corrupt JSON in storage — degrade gracefully like other stores\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\tkey: row.key as string,\n\t\t\tfingerprint: row.fingerprint as string,\n\t\t\tstatus: row.status as typeof RECORD_STATUS_PROCESSING | typeof RECORD_STATUS_COMPLETED,\n\t\t\tresponse,\n\t\t\tcreatedAt: row.created_at as number,\n\t\t};\n\t};\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tawait ensureTable();\n\t\t\tconst row = await db\n\t\t\t\t.prepare(`SELECT * FROM ${tableName} WHERE key = ? AND created_at >= ?`)\n\t\t\t\t.bind(key, ttlThreshold())\n\t\t\t\t.first();\n\t\t\tif (!row) return undefined;\n\t\t\treturn toRecord(row);\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tawait ensureTable();\n\t\t\tconst result = await db\n\t\t\t\t.prepare(\n\t\t\t\t\t`INSERT OR IGNORE INTO ${tableName} (key, fingerprint, status, response, created_at)\n\t\t\t\t\tSELECT ?, ?, ?, ?, ?\n\t\t\t\t\tWHERE NOT EXISTS (SELECT 1 FROM ${tableName} WHERE key = ? AND created_at >= ?)`,\n\t\t\t\t)\n\t\t\t\t.bind(key, record.fingerprint, record.status, null, record.createdAt, key, ttlThreshold())\n\t\t\t\t.run();\n\t\t\treturn result.meta.changes > 0;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tawait ensureTable();\n\t\t\tlet serialized: string;\n\t\t\ttry {\n\t\t\t\tserialized = JSON.stringify(response);\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait db\n\t\t\t\t.prepare(`UPDATE ${tableName} SET status = ?, response = ? WHERE key = ?`)\n\t\t\t\t.bind(RECORD_STATUS_COMPLETED, serialized, key)\n\t\t\t\t.run();\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait ensureTable();\n\t\t\tawait db.prepare(`DELETE FROM ${tableName} WHERE key = ?`).bind(key).run();\n\t\t},\n\n\t\tasync purge() {\n\t\t\tawait ensureTable();\n\t\t\tconst result = await db\n\t\t\t\t.prepare(`DELETE FROM ${tableName} WHERE created_at < ?`)\n\t\t\t\t.bind(ttlThreshold())\n\t\t\t\t.run();\n\t\t\treturn result.meta.changes;\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;;;ADGvC,IAAM,gBAAgB;AACtB,IAAM,cAAc;AACpB,IAAM,gBAAgB;AAsBf,SAAS,QAAQ,SAA2C;AAClE,QAAM,EAAE,UAAU,IAAI,YAAY,eAAe,MAAM,YAAY,IAAI;AAEvE,MAAI,CAAC,cAAc,KAAK,SAAS,GAAG;AACnC,UAAM,IAAI,MAAM,wBAAwB,SAAS,iBAAiB,aAAa,EAAE;AAAA,EAClF;AAEA,MAAI,cAAc;AAElB,QAAM,cAAc,YAA2B;AAC9C,QAAI,YAAa;AACjB,UAAM,GACJ;AAAA,MACA,8BAA8B,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOxC,EACC,IAAI;AACN,kBAAc;AAAA,EACf;AAEA,QAAM,eAAe,MAAc;AAClC,WAAO,KAAK,IAAI,IAAI,MAAM;AAAA,EAC3B;AAEA,QAAM,WAAW,CAAC,QAAoD;AACrE,QAAI;AACJ,QAAI,IAAI,UAAU;AACjB,UAAI;AACH,mBAAW,KAAK,MAAM,IAAI,QAAkB;AAAA,MAC7C,QAAQ;AAAA,MAER;AAAA,IACD;AACA,WAAO;AAAA,MACN,KAAK,IAAI;AAAA,MACT,aAAa,IAAI;AAAA,MACjB,QAAQ,IAAI;AAAA,MACZ;AAAA,MACA,WAAW,IAAI;AAAA,IAChB;AAAA,EACD;AAEA,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,YAAY;AAClB,YAAM,MAAM,MAAM,GAChB,QAAQ,iBAAiB,SAAS,oCAAoC,EACtE,KAAK,KAAK,aAAa,CAAC,EACxB,MAAM;AACR,UAAI,CAAC,IAAK,QAAO;AACjB,aAAO,SAAS,GAAG;AAAA,IACpB;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM,YAAY;AAClB,YAAM,SAAS,MAAM,GACnB;AAAA,QACA,yBAAyB,SAAS;AAAA;AAAA,uCAEA,SAAS;AAAA,MAC5C,EACC,KAAK,KAAK,OAAO,aAAa,OAAO,QAAQ,MAAM,OAAO,WAAW,KAAK,aAAa,CAAC,EACxF,IAAI;AACN,aAAO,OAAO,KAAK,UAAU;AAAA,IAC9B;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,YAAY;AAClB,UAAI;AACJ,UAAI;AACH,qBAAa,KAAK,UAAU,QAAQ;AAAA,MACrC,QAAQ;AACP;AAAA,MACD;AACA,YAAM,GACJ,QAAQ,UAAU,SAAS,6CAA6C,EACxE,KAAK,yBAAyB,YAAY,GAAG,EAC7C,IAAI;AAAA,IACP;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,YAAY;AAClB,YAAM,GAAG,QAAQ,eAAe,SAAS,gBAAgB,EAAE,KAAK,GAAG,EAAE,IAAI;AAAA,IAC1E;AAAA,IAEA,MAAM,QAAQ;AACb,YAAM,YAAY;AAClB,YAAM,SAAS,MAAM,GACnB,QAAQ,eAAe,SAAS,uBAAuB,EACvD,KAAK,aAAa,CAAC,EACnB,IAAI;AACN,aAAO,OAAO,KAAK;AAAA,IACpB;AAAA,EACD;AACD;","names":[]}