mcp-scraper 0.35.1 → 0.36.0

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 (90) hide show
  1. package/README.md +9 -9
  2. package/dist/bin/api-server.cjs +26406 -21015
  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 +51 -7
  6. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  7. package/dist/bin/mcp-scraper-cli.js +48 -5
  8. package/dist/bin/mcp-scraper-cli.js.map +1 -1
  9. package/dist/bin/mcp-scraper-install.cjs +2 -2
  10. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  11. package/dist/bin/mcp-scraper-install.js +2 -2
  12. package/dist/bin/mcp-stdio-server.cjs +944 -215
  13. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  14. package/dist/bin/mcp-stdio-server.js +8 -8
  15. package/dist/bin/paa-harvest.cjs +125 -70
  16. package/dist/bin/paa-harvest.cjs.map +1 -1
  17. package/dist/bin/paa-harvest.js +4 -4
  18. package/dist/chunk-345BQXZH.js +712 -0
  19. package/dist/chunk-345BQXZH.js.map +1 -0
  20. package/dist/{chunk-M2S27J6Z.js → chunk-44HZLHDV.js} +10 -1
  21. package/dist/chunk-44HZLHDV.js.map +1 -0
  22. package/dist/chunk-4HO66323.js +7 -0
  23. package/dist/chunk-4HO66323.js.map +1 -0
  24. package/dist/{chunk-BWXLTWF7.js → chunk-4ZIJ3BKZ.js} +6 -4
  25. package/dist/chunk-4ZIJ3BKZ.js.map +1 -0
  26. package/dist/{chunk-NPMW5HUS.js → chunk-5RULXBJ7.js} +879 -237
  27. package/dist/chunk-5RULXBJ7.js.map +1 -0
  28. package/dist/chunk-AN3VQARU.js +684 -0
  29. package/dist/chunk-AN3VQARU.js.map +1 -0
  30. package/dist/{chunk-XVVNKASZ.js → chunk-ANCGXUQJ.js} +118 -73
  31. package/dist/chunk-ANCGXUQJ.js.map +1 -0
  32. package/dist/{chunk-3HBPKR5G.js → chunk-D7LM5QZN.js} +3 -3
  33. package/dist/{chunk-4ZB3X6BQ.js → chunk-E5UEELA7.js} +16 -2
  34. package/dist/{chunk-4ZB3X6BQ.js.map → chunk-E5UEELA7.js.map} +1 -1
  35. package/dist/{chunk-ZID3WQID.js → chunk-FQI5PFE7.js} +9 -71
  36. package/dist/chunk-FQI5PFE7.js.map +1 -0
  37. package/dist/chunk-G3P3ZDB4.js +69 -0
  38. package/dist/chunk-G3P3ZDB4.js.map +1 -0
  39. package/dist/{chunk-YRGSEY5L.js → chunk-G7KAVJ3F.js} +2 -2
  40. package/dist/{chunk-YRGSEY5L.js.map → chunk-G7KAVJ3F.js.map} +1 -1
  41. package/dist/{chunk-62DQAWPF.js → chunk-IFYER7O4.js} +367 -42
  42. package/dist/chunk-IFYER7O4.js.map +1 -0
  43. package/dist/chunk-O2MCWFXQ.js +499 -0
  44. package/dist/chunk-O2MCWFXQ.js.map +1 -0
  45. package/dist/chunk-QZXKQB7Y.js +414 -0
  46. package/dist/chunk-QZXKQB7Y.js.map +1 -0
  47. package/dist/{db-YAI5AQOI.js → db-N6MPVMEF.js} +8 -2
  48. package/dist/{extract-bundle-ONWZVV55.js → extract-bundle-M4SDJG3V.js} +284 -98
  49. package/dist/extract-bundle-M4SDJG3V.js.map +1 -0
  50. package/dist/index.cjs +129 -70
  51. package/dist/index.cjs.map +1 -1
  52. package/dist/index.d.cts +11 -0
  53. package/dist/index.d.ts +11 -0
  54. package/dist/index.js +4 -4
  55. package/dist/location-data-repository-Z4NQOU5Y.js +35 -0
  56. package/dist/{server-GKUTC73B.js → server-ZIGAFKOL.js} +10791 -7629
  57. package/dist/server-ZIGAFKOL.js.map +1 -0
  58. package/dist/site-extract-repository-PGQZNW6V.js +62 -0
  59. package/dist/site-extract-repository-PGQZNW6V.js.map +1 -0
  60. package/dist/{worker-645BZPEK.js → worker-EBB6CTGW.js} +7 -7
  61. package/docs/hosted-location-data.md +108 -0
  62. package/docs/mcp-tool-craft-lint.generated.md +6 -3
  63. package/docs/mcp-tool-manifest.generated.json +1253 -222
  64. package/docs/mcp-tool-quality-spec.md +1 -1
  65. package/docs/specs/connected-services-control-plane-decoupling-spec.md +1044 -0
  66. package/docs/specs/kernel-stealth-captcha-test-matrix.md +278 -0
  67. package/docs/specs/multimodal-image-memory-architecture-spec.md +1022 -0
  68. package/docs/specs/unified-credit-and-scheduled-execution-billing-spec.md +36 -27
  69. package/package.json +6 -5
  70. package/dist/chunk-62DQAWPF.js.map +0 -1
  71. package/dist/chunk-BWXLTWF7.js.map +0 -1
  72. package/dist/chunk-M2S27J6Z.js.map +0 -1
  73. package/dist/chunk-NPMW5HUS.js.map +0 -1
  74. package/dist/chunk-R7EETU7Z.js +0 -419
  75. package/dist/chunk-R7EETU7Z.js.map +0 -1
  76. package/dist/chunk-U44TPRST.js +0 -130
  77. package/dist/chunk-U44TPRST.js.map +0 -1
  78. package/dist/chunk-XVVNKASZ.js.map +0 -1
  79. package/dist/chunk-YR4LJ6AQ.js +0 -7
  80. package/dist/chunk-YR4LJ6AQ.js.map +0 -1
  81. package/dist/chunk-YV2FUEBX.js +0 -851
  82. package/dist/chunk-YV2FUEBX.js.map +0 -1
  83. package/dist/chunk-ZID3WQID.js.map +0 -1
  84. package/dist/extract-bundle-ONWZVV55.js.map +0 -1
  85. package/dist/server-GKUTC73B.js.map +0 -1
  86. package/dist/site-extract-repository-L6BHWVDU.js +0 -30
  87. /package/dist/{chunk-3HBPKR5G.js.map → chunk-D7LM5QZN.js.map} +0 -0
  88. /package/dist/{db-YAI5AQOI.js.map → db-N6MPVMEF.js.map} +0 -0
  89. /package/dist/{site-extract-repository-L6BHWVDU.js.map → location-data-repository-Z4NQOU5Y.js.map} +0 -0
  90. /package/dist/{worker-645BZPEK.js.map → worker-EBB6CTGW.js.map} +0 -0
