{"version":3,"sources":["../../src/stores/cloudflare-d1.ts"],"sourcesContent":["import type { DPoPNonceStore } from \"./types.js\";\n\nconst DEFAULT_TABLE = \"dpop_jti\";\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: \"dpop_jti\"). Must match /^[A-Za-z_][A-Za-z0-9_]*$/. */\n\ttableName?: string;\n}\n\n/**\n * Cloudflare D1-backed replay cache. Uses `INSERT OR IGNORE` on a `jti` PRIMARY KEY for\n * an atomic insert-if-absent: D1 reports `meta.changes === 1` for fresh inserts, `0` for\n * collisions. Strong consistency from the SQLite primary makes this safe across\n * concurrent Worker invocations.\n *\n * Note: `INSERT OR IGNORE` does not honor TTL — an expired-but-not-yet-purged row will\n * still block re-acquisition of the same jti. Operators should call `purge()` periodically\n * (cron trigger or scheduled handler) to delete rows whose `expires_at < now`.\n *\n * The schema is created on demand via `CREATE TABLE IF NOT EXISTS` once per instance.\n */\nexport function d1Store(options: D1StoreOptions): DPoPNonceStore {\n\tconst { database: db, tableName = DEFAULT_TABLE } = 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\tjti TEXT PRIMARY KEY,\n\t\t\t\texpires_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\treturn {\n\t\tasync check(jti, expiresAt) {\n\t\t\tawait ensureTable();\n\t\t\tconst result = await db\n\t\t\t\t.prepare(`INSERT OR IGNORE INTO ${tableName} (jti, expires_at) VALUES (?, ?)`)\n\t\t\t\t.bind(jti, expiresAt)\n\t\t\t\t.run();\n\t\t\treturn result.meta.changes === 1;\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 expires_at < ?`)\n\t\t\t\t.bind(Date.now())\n\t\t\t\t.run();\n\t\t\treturn result.meta.changes;\n\t\t},\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,IAAM,gBAAgB;AACtB,IAAM,gBAAgB;AAgCf,SAAS,QAAQ,SAAyC;AAChE,QAAM,EAAE,UAAU,IAAI,YAAY,cAAc,IAAI;AAEpD,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,IAIxC,EACC,IAAI;AACN,kBAAc;AAAA,EACf;AAEA,SAAO;AAAA,IACN,MAAM,MAAM,KAAK,WAAW;AAC3B,YAAM,YAAY;AAClB,YAAM,SAAS,MAAM,GACnB,QAAQ,yBAAyB,SAAS,kCAAkC,EAC5E,KAAK,KAAK,SAAS,EACnB,IAAI;AACN,aAAO,OAAO,KAAK,YAAY;AAAA,IAChC;AAAA,IAEA,MAAM,QAAQ;AACb,YAAM,YAAY;AAClB,YAAM,SAAS,MAAM,GACnB,QAAQ,eAAe,SAAS,uBAAuB,EACvD,KAAK,KAAK,IAAI,CAAC,EACf,IAAI;AACN,aAAO,OAAO,KAAK;AAAA,IACpB;AAAA,EACD;AACD;","names":[]}