mcp-scraper 0.21.3 → 0.21.4

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.
Files changed (56) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/api-server.cjs +728 -181
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +3 -3
  5. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  6. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  7. package/dist/bin/mcp-scraper-cli.js +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs +1 -1
  9. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-install.js +1 -1
  11. package/dist/bin/mcp-stdio-server.cjs +97 -32
  12. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-stdio-server.js +5 -5
  14. package/dist/bin/paa-harvest.cjs +21 -0
  15. package/dist/bin/paa-harvest.cjs.map +1 -1
  16. package/dist/bin/paa-harvest.js +3 -3
  17. package/dist/{chunk-JJ5RMEGX.js → chunk-E5J4HJBO.js} +2 -2
  18. package/dist/{chunk-H7APH5UG.js → chunk-ICT7DDHL.js} +366 -50
  19. package/dist/chunk-ICT7DDHL.js.map +1 -0
  20. package/dist/{chunk-OBNKOYOE.js → chunk-JHI373VB.js} +7 -5
  21. package/dist/chunk-JHI373VB.js.map +1 -0
  22. package/dist/{chunk-5IU5ZFVO.js → chunk-MTSBI7ZH.js} +23 -6
  23. package/dist/chunk-MTSBI7ZH.js.map +1 -0
  24. package/dist/{chunk-7LO6KLKD.js → chunk-N4JHXPW6.js} +89 -27
  25. package/dist/chunk-N4JHXPW6.js.map +1 -0
  26. package/dist/{chunk-XDRHBGJR.js → chunk-PNBYYAEK.js} +3 -3
  27. package/dist/{chunk-2P5VFIJM.js → chunk-SDHPQTFM.js} +2 -2
  28. package/dist/chunk-WQI7JIIN.js +7 -0
  29. package/dist/chunk-WQI7JIIN.js.map +1 -0
  30. package/dist/{db-Z35S224E.js → db-WRILVOFY.js} +4 -2
  31. package/dist/{extract-bundle-4RO3YU4Q.js → extract-bundle-NFFVMUMT.js} +2 -2
  32. package/dist/index.cjs +21 -0
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.js +3 -3
  35. package/dist/{server-PBBRSLBP.js → server-OLOOFN5W.js} +246 -100
  36. package/dist/server-OLOOFN5W.js.map +1 -0
  37. package/dist/{site-extract-repository-FDH6ZXKT.js → site-extract-repository-2QQSL2QC.js} +4 -4
  38. package/dist/{worker-BXJQ7J4N.js → worker-RV2TYXE3.js} +9 -8
  39. package/dist/worker-RV2TYXE3.js.map +1 -0
  40. package/docs/mcp-tool-manifest.generated.json +90 -11
  41. package/docs/specs/oauth-mcp-spec.md +2 -2
  42. package/package.json +1 -1
  43. package/dist/chunk-5IU5ZFVO.js.map +0 -1
  44. package/dist/chunk-7LO6KLKD.js.map +0 -1
  45. package/dist/chunk-H7APH5UG.js.map +0 -1
  46. package/dist/chunk-OBNKOYOE.js.map +0 -1
  47. package/dist/chunk-VYUWWIVR.js +0 -7
  48. package/dist/chunk-VYUWWIVR.js.map +0 -1
  49. package/dist/server-PBBRSLBP.js.map +0 -1
  50. package/dist/worker-BXJQ7J4N.js.map +0 -1
  51. /package/dist/{chunk-JJ5RMEGX.js.map → chunk-E5J4HJBO.js.map} +0 -0
  52. /package/dist/{chunk-XDRHBGJR.js.map → chunk-PNBYYAEK.js.map} +0 -0
  53. /package/dist/{chunk-2P5VFIJM.js.map → chunk-SDHPQTFM.js.map} +0 -0
  54. /package/dist/{db-Z35S224E.js.map → db-WRILVOFY.js.map} +0 -0
  55. /package/dist/{extract-bundle-4RO3YU4Q.js.map → extract-bundle-NFFVMUMT.js.map} +0 -0
  56. /package/dist/{site-extract-repository-FDH6ZXKT.js.map → site-extract-repository-2QQSL2QC.js.map} +0 -0
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  harvest
4
- } from "../chunk-JJ5RMEGX.js";
4
+ } from "../chunk-E5J4HJBO.js";
5
5
  import {
6
6
  browserServiceApiKey
7
- } from "../chunk-5IU5ZFVO.js";
7
+ } from "../chunk-MTSBI7ZH.js";
8
8
  import "../chunk-K443GQY5.js";