@@ -10,8 +10,10 @@ var CORE_SCHEMA_VERSION = "2026-07-24.2";
10
10
  var CORE_SCHEMA_TABLES = [
11
11
  "billing_authorizations",
12
12
  "billing_debits",
13
+ "billing_debit_allocations",
13
14
  "billing_events",
14
15
  "browser_profile_labels",
16
+ "connected_account_billing",
15
17
  "concurrency_locks",
16
18
  "credit_lots",
17
19
  "free_credit_refreshes",
@@ -65,6 +67,7 @@ var CORE_SCHEMA_INDEXES = [
65
67
  "site_audit_jobs_user_id",
66
68
  "site_audit_phase_log_job_id",
67
69
  "site_extract_jobs_status",
70
+ "site_extract_jobs_idempotency",
68
71
  "site_extract_jobs_user_id",
69
72
  "site_extract_pages_job_id",
70
73
  "workflow_artifacts_run",
@@ -79,6 +82,19 @@ var CORE_SCHEMA_COLUMNS = {
79
82
  credit_lots: ["never_expires"],
80
83
  harvest_attempts: ["debug_json"],
81
84
  oauth_tokens: ["replaced_by", "rotated_at"],
85
+ site_extract_jobs: [
86
+ "attempted_urls",
87
+ "successful_urls",
88
+ "failed_urls",
89
+ "idempotency_key",
90
+ "request_fingerprint",
91
+ "dispatch_attempts",
92
+ "next_dispatch_at",
93
+ "dispatch_error",
94
+ "settlement_attempts",
95
+ "next_settlement_at",
96
+ "settlement_error"
97
+ ],
82
98
  users: [
83
99
  "balance_mc",
84
100
  "concurrency_stripe_sub_id",
@@ -112,6 +128,46 @@ async function hasCurrentCoreSchema(db) {
112
128
  const result = await db.execute(`SELECT ${checks.join(" AND ")} AS ready`);
113
129
  return Number(result.rows[0]?.ready ?? 0) === 1;
114
130
  }
131
+ async function runIdempotentCoreDataBackfills(db) {
132
+ await db.execute(`
133
+ UPDATE site_extract_jobs SET
134
+ attempted_urls = done_urls,
135
+ successful_urls = (
136
+ SELECT COUNT(*) FROM site_extract_pages
137
+ WHERE job_id = site_extract_jobs.id
138
+ AND CASE
139
+ WHEN json_valid(page) = 0 THEN 0
140
+ WHEN json_extract(page, '$.extractionStatus') IS NOT NULL
141
+ THEN json_extract(page, '$.extractionStatus') = 'successful'
142
+ ELSE COALESCE(
143
+ json_extract(page, '$.status') >= 200
144
+ AND json_extract(page, '$.status') < 300
145
+ AND json_extract(page, '$.wordCount') > 0,
146
+ 0
147
+ )
148
+ END
149
+ ),
150
+ failed_urls = MAX(0, done_urls - (
151
+ SELECT COUNT(*) FROM site_extract_pages
152
+ WHERE job_id = site_extract_jobs.id
153
+ AND CASE
154
+ WHEN json_valid(page) = 0 THEN 0
155
+ WHEN json_extract(page, '$.extractionStatus') IS NOT NULL
156
+ THEN json_extract(page, '$.extractionStatus') = 'successful'
157
+ ELSE COALESCE(
158
+ json_extract(page, '$.status') >= 200
159
+ AND json_extract(page, '$.status') < 300
160
+ AND json_extract(page, '$.wordCount') > 0,
161
+ 0
162
+ )
163
+ END
164
+ ))
165
+ WHERE done_urls > 0
166
+ AND attempted_urls = 0
167
+ AND successful_urls = 0
168
+ AND failed_urls = 0
169
+ `);
170
+ }
115
171
  function getDb() {
116
172
  if (!_db) _db = createClient({ url: DB_URL, authToken: DB_TOKEN });
117
173
  return _db;
@@ -138,8 +194,12 @@ async function migrate() {
138
194
  `);
139
195
  applied = { rows: [] };
140
196
  }
141
- if (applied.rows.length > 0 && await hasCurrentCoreSchema(db)) return;
197
+ if (applied.rows.length > 0 && await hasCurrentCoreSchema(db)) {
198
+ await runIdempotentCoreDataBackfills(db);
199
+ return;
200
+ }
142
201
  if (await hasCurrentCoreSchema(db)) {
202
+ await runIdempotentCoreDataBackfills(db);
143
203
  await db.execute({
144
204
  sql: "INSERT OR IGNORE INTO schema_migrations (version) VALUES (?)",
145
205
  args: [CORE_SCHEMA_VERSION]
@@ -397,6 +457,30 @@ async function migrate() {
397
457
  )
398
458
  `);
399
459
  await db.execute(`CREATE INDEX IF NOT EXISTS billing_debits_user_status ON billing_debits(user_id, status)`);
460
+ await db.execute(`
461
+ CREATE TABLE IF NOT EXISTS billing_debit_allocations (
462
+ debit_idempotency_key TEXT NOT NULL REFERENCES billing_debits(idempotency_key),
463
+ lot_id INTEGER NOT NULL REFERENCES credit_lots(id),
464
+ user_id INTEGER NOT NULL REFERENCES users(id),
465
+ amount_mc INTEGER NOT NULL,
466
+ expires_at TEXT NOT NULL,
467
+ PRIMARY KEY (debit_idempotency_key, lot_id)
468
+ )
469
+ `);
470
+ await db.execute(`
471
+ CREATE TABLE IF NOT EXISTS connected_account_billing (
472
+ user_id INTEGER PRIMARY KEY REFERENCES users(id),
473
+ stripe_subscription_id TEXT,
474
+ stripe_subscription_item_id TEXT,
475
+ price_id TEXT,
476
+ quantity INTEGER NOT NULL DEFAULT 0,
477
+ status TEXT NOT NULL DEFAULT 'pending',
478
+ last_error_code TEXT,
479
+ checked_at TEXT NOT NULL DEFAULT (datetime('now')),
480
+ synced_at TEXT,
481
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
482
+ )
483
+ `);
400
484
  await db.execute(`
401
485
  CREATE TABLE IF NOT EXISTS site_audit_jobs (
402
486
  id TEXT PRIMARY KEY,
@@ -432,6 +516,17 @@ async function migrate() {
432
516
  options TEXT,
433
517
  total_urls INTEGER DEFAULT 0,
434
518
  done_urls INTEGER DEFAULT 0,
519
+ attempted_urls INTEGER DEFAULT 0,
520
+ successful_urls INTEGER DEFAULT 0,
521
+ failed_urls INTEGER DEFAULT 0,
522
+ idempotency_key TEXT,
523
+ request_fingerprint TEXT,
524
+ dispatch_attempts INTEGER DEFAULT 0,
525
+ next_dispatch_at TEXT,
526
+ dispatch_error TEXT,
527
+ settlement_attempts INTEGER DEFAULT 0,
528
+ next_settlement_at TEXT,
529
+ settlement_error TEXT,
435
530
  artifacts TEXT,
436
531
  error TEXT,
437
532
  billed_mc INTEGER,
@@ -450,6 +545,51 @@ async function migrate() {
450
545
  await db.execute(`CREATE INDEX IF NOT EXISTS site_extract_jobs_user_id ON site_extract_jobs(user_id)`);
451
546
  await db.execute(`CREATE INDEX IF NOT EXISTS site_extract_jobs_status ON site_extract_jobs(status)`);
452
547
  await db.execute(`CREATE INDEX IF NOT EXISTS site_extract_pages_job_id ON site_extract_pages(job_id)`);
548
+ try {
549
+ await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN attempted_urls INTEGER DEFAULT 0`);
550
+ } catch {
551
+ }
552
+ try {
553
+ await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN successful_urls INTEGER DEFAULT 0`);
554
+ } catch {
555
+ }
556
+ try {
557
+ await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN failed_urls INTEGER DEFAULT 0`);
558
+ } catch {
559
+ }
560
+ try {
561
+ await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN idempotency_key TEXT`);
562
+ } catch {
563
+ }
564
+ try {
565
+ await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN request_fingerprint TEXT`);
566
+ } catch {
567
+ }
568
+ try {
569
+ await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN dispatch_attempts INTEGER DEFAULT 0`);
570
+ } catch {
571
+ }
572
+ try {
573
+ await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN next_dispatch_at TEXT`);
574
+ } catch {
575
+ }
576
+ try {
577
+ await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN dispatch_error TEXT`);
578
+ } catch {
579
+ }
580
+ try {
581
+ await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN settlement_attempts INTEGER DEFAULT 0`);
582
+ } catch {
583
+ }
584
+ try {
585
+ await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN next_settlement_at TEXT`);
586
+ } catch {
587
+ }
588
+ try {
589
+ await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN settlement_error TEXT`);
590
+ } catch {
591
+ }
592
+ await db.execute(`CREATE UNIQUE INDEX IF NOT EXISTS site_extract_jobs_idempotency ON site_extract_jobs(user_id, idempotency_key) WHERE idempotency_key IS NOT NULL`);
453
593
  await db.execute(`
454
594
  CREATE TABLE IF NOT EXISTS password_reset_tokens (
455
595
  token TEXT PRIMARY KEY,
@@ -670,6 +810,7 @@ async function migrate() {
670
810
  if (!await hasCurrentCoreSchema(db)) {
671
811
  throw new Error("core schema migration finished without all required tables, indexes, and columns");
672
812
  }
813
+ await runIdempotentCoreDataBackfills(db);
673
814
  await db.execute({
674
815
  sql: "INSERT OR IGNORE INTO schema_migrations (version) VALUES (?)",
675
816
  args: [CORE_SCHEMA_VERSION]
@@ -1647,6 +1788,75 @@ async function setSubscriptionTier(userId, tier, concurrency, subId) {
1647
1788
  args: [tier, Math.max(0, Math.round(concurrency)), subId, userId]
1648
1789
  });
1649
1790
  }
1791
+ function rowToConnectedAccountBillingState(row) {
1792
+ return {
1793
+ user_id: Number(row.user_id),
1794
+ stripe_subscription_id: row.stripe_subscription_id != null ? String(row.stripe_subscription_id) : null,
1795
+ stripe_subscription_item_id: row.stripe_subscription_item_id != null ? String(row.stripe_subscription_item_id) : null,
1796
+ price_id: row.price_id != null ? String(row.price_id) : null,
1797
+ quantity: Math.max(0, Number(row.quantity ?? 0)),
1798
+ status: ["pending", "synced", "blocked", "error"].includes(String(row.status)) ? String(row.status) : "error",
1799
+ last_error_code: row.last_error_code != null ? String(row.last_error_code) : null,
1800
+ checked_at: String(row.checked_at),
1801
+ synced_at: row.synced_at != null ? String(row.synced_at) : null,
1802
+ updated_at: String(row.updated_at)
1803
+ };
1804
+ }
1805
+ async function getConnectedAccountBillingState(userId) {
1806
+ const res = await getDb().execute({
1807
+ sql: "SELECT * FROM connected_account_billing WHERE user_id = ? LIMIT 1",
1808
+ args: [userId]
1809
+ });
1810
+ const row = res.rows[0];
1811
+ return row ? rowToConnectedAccountBillingState(row) : null;
1812
+ }
1813
+ async function setConnectedAccountBillingState(input) {
1814
+ const quantity = Math.max(0, Math.round(input.quantity));
1815
+ await getDb().execute({
1816
+ sql: `INSERT INTO connected_account_billing
1817
+ (user_id, stripe_subscription_id, stripe_subscription_item_id, price_id, quantity, status, last_error_code, checked_at, synced_at, updated_at)
1818
+ VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), CASE WHEN ? THEN datetime('now') ELSE NULL END, datetime('now'))
1819
+ ON CONFLICT(user_id) DO UPDATE SET
1820
+ stripe_subscription_id = excluded.stripe_subscription_id,
1821
+ stripe_subscription_item_id = excluded.stripe_subscription_item_id,
1822
+ price_id = excluded.price_id,
1823
+ quantity = excluded.quantity,
1824
+ status = excluded.status,
1825
+ last_error_code = excluded.last_error_code,
1826
+ checked_at = datetime('now'),
1827
+ synced_at = CASE WHEN ? THEN datetime('now') ELSE connected_account_billing.synced_at END,
1828
+ updated_at = datetime('now')`,
1829
+ args: [
1830
+ input.userId,
1831
+ input.stripeSubscriptionId,
1832
+ input.stripeSubscriptionItemId,
1833
+ input.priceId,
1834
+ quantity,
1835
+ input.status,
1836
+ input.lastErrorCode ?? null,
1837
+ input.synced === true ? 1 : 0,
1838
+ input.synced === true ? 1 : 0
1839
+ ]
1840
+ });
1841
+ const state = await getConnectedAccountBillingState(input.userId);
1842
+ if (!state) throw new Error("connected account billing state was not persisted");
1843
+ return state;
1844
+ }
1845
+ async function listUsersDueForConnectedAccountBillingReconciliation(staleMinutes = 60, limit = 20) {
1846
+ const modifier = `-${Math.max(1, Math.round(staleMinutes))} minutes`;
1847
+ const res = await getDb().execute({
1848
+ sql: `SELECT u.*
1849
+ FROM users u
1850
+ LEFT JOIN connected_account_billing b ON b.user_id = u.id
1851
+ WHERE u.active = 1
1852
+ AND u.subscription_id IS NOT NULL
1853
+ AND (b.checked_at IS NULL OR b.checked_at <= datetime('now', ?))
1854
+ ORDER BY COALESCE(b.checked_at, u.created_at) ASC
1855
+ LIMIT ?`,
1856
+ args: [modifier, Math.max(1, Math.min(100, Math.round(limit)))]
1857
+ });
1858
+ return res.rows.map((row) => rowToUser(row));
1859
+ }
1650
1860
  async function setMemoryPlan(userId, plan, subscriptionId) {
1651
1861
  await getDb().execute({
1652
1862
  sql: "UPDATE users SET memory_plan = ?, memory_subscription_id = ? WHERE id = ?",
@@ -1826,6 +2036,8 @@ async function debitMcIdempotent(userId, mc, operation, description, idempotency
1826
2036
  try {
1827
2037
  results = await db.batch([
1828
2038
  {
2039
+ // An unfunded claim may be retried with a corrected estimate under the
2040
+ // same logical operation fingerprint. Applied claims remain immutable.
1829
2041
  sql: "UPDATE billing_debits SET amount_mc = ? WHERE idempotency_key = ? AND user_id = ? AND operation = ? AND description = ? AND status = 'claimed'",
1830
2042
  args: [mc, idempotencyKey, userId, operation, description]
1831
2043
  },
@@ -1834,17 +2046,29 @@ async function debitMcIdempotent(userId, mc, operation, description, idempotency
1834
2046
  args: [idempotencyKey, userId, mc, operation, description]
1835
2047
  },
1836
2048
  {
1837
- 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')",
1838
- args: [mc, userId, mc, idempotencyKey, userId, mc, operation, description]
2049
+ sql: `UPDATE users
2050
+ SET balance_mc = balance_mc - ?
2051
+ WHERE id = ?
2052
+ AND balance_mc >= ?
2053
+ AND (SELECT COALESCE(SUM(remaining_mc), 0)
2054
+ FROM credit_lots
2055
+ WHERE user_id = ?
2056
+ AND remaining_mc > 0
2057
+ AND (never_expires = 1 OR expires_at > datetime('now'))) >= ?
2058
+ AND EXISTS (SELECT 1 FROM billing_debits WHERE idempotency_key = ? AND user_id = ? AND amount_mc = ? AND operation = ? AND description = ? AND status = 'claimed')`,
2059
+ args: [mc, userId, mc, userId, mc, idempotencyKey, userId, mc, operation, description]
1839
2060
  },
1840
2061
  {
1841
2062
  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",
1842
2063
  args: [idempotencyKey, userId, mc, operation, description]
1843
2064
  },
1844
2065
  {
1845
- sql: `WITH ordered AS MATERIALIZED (
2066
+ sql: `INSERT OR IGNORE INTO billing_debit_allocations
2067
+ (debit_idempotency_key, lot_id, user_id, amount_mc, expires_at)
2068
+ WITH ordered AS MATERIALIZED (
1846
2069
  SELECT id,
1847
2070
  remaining_mc,
2071
+ expires_at,
1848
2072
  COALESCE(SUM(remaining_mc) OVER (
1849
2073
  ORDER BY never_expires ASC, expires_at ASC, granted_at ASC, id ASC
1850
2074
  ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
@@ -1852,14 +2076,26 @@ async function debitMcIdempotent(userId, mc, operation, description, idempotency
1852
2076
  FROM credit_lots
1853
2077
  WHERE user_id = ? AND remaining_mc > 0 AND (never_expires = 1 OR expires_at > datetime('now'))
1854
2078
  ), allocation AS (
1855
- SELECT id, MIN(remaining_mc, MAX(0, ? - prior_mc)) AS take_mc
2079
+ SELECT id, expires_at, MIN(remaining_mc, MAX(0, ? - prior_mc)) AS take_mc
1856
2080
  FROM ordered
1857
2081
  )
1858
- UPDATE credit_lots
1859
- SET remaining_mc = remaining_mc - COALESCE((SELECT take_mc FROM allocation WHERE allocation.id = credit_lots.id), 0)
1860
- WHERE id IN (SELECT id FROM allocation WHERE take_mc > 0)
2082
+ SELECT ?, id, ?, take_mc, expires_at
2083
+ FROM allocation
2084
+ WHERE take_mc > 0
2085
+ AND EXISTS (SELECT 1 FROM billing_debits WHERE idempotency_key = ? AND status = 'applying')`,
2086
+ args: [userId, mc, idempotencyKey, userId, idempotencyKey]
2087
+ },
2088
+ {
2089
+ sql: `UPDATE credit_lots
2090
+ SET remaining_mc = remaining_mc - COALESCE((
2091
+ SELECT amount_mc FROM billing_debit_allocations
2092
+ WHERE debit_idempotency_key = ? AND lot_id = credit_lots.id
2093
+ ), 0)
2094
+ WHERE id IN (
2095
+ SELECT lot_id FROM billing_debit_allocations WHERE debit_idempotency_key = ?
2096
+ )
1861
2097
  AND EXISTS (SELECT 1 FROM billing_debits WHERE idempotency_key = ? AND status = 'applying')`,
1862
- args: [userId, mc, idempotencyKey]
2098
+ args: [idempotencyKey, idempotencyKey, idempotencyKey]
1863
2099
  },
