{"version":3,"file":"billing-C6yftryb.mjs","names":[],"sources":["../src/database/repositories/audit.ts","../src/database/repositories/usage.ts","../src/api/handlers/billing.ts"],"sourcesContent":["import type { Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport type { Database, AuditLogTable } from \"../types.js\";\nimport { encodeCursor, decodeCursor, type FindManyResult } from \"./types.js\";\n\nexport type AuditAction =\n\t| \"create\"\n\t| \"update\"\n\t| \"delete\"\n\t| \"publish\"\n\t| \"unpublish\"\n\t| \"login\"\n\t| \"logout\"\n\t| \"password_change\"\n\t| \"settings_update\"\n\t| \"schema_change\"\n\t| \"plugin_tool_invoke\";\n\nexport type AuditStatus = \"success\" | \"failure\" | \"denied\";\n\nexport interface AuditLog {\n\tid: string;\n\ttimestamp: string;\n\tactorId: string | null;\n\tactorIp: string | null;\n\taction: AuditAction;\n\tresourceType: string | null;\n\tresourceId: string | null;\n\tdetails: Record<string, unknown> | null;\n\tstatus: AuditStatus | null;\n}\n\nexport interface CreateAuditLogInput {\n\tactorId?: string;\n\tactorIp?: string;\n\taction: AuditAction;\n\tresourceType?: string;\n\tresourceId?: string;\n\tdetails?: Record<string, unknown>;\n\tstatus?: AuditStatus;\n}\n\nexport interface AuditLogQuery {\n\tactorId?: string;\n\taction?: AuditAction;\n\tresourceType?: string;\n\tresourceId?: string;\n\tstatus?: AuditStatus;\n\tsince?: string; // ISO date\n\tuntil?: string; // ISO date\n\tlimit?: number;\n\tcursor?: string;\n}\n\n/**\n * Audit repository for logging system events\n *\n * Tracks user actions for security, debugging, and compliance.\n * All mutations should create an audit log entry.\n */\nexport class AuditRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Create an audit log entry\n\t */\n\tasync log(input: CreateAuditLogInput): Promise<AuditLog> {\n\t\tconst id = ulid();\n\n\t\tconst row: Omit<AuditLogTable, \"timestamp\"> = {\n\t\t\tid,\n\t\t\tactor_id: input.actorId ?? null,\n\t\t\tactor_ip: input.actorIp ?? null,\n\t\t\taction: input.action,\n\t\t\tresource_type: input.resourceType ?? null,\n\t\t\tresource_id: input.resourceId ?? null,\n\t\t\tdetails: input.details ? JSON.stringify(input.details) : null,\n\t\t\tstatus: input.status ?? null,\n\t\t};\n\n\t\tawait this.db.insertInto(\"audit_logs\").values(row).execute();\n\n\t\tconst log = await this.findById(id);\n\t\tif (!log) {\n\t\t\tthrow new Error(\"Failed to create audit log\");\n\t\t}\n\t\treturn log;\n\t}\n\n\t/**\n\t * Find audit log by ID\n\t */\n\tasync findById(id: string): Promise<AuditLog | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"audit_logs\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToAuditLog(row) : null;\n\t}\n\n\t/**\n\t * Query audit logs with filters and cursor-based pagination\n\t */\n\tasync findMany(query: AuditLogQuery = {}): Promise<FindManyResult<AuditLog>> {\n\t\tconst limit = Math.min(Math.max(1, query.limit || 50), 100);\n\n\t\tlet q = this.db\n\t\t\t.selectFrom(\"audit_logs\")\n\t\t\t.selectAll()\n\t\t\t.orderBy(\"timestamp\", \"desc\")\n\t\t\t.orderBy(\"id\", \"desc\")\n\t\t\t.limit(limit + 1);\n\n\t\tif (query.actorId) {\n\t\t\tq = q.where(\"actor_id\", \"=\", query.actorId);\n\t\t}\n\n\t\tif (query.action) {\n\t\t\tq = q.where(\"action\", \"=\", query.action);\n\t\t}\n\n\t\tif (query.resourceType) {\n\t\t\tq = q.where(\"resource_type\", \"=\", query.resourceType);\n\t\t}\n\n\t\tif (query.resourceId) {\n\t\t\tq = q.where(\"resource_id\", \"=\", query.resourceId);\n\t\t}\n\n\t\tif (query.status) {\n\t\t\tq = q.where(\"status\", \"=\", query.status);\n\t\t}\n\n\t\tif (query.since) {\n\t\t\tq = q.where(\"timestamp\", \">=\", query.since);\n\t\t}\n\n\t\tif (query.until) {\n\t\t\tq = q.where(\"timestamp\", \"<=\", query.until);\n\t\t}\n\n\t\tif (query.cursor) {\n\t\t\tconst decoded = decodeCursor(query.cursor);\n\t\t\tq = q.where((eb) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb(\"timestamp\", \"<\", decoded.orderValue),\n\t\t\t\t\teb.and([eb(\"timestamp\", \"=\", decoded.orderValue), eb(\"id\", \"<\", decoded.id)]),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tconst rows = await q.execute();\n\t\tconst items = rows.slice(0, limit).map((row) => this.rowToAuditLog(row));\n\t\tconst result: FindManyResult<AuditLog> = { items };\n\n\t\tif (rows.length > limit && items.length > 0) {\n\t\t\tconst last = items.at(-1)!;\n\t\t\tresult.nextCursor = encodeCursor(last.timestamp, last.id);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get all logs for a specific resource\n\t */\n\tasync findByResource(\n\t\tresourceType: string,\n\t\tresourceId: string,\n\t\toptions: { limit?: number } = {},\n\t): Promise<AuditLog[]> {\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"audit_logs\")\n\t\t\t.selectAll()\n\t\t\t.where(\"resource_type\", \"=\", resourceType)\n\t\t\t.where(\"resource_id\", \"=\", resourceId)\n\t\t\t.orderBy(\"timestamp\", \"desc\");\n\n\t\tif (options.limit) {\n\t\t\tquery = query.limit(options.limit);\n\t\t}\n\n\t\tconst rows = await query.execute();\n\t\treturn rows.map((row) => this.rowToAuditLog(row));\n\t}\n\n\t/**\n\t * Get all logs for a specific user\n\t */\n\tasync findByActor(\n\t\tactorId: string,\n\t\toptions: { limit?: number; since?: string } = {},\n\t): Promise<AuditLog[]> {\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"audit_logs\")\n\t\t\t.selectAll()\n\t\t\t.where(\"actor_id\", \"=\", actorId)\n\t\t\t.orderBy(\"timestamp\", \"desc\");\n\n\t\tif (options.since) {\n\t\t\tquery = query.where(\"timestamp\", \">=\", options.since);\n\t\t}\n\n\t\tif (options.limit) {\n\t\t\tquery = query.limit(options.limit);\n\t\t}\n\n\t\tconst rows = await query.execute();\n\t\treturn rows.map((row) => this.rowToAuditLog(row));\n\t}\n\n\t/**\n\t * Count logs matching a query\n\t */\n\tasync count(query: Omit<AuditLogQuery, \"limit\" | \"cursor\"> = {}): Promise<number> {\n\t\tlet q = this.db.selectFrom(\"audit_logs\").select((eb) => eb.fn.count(\"id\").as(\"count\"));\n\n\t\tif (query.actorId) {\n\t\t\tq = q.where(\"actor_id\", \"=\", query.actorId);\n\t\t}\n\n\t\tif (query.action) {\n\t\t\tq = q.where(\"action\", \"=\", query.action);\n\t\t}\n\n\t\tif (query.resourceType) {\n\t\t\tq = q.where(\"resource_type\", \"=\", query.resourceType);\n\t\t}\n\n\t\tif (query.resourceId) {\n\t\t\tq = q.where(\"resource_id\", \"=\", query.resourceId);\n\t\t}\n\n\t\tif (query.status) {\n\t\t\tq = q.where(\"status\", \"=\", query.status);\n\t\t}\n\n\t\tif (query.since) {\n\t\t\tq = q.where(\"timestamp\", \">=\", query.since);\n\t\t}\n\n\t\tif (query.until) {\n\t\t\tq = q.where(\"timestamp\", \"<=\", query.until);\n\t\t}\n\n\t\tconst result = await q.executeTakeFirst();\n\t\treturn Number(result?.count || 0);\n\t}\n\n\t/**\n\t * Delete old audit logs (for retention policy)\n\t */\n\tasync deleteOlderThan(date: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"audit_logs\")\n\t\t\t.where(\"timestamp\", \"<\", date)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result.numDeletedRows ?? 0);\n\t}\n\n\t/**\n\t * Convert database row to AuditLog object\n\t */\n\tprivate rowToAuditLog(row: {\n\t\tid: string;\n\t\ttimestamp: string;\n\t\tactor_id: string | null;\n\t\tactor_ip: string | null;\n\t\taction: string;\n\t\tresource_type: string | null;\n\t\tresource_id: string | null;\n\t\tdetails: string | null;\n\t\tstatus: string | null;\n\t}): AuditLog {\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\ttimestamp: row.timestamp,\n\t\t\tactorId: row.actor_id,\n\t\t\tactorIp: row.actor_ip,\n\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- DB stores string; validated at insert but linter can't follow\n\t\t\taction: row.action as AuditAction,\n\t\t\tresourceType: row.resource_type,\n\t\t\tresourceId: row.resource_id,\n\t\t\tdetails: row.details ? JSON.parse(row.details) : null,\n\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- DB stores string; validated at insert but linter can't follow\n\t\t\tstatus: row.status as AuditStatus | null,\n\t\t};\n\t}\n}\n","import type { Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport type { Database, UsageTable } from \"../types.js\";\n\n/** One appended ledger row, as returned to the admin screens. */\nexport interface UsageEntry {\n\tid: string;\n\tts: string;\n\tday: string;\n\tkind: string;\n\tkey: string;\n\tquantity: number;\n\tcostMicros: number;\n\tchargeMicros: number;\n\tactorId: string | null;\n\tmeta: Record<string, unknown> | null;\n\tsource: string;\n\tref: string | null;\n}\n\n/** A row to append. `ts`/`day`/`id` default to now/ulid when omitted. */\nexport interface RecordUsageInput {\n\tkind: string;\n\tkey: string;\n\tquantity?: number;\n\tcostMicros?: number;\n\tchargeMicros?: number;\n\tactorId?: string | null;\n\tmeta?: Record<string, unknown> | null;\n\tsource?: string;\n\tref?: string | null;\n\tts?: string;\n}\n\n/** Aggregate billing view backing the `/billing` screen. */\nexport interface BillingSummary {\n\t/** Remaining credit in micro-dollars: purchased minus spent. */\n\tbalanceMicros: number;\n\t/** Total credits ever purchased (positive micro-dollars). */\n\tpurchasedMicros: number;\n\t/** Total charged for usage (positive micro-dollars). */\n\tspentMicros: number;\n\t/** Provider's own underlying cost so far (positive micro-dollars). */\n\tcostMicros: number;\n\t/** Charge broken down by metric key, most expensive first. */\n\tbyKey: Array<{ key: string; quantity: number; chargeMicros: number }>;\n\t/** Charge per day (ascending), for the trend chart. */\n\tdaily: Array<{ day: string; chargeMicros: number; costMicros: number }>;\n\t/** Most recent ledger rows, newest first. */\n\trecent: UsageEntry[];\n}\n\nfunction toEntry(row: UsageTable): UsageEntry {\n\tlet meta: Record<string, unknown> | null = null;\n\tif (row.meta) {\n\t\ttry {\n\t\t\tmeta = JSON.parse(row.meta) as Record<string, unknown>;\n\t\t} catch {\n\t\t\tmeta = null;\n\t\t}\n\t}\n\treturn {\n\t\tid: row.id,\n\t\tts: row.ts,\n\t\tday: row.day,\n\t\tkind: row.kind,\n\t\tkey: row.key,\n\t\tquantity: Number(row.quantity) || 0,\n\t\tcostMicros: Number(row.cost_micros) || 0,\n\t\tchargeMicros: Number(row.charge_micros) || 0,\n\t\tactorId: row.actor_id,\n\t\tmeta,\n\t\tsource: row.source,\n\t\tref: row.ref,\n\t};\n}\n\n/**\n * Reads and appends to the per-instance billing ledger (`_emdash_usage`).\n *\n * The ledger is append-only. Charges (usage) are positive `charge_micros`;\n * credit purchases are negative — so the running balance is `-SUM(charge)`.\n * The parent that hosts this instance normally writes usage rows straight into\n * this table over the Cloudflare D1 API; this repository is the read side the\n * admin screens use, plus an idempotent `record` for in-process callers.\n */\nexport class UsageRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Append a ledger row. When `ref` is set the write is idempotent (a repeat\n\t * with the same ref is ignored), so a re-run of a daily usage sync cannot\n\t * double-charge. Returns whether a new row was inserted.\n\t */\n\tasync record(input: RecordUsageInput): Promise<boolean> {\n\t\tconst ts = input.ts ?? new Date().toISOString();\n\t\tconst day = ts.slice(0, 10);\n\t\tconst values = {\n\t\t\tid: ulid(),\n\t\t\tts,\n\t\t\tday,\n\t\t\tkind: input.kind,\n\t\t\tkey: input.key,\n\t\t\tquantity: input.quantity ?? 0,\n\t\t\tcost_micros: Math.round(input.costMicros ?? 0),\n\t\t\tcharge_micros: Math.round(input.chargeMicros ?? 0),\n\t\t\tactor_id: input.actorId ?? null,\n\t\t\tmeta: input.meta ? JSON.stringify(input.meta) : null,\n\t\t\tsource: input.source ?? \"system\",\n\t\t\tref: input.ref ?? null,\n\t\t};\n\t\tconst res = await this.db\n\t\t\t.insertInto(\"_emdash_usage\")\n\t\t\t.values(values)\n\t\t\t.onConflict((oc) => oc.column(\"ref\").doNothing())\n\t\t\t.executeTakeFirst();\n\t\treturn Number(res?.numInsertedOrUpdatedRows ?? 0n) > 0;\n\t}\n\n\t/** Current balance in micro-dollars (`purchased - spent`). */\n\tasync balanceMicros(): Promise<number> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_usage\")\n\t\t\t.select((eb) => eb.fn.sum<number>(\"charge_micros\").as(\"charged\"))\n\t\t\t.executeTakeFirst();\n\t\treturn -(Number(row?.charged) || 0);\n\t}\n\n\t/** Aggregate view for the billing screen. `recentLimit` caps the tail. */\n\tasync summary(recentLimit = 25): Promise<BillingSummary> {\n\t\tconst totals = await this.db\n\t\t\t.selectFrom(\"_emdash_usage\")\n\t\t\t.select((eb) => [\n\t\t\t\teb.fn\n\t\t\t\t\t.sum<number>(eb.case().when(\"charge_micros\", \">\", 0).then(eb.ref(\"charge_micros\")).else(0).end())\n\t\t\t\t\t.as(\"spent\"),\n\t\t\t\teb.fn\n\t\t\t\t\t.sum<number>(eb.case().when(\"charge_micros\", \"<\", 0).then(eb.ref(\"charge_micros\")).else(0).end())\n\t\t\t\t\t.as(\"credited\"),\n\t\t\t\teb.fn.sum<number>(\"cost_micros\").as(\"cost\"),\n\t\t\t\teb.fn.sum<number>(\"charge_micros\").as(\"net\"),\n\t\t\t])\n\t\t\t.executeTakeFirst();\n\n\t\tconst spentMicros = Number(totals?.spent) || 0;\n\t\tconst purchasedMicros = -(Number(totals?.credited) || 0);\n\t\tconst costMicros = Number(totals?.cost) || 0;\n\t\tconst balanceMicros = -(Number(totals?.net) || 0);\n\n\t\tconst byKeyRows = await this.db\n\t\t\t.selectFrom(\"_emdash_usage\")\n\t\t\t.where(\"charge_micros\", \">\", 0)\n\t\t\t.select((eb) => [\n\t\t\t\t\"key\",\n\t\t\t\teb.fn.sum<number>(\"quantity\").as(\"quantity\"),\n\t\t\t\teb.fn.sum<number>(\"charge_micros\").as(\"charge\"),\n\t\t\t])\n\t\t\t.groupBy(\"key\")\n\t\t\t.orderBy(\"charge\", \"desc\")\n\t\t\t.execute();\n\n\t\tconst dailyRows = await this.db\n\t\t\t.selectFrom(\"_emdash_usage\")\n\t\t\t.select((eb) => [\n\t\t\t\t\"day\",\n\t\t\t\teb.fn\n\t\t\t\t\t.sum<number>(eb.case().when(\"charge_micros\", \">\", 0).then(eb.ref(\"charge_micros\")).else(0).end())\n\t\t\t\t\t.as(\"charge\"),\n\t\t\t\teb.fn.sum<number>(\"cost_micros\").as(\"cost\"),\n\t\t\t])\n\t\t\t.groupBy(\"day\")\n\t\t\t.orderBy(\"day\", \"asc\")\n\t\t\t.execute();\n\n\t\tconst recentRows = await this.db\n\t\t\t.selectFrom(\"_emdash_usage\")\n\t\t\t.selectAll()\n\t\t\t.orderBy(\"ts\", \"desc\")\n\t\t\t.limit(recentLimit)\n\t\t\t.execute();\n\n\t\treturn {\n\t\t\tbalanceMicros,\n\t\t\tpurchasedMicros,\n\t\t\tspentMicros,\n\t\t\tcostMicros,\n\t\t\tbyKey: byKeyRows.map((r) => ({\n\t\t\t\tkey: r.key,\n\t\t\t\tquantity: Number(r.quantity) || 0,\n\t\t\t\tchargeMicros: Number(r.charge) || 0,\n\t\t\t})),\n\t\t\tdaily: dailyRows.map((r) => ({\n\t\t\t\tday: r.day,\n\t\t\t\tchargeMicros: Number(r.charge) || 0,\n\t\t\t\tcostMicros: Number(r.cost) || 0,\n\t\t\t})),\n\t\t\trecent: recentRows.map(toEntry),\n\t\t};\n\t}\n\n\t/**\n\t * Paginated ledger listing for the immutable-log screen, newest first.\n\t * `before` is an exclusive `ts` cursor (pass the last row's `ts` to page).\n\t */\n\tasync list(opts: { limit?: number; before?: string } = {}): Promise<{\n\t\tentries: UsageEntry[];\n\t\tnextCursor: string | null;\n\t}> {\n\t\tconst limit = Math.min(Math.max(opts.limit ?? 100, 1), 500);\n\t\tlet q = this.db.selectFrom(\"_emdash_usage\").selectAll().orderBy(\"ts\", \"desc\").limit(limit + 1);\n\t\tif (opts.before) q = q.where(\"ts\", \"<\", opts.before);\n\t\tconst rows = await q.execute();\n\t\tconst hasMore = rows.length > limit;\n\t\tconst page = hasMore ? rows.slice(0, limit) : rows;\n\t\treturn {\n\t\t\tentries: page.map(toEntry),\n\t\t\tnextCursor: hasMore ? page[page.length - 1].ts : null,\n\t\t};\n\t}\n}\n","/**\n * Billing + immutable-log handlers.\n *\n * Both read the per-instance billing ledger (`_emdash_usage`), which the parent\n * that hosts this instance populates over the Cloudflare D1 API (cost-plus\n * charges for real running cost, plus credit top-ups as negative rows). The\n * running balance is `purchased - spent`.\n *\n * The credit price book, markup and enforcement flag are pushed into the\n * instance's `options` (`credits:*`) by the parent; the `/billing` screen shows\n * them read-only. `billing:*` options carry where a top-up should be sent (the\n * parent's checkout endpoint + this instance's id in the parent's registry).\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { AuditRepository } from \"../../database/repositories/audit.js\";\nimport { OptionsRepository } from \"../../database/repositories/options.js\";\nimport { UsageRepository, type BillingSummary, type UsageEntry } from \"../../database/repositories/usage.js\";\nimport type { Database } from \"../../database/types.js\";\nimport type { ApiResult } from \"../types.js\";\n\n/** Credit configuration the parent pushes into this instance. */\nexport interface CreditsConfig {\n\t/** Per-unit charge (micro-dollars) by metric key — informational display. */\n\tprices: Record<string, number>;\n\t/** Cost-plus multiplier applied to provider cost (e.g. 2 = 100% markup). */\n\tmarkup: number;\n\t/** When true, the instance is metered and suspended once credits run out. */\n\tenforce: boolean;\n}\n\n/** Where a credit top-up is sent (the hosting parent's checkout). */\nexport interface TopupTarget {\n\t/** Parent checkout endpoint, e.g. https://premium-cms.com/…/billing/checkout. */\n\turl: string | null;\n\t/** This instance's project id in the parent's registry. */\n\tprojectId: string | null;\n\t/** ISO currency, default USD. */\n\tcurrency: string;\n\t/** Suggested top-up amounts in whole currency units (dollars). */\n\tpresets: number[];\n}\n\nexport interface BillingResponse extends BillingSummary {\n\tcredits: CreditsConfig;\n\ttopup: TopupTarget;\n}\n\nconst DEFAULT_PRESETS = [10, 25, 50, 100];\n\nexport async function handleBillingSummary(\n\tdb: Kysely<Database>,\n): Promise<ApiResult<BillingResponse>> {\n\tconst usage = new UsageRepository(db);\n\tconst options = new OptionsRepository(db);\n\n\tconst [summary, prices, markup, enforce, parentUrl, projectId, currency, presets] =\n\t\tawait Promise.all([\n\t\t\tusage.summary(),\n\t\t\toptions.getOrDefault<Record<string, number>>(\"credits:prices\", {}),\n\t\t\toptions.getOrDefault<number>(\"credits:markup\", 2),\n\t\t\toptions.getOrDefault<boolean>(\"credits:enforce\", false),\n\t\t\toptions.getOrDefault<string | null>(\"billing:parent_url\", null),\n\t\t\toptions.getOrDefault<string | null>(\"billing:project_id\", null),\n\t\t\toptions.getOrDefault<string>(\"billing:currency\", \"USD\"),\n\t\t\toptions.getOrDefault<number[]>(\"billing:presets\", DEFAULT_PRESETS),\n\t\t]);\n\n\treturn {\n\t\tsuccess: true,\n\t\tdata: {\n\t\t\t...summary,\n\t\t\tcredits: { prices: prices ?? {}, markup: Number(markup) || 2, enforce: Boolean(enforce) },\n\t\t\ttopup: {\n\t\t\t\turl: parentUrl,\n\t\t\t\tprojectId,\n\t\t\t\tcurrency: currency || \"USD\",\n\t\t\t\tpresets: Array.isArray(presets) && presets.length ? presets : DEFAULT_PRESETS,\n\t\t\t},\n\t\t},\n\t};\n}\n\n/** A unified immutable-log row: either a billing ledger row or an audit event. */\nexport interface LogEntry {\n\tid: string;\n\tts: string;\n\t/** \"billing\" for ledger rows, \"audit\" for admin-action rows. */\n\tchannel: \"billing\" | \"audit\";\n\t/** Ledger key or audit action. */\n\tkey: string;\n\tkind: string;\n\tactorId: string | null;\n\t/** Micro-dollar charge for billing rows; 0 for audit rows. */\n\tchargeMicros: number;\n\tquantity: number;\n\tdetails: Record<string, unknown> | null;\n}\n\nfunction usageToLog(e: UsageEntry): LogEntry {\n\treturn {\n\t\tid: e.id,\n\t\tts: e.ts,\n\t\tchannel: \"billing\",\n\t\tkey: e.key,\n\t\tkind: e.kind,\n\t\tactorId: e.actorId,\n\t\tchargeMicros: e.chargeMicros,\n\t\tquantity: e.quantity,\n\t\tdetails: e.meta,\n\t};\n}\n\n/**\n * Immutable log — \"logs everything\": the billing ledger merged with the\n * admin audit trail, newest first. `before` is an exclusive ISO-timestamp\n * cursor spanning both channels.\n */\nexport async function handleImmutableLog(\n\tdb: Kysely<Database>,\n\topts: { limit?: number; before?: string } = {},\n): Promise<ApiResult<{ entries: LogEntry[]; nextCursor: string | null }>> {\n\tconst limit = Math.min(Math.max(opts.limit ?? 100, 1), 300);\n\tconst usage = new UsageRepository(db);\n\tconst audit = new AuditRepository(db);\n\n\t// Over-fetch both channels by `limit`, merge, then trim — so the merged\n\t// page is correct even when one channel dominates a time window.\n\tconst [ledger, auditPage] = await Promise.all([\n\t\tusage.list({ limit, before: opts.before }),\n\t\taudit.findMany({ limit, until: opts.before }),\n\t]);\n\n\tconst auditLogs: LogEntry[] = auditPage.items.map((a) => ({\n\t\tid: a.id,\n\t\tts: a.timestamp,\n\t\tchannel: \"audit\",\n\t\tkey: a.action,\n\t\tkind: a.status ?? \"event\",\n\t\tactorId: a.actorId,\n\t\tchargeMicros: 0,\n\t\tquantity: 0,\n\t\tdetails: {\n\t\t\t...(a.details ?? {}),\n\t\t\t...(a.resourceType ? { resourceType: a.resourceType } : {}),\n\t\t\t...(a.resourceId ? { resourceId: a.resourceId } : {}),\n\t\t\t...(a.actorIp ? { actorIp: a.actorIp } : {}),\n\t\t},\n\t}));\n\n\tconst merged = [...ledger.entries.map(usageToLog), ...auditLogs]\n\t\t.sort((x, y) => (x.ts < y.ts ? 1 : x.ts > y.ts ? -1 : 0))\n\t\t.slice(0, limit);\n\n\tconst nextCursor = merged.length === limit ? merged[merged.length - 1].ts : null;\n\n\treturn { success: true, data: { entries: merged, nextCursor } };\n}\n"],"mappings":"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"}