9
9
  import "../chunk-XGIPATLV.js";
10
10
  import "../chunk-M2S27J6Z.js";
11
- import "../chunk-H7APH5UG.js";
11
+ import "../chunk-ICT7DDHL.js";
12
12
 
13
13
  // src/cli.ts
14
14
  import { Command } from "commander";
@@ -5,7 +5,7 @@ import {
5
5
  currentCostContext,
6
6
  recordKernelSession,
7
7
  runWithCostContext
8
- } from "./chunk-5IU5ZFVO.js";
8
+ } from "./chunk-MTSBI7ZH.js";
9
9
  import {
10
10
  postToMemoryLibrary
11
11
  } from "./chunk-K443GQY5.js";
@@ -3159,4 +3159,4 @@ export {
3159
3159
  resolveKernelProxyId,
3160
3160
  harvest
3161
3161
  };
3162
- //# sourceMappingURL=chunk-JJ5RMEGX.js.map
3162
+ //# sourceMappingURL=chunk-E5J4HJBO.js.map
@@ -6,6 +6,108 @@ var DB_URL = process.env.TURSO_DATABASE_URL ?? "file:./paa-api.db";
6
6
  var DB_TOKEN = process.env.TURSO_AUTH_TOKEN;
7
7
  var _db = null;
8
8
  var _rateLimitSchemaReady = false;