1864
2100
  {
1865
2101
  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'",
@@ -1923,6 +2159,7 @@ async function settleDebitMcIdempotent(userId, debitIdempotencyKey, finalAmountM
1923
2159
  e.amount_mc AS final_amount_mc,
1924
2160
  e.billing_class,
1925
2161
  e.source_surface,
2162
+ e.settled_at,
1926
2163
  d.amount_mc AS held_amount_mc
1927
2164
  FROM billing_events e
1928
2165
  JOIN billing_debits d ON d.idempotency_key = ?
@@ -1943,11 +2180,35 @@ async function settleDebitMcIdempotent(userId, debitIdempotencyKey, finalAmountM
1943
2180
  if (!Number.isSafeInteger(heldAmountMc) || !Number.isSafeInteger(durableFinalAmountMc) || durableFinalAmountMc < 0 || durableFinalAmountMc > heldAmountMc) {
1944
2181
  throw new Error("idempotent debit settlement receipt is invalid");
1945
2182
  }
2183
+ const nominalRefundMc = heldAmountMc - durableFinalAmountMc;
2184
+ const allocationRefund = await db.execute({
2185
+ sql: `WITH ordered AS (
2186
+ SELECT a.amount_mc,
2187
+ a.expires_at,
2188
+ COALESCE(SUM(a.amount_mc) OVER (
2189
+ ORDER BY a.expires_at ASC, a.lot_id ASC
2190
+ ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
2191
+ ), 0) AS prior_mc
2192
+ FROM billing_debit_allocations a
2193
+ WHERE a.debit_idempotency_key = ?
2194
+ ), refunds AS (
2195
+ SELECT expires_at,
2196
+ amount_mc - MIN(amount_mc, MAX(0, ? - prior_mc)) AS refund_mc
2197
+ FROM ordered
2198
+ )
2199
+ SELECT (SELECT COUNT(*) FROM ordered) AS allocation_count,
2200
+ COALESCE(SUM(CASE WHEN expires_at > ? THEN refund_mc ELSE 0 END), 0) AS credited_refund_mc
2201
+ FROM refunds`,
2202
+ args: [debitIdempotencyKey, durableFinalAmountMc, String(row.settled_at)]
2203
+ });
2204
+ const allocationCount = Number(allocationRefund.rows[0]?.allocation_count ?? 0);
2205
+ const creditedRefundMc = allocationCount > 0 ? Number(allocationRefund.rows[0]?.credited_refund_mc ?? 0) : 0;
1946
2206
  const balance = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
1947
2207
  return {
1948
2208
  balance_mc: Number(balance.rows[0]?.balance_mc ?? 0),
1949
2209
  final_amount_mc: durableFinalAmountMc,
1950
- refund_mc: heldAmountMc - durableFinalAmountMc,
2210
+ refund_mc: creditedRefundMc,
2211
+ expired_refund_mc: nominalRefundMc - creditedRefundMc,
1951
2212
  duplicate: true
1952
2213
  };
1953
2214
  };
@@ -1961,13 +2222,34 @@ async function settleDebitMcIdempotent(userId, debitIdempotencyKey, finalAmountM
1961
2222
  args: [eventId, userId, settlementKey, billingClass, sourceSurface, finalAmountMc, metadata]
1962
2223
  },
1963
2224
  {
1964
- sql: `UPDATE users
1965
- SET balance_mc = balance_mc + (
1966
- SELECT d.amount_mc - e.amount_mc
1967
- FROM billing_events e
1968
- JOIN billing_debits d ON d.idempotency_key = ?
1969
- WHERE e.idempotency_key = ?
2225
+ sql: `WITH ordered AS (
2226
+ SELECT a.lot_id,
2227
+ a.amount_mc,
2228
+ a.expires_at,
2229
+ COALESCE(SUM(a.amount_mc) OVER (
2230
+ ORDER BY a.expires_at ASC, a.lot_id ASC
2231
+ ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
2232
+ ), 0) AS prior_mc
2233
+ FROM billing_debit_allocations a
2234
+ WHERE a.debit_idempotency_key = ?
2235
+ ), refunds AS (
2236
+ SELECT lot_id,
2237
+ expires_at,
2238
+ amount_mc - MIN(amount_mc, MAX(0, ? - prior_mc)) AS refund_mc
2239
+ FROM ordered
1970
2240
  )
2241
+ UPDATE users
2242
+ SET balance_mc = balance_mc + CASE
2243
+ WHEN EXISTS (SELECT 1 FROM ordered) THEN COALESCE(
2244
+ (SELECT SUM(refund_mc) FROM refunds WHERE expires_at > datetime('now')),
2245
+ 0
2246
+ )
2247
+ -- Holds created before allocation provenance cannot be safely
2248
+ -- tied back to a source lot. Treat their unused portion as
2249
+ -- expired instead of minting spendable credits with a guessed
2250
+ -- expiry.
2251
+ ELSE 0
2252
+ END
1971
2253
  WHERE id = ?
1972
2254
  AND EXISTS (
1973
2255
  SELECT 1
@@ -1984,7 +2266,7 @@ async function settleDebitMcIdempotent(userId, debitIdempotencyKey, finalAmountM
1984
2266
  )`,
1985
2267
  args: [
1986
2268
  debitIdempotencyKey,
1987
- settlementKey,
2269
+ finalAmountMc,
1988
2270
  userId,
1989
2271
  debitIdempotencyKey,
1990
2272
  settlementKey,
@@ -2005,34 +2287,74 @@ async function settleDebitMcIdempotent(userId, debitIdempotencyKey, finalAmountM
2005
2287
  args: [settlementKey, userId, billingClass, sourceSurface]
2006
2288
  },
2007
2289
  {
2008
- sql: `INSERT INTO credit_lots
2009
- (user_id, amount_mc, remaining_mc, source, expires_at, never_expires)
2010
- SELECT e.user_id,
2011
- d.amount_mc - e.amount_mc,
2012
- d.amount_mc - e.amount_mc,
2013
- ?,
2014
- CASE WHEN u.subscription_tier LIKE 'sonic%' THEN '9999-12-31 23:59:59' ELSE datetime('now', ?) END,
2015
- CASE WHEN u.subscription_tier LIKE 'sonic%' THEN 1 ELSE 0 END
2016
- FROM billing_events e
2017
- JOIN billing_debits d ON d.idempotency_key = ?
2018
- JOIN users u ON u.id = e.user_id
2019
- WHERE e.idempotency_key = ?
2020
- AND e.status = 'applying'
2021
- AND d.amount_mc > e.amount_mc`,
2022
- args: [refundOperation, CREDIT_LOT_TTL, debitIdempotencyKey, settlementKey]
2290
+ sql: `WITH ordered AS (
2291
+ SELECT a.lot_id,
2292
+ a.amount_mc,
2293
+ a.expires_at,
2294
+ COALESCE(SUM(a.amount_mc) OVER (
2295
+ ORDER BY a.expires_at ASC, a.lot_id ASC
2296
+ ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
2297
+ ), 0) AS prior_mc
2298
+ FROM billing_debit_allocations a
2299
+ WHERE a.debit_idempotency_key = ?
2300
+ ), refunds AS (
2301
+ SELECT lot_id,
2302
+ expires_at,
2303
+ amount_mc - MIN(amount_mc, MAX(0, ? - prior_mc)) AS refund_mc
2304
+ FROM ordered
2305
+ )
2306
+ UPDATE credit_lots
2307
+ SET remaining_mc = remaining_mc + COALESCE((SELECT refund_mc FROM refunds WHERE refunds.lot_id = credit_lots.id), 0)
2308
+ WHERE id IN (
2309
+ SELECT lot_id FROM refunds
2310
+ WHERE refund_mc > 0 AND expires_at > datetime('now')
2311
+ )
2312
+ AND EXISTS (SELECT 1 FROM billing_events WHERE idempotency_key = ? AND status = 'applying')`,
2313
+ args: [debitIdempotencyKey, finalAmountMc, settlementKey]
2023
2314
  },
2024
2315
  {
2025
- sql: `INSERT INTO ledger (user_id, amount_mc, operation, description)
2026
- SELECT e.user_id,
2027
- d.amount_mc - e.amount_mc,
2316
+ sql: `WITH ordered AS (
2317
+ SELECT a.amount_mc,
2318
+ a.expires_at,
2319
+ COALESCE(SUM(a.amount_mc) OVER (
2320
+ ORDER BY a.expires_at ASC, a.lot_id ASC
2321
+ ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
2322
+ ), 0) AS prior_mc
2323
+ FROM billing_debit_allocations a
2324
+ WHERE a.debit_idempotency_key = ?
2325
+ ), refunds AS (
2326
+ SELECT expires_at,
2327
+ amount_mc - MIN(amount_mc, MAX(0, ? - prior_mc)) AS refund_mc
2328
+ FROM ordered
2329
+ ), credited AS (
2330
+ SELECT e.user_id,
2331
+ CASE
2332
+ WHEN EXISTS (SELECT 1 FROM ordered) THEN COALESCE(
2333
+ (SELECT SUM(refund_mc) FROM refunds WHERE expires_at > datetime('now')),
2334
+ 0
2335
+ )
2336
+ ELSE 0
2337
+ END AS refund_mc
2338
+ FROM billing_events e
2339
+ JOIN billing_debits d ON d.idempotency_key = ?
2340
+ WHERE e.idempotency_key = ?
2341
+ AND e.status = 'applying'
2342
+ )
2343
+ INSERT INTO ledger (user_id, amount_mc, operation, description)
2344
+ SELECT user_id,
2345
+ refund_mc,
2028
2346
  ?,
2029
2347
  ?
2030
- FROM billing_events e
2031
- JOIN billing_debits d ON d.idempotency_key = ?
2032
- WHERE e.idempotency_key = ?
2033
- AND e.status = 'applying'
2034
- AND d.amount_mc > e.amount_mc`,
2035
- args: [refundOperation, description, debitIdempotencyKey, settlementKey]
2348
+ FROM credited
2349
+ WHERE refund_mc > 0`,
2350
+ args: [
2351
+ debitIdempotencyKey,
2352
+ finalAmountMc,
2353
+ debitIdempotencyKey,
2354
+ settlementKey,
2355
+ refundOperation,
2356
+ description
2357
+ ]
2036
2358
  },
2037
2359
  {
2038
2360
  sql: `UPDATE billing_events
@@ -2311,6 +2633,9 @@ export {
2311
2633
  setExtraConcurrencySlots,
2312
2634
  setConcurrencySubId,
2313
2635
  setSubscriptionTier,
2636
+ getConnectedAccountBillingState,
2637
+ setConnectedAccountBillingState,
2638
+ listUsersDueForConnectedAccountBillingReconciliation,
2314
2639
  setMemoryPlan,
2315
2640
  getMemoryPlan,
2316
2641
  getMemoryProvisioned,
@@ -2338,4 +2663,4 @@ export {
2338
2663
  insertInboxMessage,
2339
2664
  listActiveUserIds
2340
2665
  };
2341
- //# sourceMappingURL=chunk-62DQAWPF.js.map
2666
+ //# sourceMappingURL=chunk-IFYER7O4.js.map