9
+ var CORE_SCHEMA_VERSION = "2026-07-14.1";
10
+ var CORE_SCHEMA_TABLES = [
11
+ "billing_authorizations",
12
+ "billing_debits",
13
+ "billing_events",
14
+ "concurrency_locks",
15
+ "credit_lots",
16
+ "free_credit_refreshes",
17
+ "harvest_attempts",
18
+ "jobs",
19
+ "kpo_jobs",
20
+ "kpo_phase_log",
21
+ "ledger",
22
+ "oauth_clients",
23
+ "oauth_codes",
24
+ "oauth_tokens",
25
+ "page_snapshots",
26
+ "password_reset_tokens",
27
+ "rate_limits",
28
+ "request_events",
29
+ "site_audit_jobs",
30
+ "site_audit_phase_log",
31
+ "site_extract_jobs",
32
+ "site_extract_pages",
33
+ "stripe_events",
34
+ "users",
35
+ "workflow_artifacts",
36
+ "workflow_runs",
37
+ "workflow_schedules",
38
+ "workflow_webhook_deliveries"
39
+ ];
40
+ var CORE_SCHEMA_INDEXES = [
41
+ "billing_authorizations_user_status",
42
+ "billing_debits_user_status",
43
+ "billing_events_authorization",
44
+ "billing_events_user_created_at",
45
+ "concurrency_locks_operation",
46
+ "concurrency_locks_user_status",
47
+ "credit_lots_user_active",
48
+ "harvest_attempts_job_id",
49
+ "harvest_attempts_user_created_at",
50
+ "jobs_status",
51
+ "jobs_user_id",
52
+ "kpo_jobs_status",
53
+ "kpo_jobs_user_id",
54
+ "kpo_phase_log_job_id",
55
+ "ledger_stripe_pi_unique",
56
+ "ledger_user_id",
57
+ "oauth_tokens_identity",
58
+ "page_snapshots_user_checked_at",
59
+ "prt_user_id",
60
+ "request_events_user_created_at",
61
+ "site_audit_jobs_status",
62
+ "site_audit_jobs_user_id",
63
+ "site_audit_phase_log_job_id",
64
+ "site_extract_jobs_status",
65
+ "site_extract_jobs_user_id",
66
+ "site_extract_pages_job_id",
67
+ "workflow_artifacts_run",
68
+ "workflow_runs_idempotency",
69
+ "workflow_runs_schedule",
70
+ "workflow_runs_user_created",
71
+ "workflow_schedules_due",
72
+ "workflow_schedules_user_status",
73
+ "workflow_webhook_deliveries_run"
74
+ ];
75
+ var CORE_SCHEMA_COLUMNS = {
76
+ harvest_attempts: ["debug_json"],
77
+ oauth_tokens: ["replaced_by", "rotated_at"],
78
+ users: [
79
+ "balance_mc",
80
+ "concurrency_stripe_sub_id",
81
+ "extra_concurrency_slots",
82
+ "key_active",
83
+ "memory_identity",
84
+ "memory_key",
85
+ "memory_plan",
86
+ "memory_provisioned",
87
+ "memory_subscription_id",
88
+ "password_hash",
89
+ "stripe_customer_id",
90
+ "subscription_concurrency",
91
+ "subscription_id",
92
+ "subscription_tier"
93
+ ],
94
+ workflow_artifacts: ["content"],
95
+ workflow_runs: ["step_index", "step_log_json", "step_state_json", "total_steps", "webhook_url"]
96
+ };
97
+ function sqlStringList(values) {
98
+ return values.map((value) => `'${value.replaceAll("'", "''")}'`).join(", ");
99
+ }
100
+ async function hasCurrentCoreSchema(db) {
101
+ const checks = [
102
+ `(SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name IN (${sqlStringList(CORE_SCHEMA_TABLES)})) = ${CORE_SCHEMA_TABLES.length}`,
103
+ `(SELECT COUNT(*) FROM sqlite_master WHERE type = 'index' AND name IN (${sqlStringList(CORE_SCHEMA_INDEXES)})) = ${CORE_SCHEMA_INDEXES.length}`,
104
+ ...Object.entries(CORE_SCHEMA_COLUMNS).map(
105
+ ([table, columns]) => `(SELECT COUNT(*) FROM pragma_table_info('${table}') WHERE name IN (${sqlStringList(columns)})) = ${columns.length}`
106
+ )
107
+ ];
108
+ const result = await db.execute(`SELECT ${checks.join(" AND ")} AS ready`);
109
+ return Number(result.rows[0]?.ready ?? 0) === 1;
110
+ }
9
111
  function getDb() {
10
112
  if (!_db) _db = createClient({ url: DB_URL, authToken: DB_TOKEN });
11
113
  return _db;
@@ -15,6 +117,31 @@ function setDbForTesting(client) {
15
117
  }
16
118
  async function migrate() {
17
119
  const db = getDb();
120
+ let applied;
121
+ try {
122
+ applied = await db.execute({
123
+ sql: "SELECT 1 AS applied FROM schema_migrations WHERE version = ? LIMIT 1",
124
+ args: [CORE_SCHEMA_VERSION]
125
+ });
126
+ } catch (error) {
127
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
128
+ if (!message.includes("no such table") || !message.includes("schema_migrations")) throw error;
129
+ await db.execute(`
130
+ CREATE TABLE IF NOT EXISTS schema_migrations (
131
+ version TEXT PRIMARY KEY,
132
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
133
+ )
134
+ `);
135
+ applied = { rows: [] };
136
+ }
137
+ if (applied.rows.length > 0) return;
138
+ if (await hasCurrentCoreSchema(db)) {
139
+ await db.execute({
140
+ sql: "INSERT OR IGNORE INTO schema_migrations (version) VALUES (?)",
141
+ args: [CORE_SCHEMA_VERSION]
142
+ });
143
+ return;
144
+ }
18
145
  await db.execute(`
19
146
  CREATE TABLE IF NOT EXISTS users (
20
147
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -508,6 +635,13 @@ async function migrate() {
508
635
  )
509
636
  `);
510
637
  await db.execute(`CREATE INDEX IF NOT EXISTS page_snapshots_user_checked_at ON page_snapshots(user_id, checked_at DESC)`);
638
+ if (!await hasCurrentCoreSchema(db)) {
639
+ throw new Error("core schema migration finished without all required tables, indexes, and columns");
640
+ }
641
+ await db.execute({
642
+ sql: "INSERT OR IGNORE INTO schema_migrations (version) VALUES (?)",
643
+ args: [CORE_SCHEMA_VERSION]
644
+ });
511
645
  }
512
646
  async function registerClient(clientId, redirectUris, name) {
513
647
  await getDb().execute({
@@ -970,6 +1104,18 @@ async function upsertPageSnapshot(userId, url, snapshot) {
970
1104
  args: [userId, url, snapshot.contentHash, snapshot.content, snapshot.title, snapshot.contentBytes, snapshot.truncated ? 1 : 0]
971
1105
  });
972
1106
  }
1107
+ var REQUEST_EVENT_RESULT_MAX_BYTES = 32 * 1024;
1108
+ function serializeRequestEventResult(result) {
1109
+ if (result == null) return null;
1110
+ const serialized = JSON.stringify(result);
1111
+ const bytes = Buffer.byteLength(serialized, "utf8");
1112
+ if (bytes <= REQUEST_EVENT_RESULT_MAX_BYTES) return serialized;
1113
+ return JSON.stringify({
1114
+ truncated: true,
1115
+ originalBytes: bytes,
1116
+ sha256: createHash("sha256").update(serialized).digest("hex")
1117
+ });
1118
+ }
973
1119
  function rowToRequestEvent(row) {
974
1120
  const rawResult = row.result != null ? String(row.result) : null;
975
1121
  return {
@@ -1000,7 +1146,7 @@ async function logRequestEvent(input) {
1000
1146
  input.query,
1001
1147
  input.location ?? null,
1002
1148
  input.resultCount ?? null,
1003
- input.result != null ? JSON.stringify(input.result) : null,
1149
+ serializeRequestEventResult(input.result),
1004
1150
  input.error ?? null
1005
1151
  ]
1006
1152
  });
@@ -1009,7 +1155,7 @@ async function logRequestEvent(input) {
1009
1155
  async function listRequestEvents(userId, limit = 100) {
1010
1156
  const res = await getDb().execute({
1011
1157
  sql: `
1012
- SELECT id, user_id, source, status, query, location, result_count, result, error, created_at
1158
+ SELECT id, user_id, source, status, query, location, result_count, NULL AS result, error, created_at
1013
1159
  FROM request_events
1014
1160
  WHERE user_id = ?
1015
1161
  ORDER BY created_at DESC
@@ -1638,58 +1784,75 @@ async function debitMcIdempotent(userId, mc, operation, description, idempotency
1638
1784
  if (!idempotencyKey || idempotencyKey.length > 500) throw new Error("idempotent debit key is required and must be bounded");
1639
1785
  const db = getDb();
1640
1786
  await ensureMigrated(userId);
1641
- const results = await db.batch([
1642
- {
1643
- sql: "UPDATE billing_debits SET amount_mc = ?, description = ? WHERE idempotency_key = ? AND user_id = ? AND operation = ? AND status = 'claimed'",
1644
- args: [mc, description, idempotencyKey, userId, operation]
1645
- },
1646
- {
1647
- sql: "INSERT OR IGNORE INTO billing_debits (idempotency_key, user_id, amount_mc, operation, description, status) VALUES (?, ?, ?, ?, ?, 'claimed')",
1648
- args: [idempotencyKey, userId, mc, operation, description]
1649
- },
1650
- {
1651
- sql: "UPDATE users SET balance_mc = balance_mc - ? WHERE id = ? AND balance_mc >= ? AND EXISTS (SELECT 1 FROM billing_debits WHERE idempotency_key = ? AND user_id = ? AND amount_mc = ? AND operation = ? AND status = 'claimed')",
1652
- args: [mc, userId, mc, idempotencyKey, userId, mc, operation]
1653
- },
1654
- {
1655
- sql: "UPDATE billing_debits SET status = 'applying' WHERE idempotency_key = ? AND user_id = ? AND amount_mc = ? AND operation = ? AND status = 'claimed' AND changes() = 1",
1656
- args: [idempotencyKey, userId, mc, operation]
1657
- },
1658
- {
1659
- sql: `WITH ordered AS MATERIALIZED (
1660
- SELECT id,
1661
- remaining_mc,
1662
- COALESCE(SUM(remaining_mc) OVER (
1663
- ORDER BY expires_at ASC, granted_at ASC, id ASC
1664
- ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
1665
- ), 0) AS prior_mc
1666
- FROM credit_lots
1667
- WHERE user_id = ? AND remaining_mc > 0 AND expires_at > datetime('now')
1668
- ), allocation AS (
1669
- SELECT id, MIN(remaining_mc, MAX(0, ? - prior_mc)) AS take_mc
1670
- FROM ordered
1671
- )
1672
- UPDATE credit_lots
1673
- SET remaining_mc = remaining_mc - COALESCE((SELECT take_mc FROM allocation WHERE allocation.id = credit_lots.id), 0)
1674
- WHERE id IN (SELECT id FROM allocation WHERE take_mc > 0)
1675
- AND EXISTS (SELECT 1 FROM billing_debits WHERE idempotency_key = ? AND status = 'applying')`,
1676
- args: [userId, mc, idempotencyKey]
1677
- },
1678
- {
1679
- sql: "INSERT INTO ledger (user_id, amount_mc, operation, description) SELECT user_id, -amount_mc, operation, description FROM billing_debits WHERE idempotency_key = ? AND status = 'applying'",
1680
- args: [idempotencyKey]
1681
- },
1682
- {
1683
- sql: "UPDATE billing_debits SET status = 'applied', applied_at = datetime('now') WHERE idempotency_key = ? AND status = 'applying'",
1684
- args: [idempotencyKey]
1787
+ let results;
1788
+ try {
1789
+ results = await db.batch([
1790
+ {
1791
+ sql: "UPDATE billing_debits SET amount_mc = ? WHERE idempotency_key = ? AND user_id = ? AND operation = ? AND description = ? AND status = 'claimed'",
1792
+ args: [mc, idempotencyKey, userId, operation, description]
1793
+ },
1794
+ {
1795
+ sql: "INSERT OR IGNORE INTO billing_debits (idempotency_key, user_id, amount_mc, operation, description, status) VALUES (?, ?, ?, ?, ?, 'claimed')",
1796
+ args: [idempotencyKey, userId, mc, operation, description]
1797
+ },
1798
+ {
1799
+ sql: "UPDATE users SET balance_mc = balance_mc - ? WHERE id = ? AND balance_mc >= ? AND EXISTS (SELECT 1 FROM billing_debits WHERE idempotency_key = ? AND user_id = ? AND amount_mc = ? AND operation = ? AND description = ? AND status = 'claimed')",
1800
+ args: [mc, userId, mc, idempotencyKey, userId, mc, operation, description]
1801
+ },
1802
+ {
1803
+ sql: "UPDATE billing_debits SET status = 'applying' WHERE idempotency_key = ? AND user_id = ? AND amount_mc = ? AND operation = ? AND description = ? AND status = 'claimed' AND changes() = 1",
1804
+ args: [idempotencyKey, userId, mc, operation, description]
1805
+ },
1806
+ {
1807
+ sql: `WITH ordered AS MATERIALIZED (
1808
+ SELECT id,
1809
+ remaining_mc,
1810
+ COALESCE(SUM(remaining_mc) OVER (
1811
+ ORDER BY expires_at ASC, granted_at ASC, id ASC
1812
+ ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
1813
+ ), 0) AS prior_mc
1814
+ FROM credit_lots
1815
+ WHERE user_id = ? AND remaining_mc > 0 AND expires_at > datetime('now')
1816
+ ), allocation AS (
1817
+ SELECT id, MIN(remaining_mc, MAX(0, ? - prior_mc)) AS take_mc
1818
+ FROM ordered
1819
+ )
1820
+ UPDATE credit_lots
1821
+ SET remaining_mc = remaining_mc - COALESCE((SELECT take_mc FROM allocation WHERE allocation.id = credit_lots.id), 0)
1822
+ WHERE id IN (SELECT id FROM allocation WHERE take_mc > 0)
1823
+ AND EXISTS (SELECT 1 FROM billing_debits WHERE idempotency_key = ? AND status = 'applying')`,
1824
+ args: [userId, mc, idempotencyKey]
1825
+ },
1826
+ {
1827
+ sql: "INSERT INTO ledger (user_id, amount_mc, operation, description) SELECT user_id, -amount_mc, operation, description FROM billing_debits WHERE idempotency_key = ? AND status = 'applying'",
1828
+ args: [idempotencyKey]
1829
+ },
1830
+ {
1831
+ sql: "UPDATE billing_debits SET status = 'applied', applied_at = datetime('now') WHERE idempotency_key = ? AND status = 'applying'",
1832
+ args: [idempotencyKey]
1833
+ }
1834
+ ], "write");
1835
+ } catch (error) {
1836
+ try {
1837
+ const recovered = await db.execute({
1838
+ sql: "SELECT user_id, amount_mc, operation, description, status FROM billing_debits WHERE idempotency_key = ? AND status = 'applied' LIMIT 1",
1839
+ args: [idempotencyKey]
1840
+ });
1841
+ const row2 = recovered.rows[0];
1842
+ if (row2 && Number(row2.user_id) === userId && Number(row2.amount_mc) === mc && String(row2.operation) === operation && String(row2.description) === description) {
1843
+ const balance2 = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
1844
+ return { ok: true, balance_mc: Number(balance2.rows[0]?.balance_mc ?? 0), duplicate: true };
1845
+ }
1846
+ } catch {
1685
1847
  }
1686
- ], "write");
1848
+ throw error;
1849
+ }
1687
1850
  const claim = await db.execute({
1688
- sql: "SELECT user_id, amount_mc, operation, status FROM billing_debits WHERE idempotency_key = ? LIMIT 1",
1851
+ sql: "SELECT user_id, amount_mc, operation, description, status FROM billing_debits WHERE idempotency_key = ? LIMIT 1",
1689
1852
  args: [idempotencyKey]
1690
1853
  });
1691
1854
  const row = claim.rows[0];
1692
- if (!row || Number(row.user_id) !== userId || Number(row.amount_mc) !== mc || String(row.operation) !== operation) {
1855
+ if (!row || Number(row.user_id) !== userId || Number(row.amount_mc) !== mc || String(row.operation) !== operation || String(row.description) !== description) {
1693
1856
  throw new Error("idempotent debit key conflicts with another debit");
1694
1857
  }
1695
1858
  const balance = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
@@ -1699,6 +1862,158 @@ async function debitMcIdempotent(userId, mc, operation, description, idempotency
1699
1862
  duplicate: results[2]?.rowsAffected === 0 && String(row.status) === "applied"
1700
1863
  };
1701
1864
  }
1865
+ async function settleDebitMcIdempotent(userId, debitIdempotencyKey, finalAmountMc, refundOperation, description, sourceSurface) {
1866
+ if (!Number.isSafeInteger(finalAmountMc) || finalAmountMc < 0) {
1867
+ throw new Error("idempotent debit settlement amount must be a non-negative integer");
1868
+ }
1869
+ if (!debitIdempotencyKey || debitIdempotencyKey.length > 500) {
1870
+ throw new Error("idempotent debit settlement key is required and must be bounded");
1871
+ }
1872
+ if (!refundOperation || !sourceSurface) {
1873
+ throw new Error("idempotent debit settlement operation and source are required");
1874
+ }
1875
+ const db = getDb();
1876
+ await ensureMigrated(userId);
1877
+ const settlementKey = `debit-settlement:${createHash("sha256").update(debitIdempotencyKey).digest("hex")}`;
1878
+ const billingClass = "wallet_hold_settlement";
1879
+ const eventId = randomUUID();
1880
+ const metadata = JSON.stringify({ debitIdempotencyKey, refundOperation, description });
1881
+ const readReceipt = async () => {
1882
+ const receipt2 = await db.execute({
1883
+ sql: `SELECT e.user_id,
1884
+ e.status,
1885
+ e.amount_mc AS final_amount_mc,
1886
+ e.billing_class,
1887
+ e.source_surface,
1888
+ d.amount_mc AS held_amount_mc
1889
+ FROM billing_events e
1890
+ JOIN billing_debits d ON d.idempotency_key = ?
1891
+ WHERE e.idempotency_key = ?
1892
+ AND d.user_id = e.user_id
1893
+ AND d.status = 'applied'
1894
+ LIMIT 1`,
1895
+ args: [debitIdempotencyKey, settlementKey]
1896
+ });
1897
+ const row = receipt2.rows[0];
1898
+ if (!row) return null;
1899
+ if (Number(row.user_id) !== userId || String(row.billing_class) !== billingClass || String(row.source_surface) !== sourceSurface) {
1900
+ throw new Error("idempotent debit settlement key conflicts with another settlement");
1901
+ }
1902
+ if (String(row.status) !== "settled") return null;
1903
+ const heldAmountMc = Number(row.held_amount_mc);
1904
+ const durableFinalAmountMc = Number(row.final_amount_mc);
1905
+ if (!Number.isSafeInteger(heldAmountMc) || !Number.isSafeInteger(durableFinalAmountMc) || durableFinalAmountMc < 0 || durableFinalAmountMc > heldAmountMc) {
1906
+ throw new Error("idempotent debit settlement receipt is invalid");
1907
+ }
1908
+ const balance = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
1909
+ return {
1910
+ balance_mc: Number(balance.rows[0]?.balance_mc ?? 0),
1911
+ final_amount_mc: durableFinalAmountMc,
1912
+ refund_mc: heldAmountMc - durableFinalAmountMc,
1913
+ duplicate: true
1914
+ };
1915
+ };
1916
+ let results;
1917
+ try {
1918
+ results = await db.batch([
1919
+ {
1920
+ sql: `INSERT OR IGNORE INTO billing_events
1921
+ (id, user_id, idempotency_key, billing_class, source_surface, status, amount_mc, metadata)
1922
+ VALUES (?, ?, ?, ?, ?, 'settling', ?, ?)`,
1923
+ args: [eventId, userId, settlementKey, billingClass, sourceSurface, finalAmountMc, metadata]
1924
+ },
1925
+ {
1926
+ sql: `UPDATE users
1927
+ SET balance_mc = balance_mc + (
1928
+ SELECT d.amount_mc - e.amount_mc
1929
+ FROM billing_events e
1930
+ JOIN billing_debits d ON d.idempotency_key = ?
1931
+ WHERE e.idempotency_key = ?
1932
+ )
1933
+ WHERE id = ?
1934
+ AND EXISTS (
1935
+ SELECT 1
1936
+ FROM billing_events e
1937
+ JOIN billing_debits d ON d.idempotency_key = ?
1938
+ WHERE e.idempotency_key = ?
1939
+ AND e.user_id = ?
1940
+ AND e.billing_class = ?
1941
+ AND e.source_surface = ?
1942
+ AND e.status = 'settling'
1943
+ AND d.user_id = e.user_id
1944
+ AND d.status = 'applied'
1945
+ AND e.amount_mc BETWEEN 0 AND d.amount_mc
1946
+ )`,
1947
+ args: [
1948
+ debitIdempotencyKey,
1949
+ settlementKey,
1950
+ userId,
1951
+ debitIdempotencyKey,
1952
+ settlementKey,
1953
+ userId,
1954
+ billingClass,
1955
+ sourceSurface
1956
+ ]
1957
+ },
1958
+ {
1959
+ sql: `UPDATE billing_events
1960
+ SET status = 'applying'
1961
+ WHERE idempotency_key = ?
1962
+ AND user_id = ?
1963
+ AND billing_class = ?
1964
+ AND source_surface = ?
1965
+ AND status = 'settling'
1966
+ AND changes() = 1`,
1967
+ args: [settlementKey, userId, billingClass, sourceSurface]
1968
+ },
1969
+ {
1970
+ sql: `INSERT INTO credit_lots
1971
+ (user_id, amount_mc, remaining_mc, source, expires_at)
1972
+ SELECT e.user_id,
1973
+ d.amount_mc - e.amount_mc,
1974
+ d.amount_mc - e.amount_mc,
1975
+ ?,
1976
+ datetime('now', ?)
1977
+ FROM billing_events e
1978
+ JOIN billing_debits d ON d.idempotency_key = ?
1979
+ WHERE e.idempotency_key = ?
1980
+ AND e.status = 'applying'
1981
+ AND d.amount_mc > e.amount_mc`,
1982
+ args: [refundOperation, CREDIT_LOT_TTL, debitIdempotencyKey, settlementKey]
1983
+ },
1984
+ {
1985
+ sql: `INSERT INTO ledger (user_id, amount_mc, operation, description)
1986
+ SELECT e.user_id,
1987
+ d.amount_mc - e.amount_mc,
1988
+ ?,
1989
+ ?
1990
+ FROM billing_events e
1991
+ JOIN billing_debits d ON d.idempotency_key = ?
1992
+ WHERE e.idempotency_key = ?
1993
+ AND e.status = 'applying'
1994
+ AND d.amount_mc > e.amount_mc`,
1995
+ args: [refundOperation, description, debitIdempotencyKey, settlementKey]
1996
+ },
1997
+ {
1998
+ sql: `UPDATE billing_events
1999
+ SET status = 'settled', settled_at = datetime('now')
2000
+ WHERE idempotency_key = ?
2001
+ AND status = 'applying'`,
2002
+ args: [settlementKey]
2003
+ }
2004
+ ], "write");
2005
+ } catch (error) {
2006
+ try {
2007
+ const recovered = await readReceipt();
2008
+ if (recovered) return recovered;
2009
+ } catch {
2010
+ }
2011
+ throw error;
2012
+ }
2013
+ const receipt = await readReceipt();
2014
+ if (!receipt) throw new Error("idempotent debit settlement did not reach a durable settled state");
2015
+ return { ...receipt, duplicate: results[0]?.rowsAffected === 0 };
2016
+ }
1702
2017
  async function getLedger(userId, limit = 50) {
1703
2018
  const res = await getDb().execute({
1704
2019
  sql: "SELECT * FROM ledger WHERE user_id = ? ORDER BY created_at DESC LIMIT ?",
@@ -1864,10 +2179,11 @@ export {
1864
2179
  claimMonthlyFreeRefresh,
1865
2180
  debitMc,
1866
2181
  debitMcIdempotent,
2182
+ settleDebitMcIdempotent,
1867
2183
  getLedger,
1868
2184
  ledgerExistsForStripePI,
1869
2185
  reconcileBalanceMc,
1870
2186
  SiteAuditJobRowSchema,
1871
2187
  SiteAuditPhaseLogRowSchema
1872
2188
  };
1873
- //# sourceMappingURL=chunk-H7APH5UG.js.map
2189
+ //# sourceMappingURL=chunk-ICT7DDHL.js.map