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.
- package/README.md +1 -1
- package/dist/bin/api-server.cjs +728 -181
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +3 -3
- package/dist/bin/mcp-scraper-cli.cjs +1 -1
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-install.cjs +1 -1
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +1 -1
- package/dist/bin/mcp-stdio-server.cjs +97 -32
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +5 -5
- package/dist/bin/paa-harvest.cjs +21 -0
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +3 -3
- package/dist/{chunk-JJ5RMEGX.js → chunk-E5J4HJBO.js} +2 -2
- package/dist/{chunk-H7APH5UG.js → chunk-ICT7DDHL.js} +366 -50
- package/dist/chunk-ICT7DDHL.js.map +1 -0
- package/dist/{chunk-OBNKOYOE.js → chunk-JHI373VB.js} +7 -5
- package/dist/chunk-JHI373VB.js.map +1 -0
- package/dist/{chunk-5IU5ZFVO.js → chunk-MTSBI7ZH.js} +23 -6
- package/dist/chunk-MTSBI7ZH.js.map +1 -0
- package/dist/{chunk-7LO6KLKD.js → chunk-N4JHXPW6.js} +89 -27
- package/dist/chunk-N4JHXPW6.js.map +1 -0
- package/dist/{chunk-XDRHBGJR.js → chunk-PNBYYAEK.js} +3 -3
- package/dist/{chunk-2P5VFIJM.js → chunk-SDHPQTFM.js} +2 -2
- package/dist/chunk-WQI7JIIN.js +7 -0
- package/dist/chunk-WQI7JIIN.js.map +1 -0
- package/dist/{db-Z35S224E.js → db-WRILVOFY.js} +4 -2
- package/dist/{extract-bundle-4RO3YU4Q.js → extract-bundle-NFFVMUMT.js} +2 -2
- package/dist/index.cjs +21 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -3
- package/dist/{server-PBBRSLBP.js → server-OLOOFN5W.js} +246 -100
- package/dist/server-OLOOFN5W.js.map +1 -0
- package/dist/{site-extract-repository-FDH6ZXKT.js → site-extract-repository-2QQSL2QC.js} +4 -4
- package/dist/{worker-BXJQ7J4N.js → worker-RV2TYXE3.js} +9 -8
- package/dist/worker-RV2TYXE3.js.map +1 -0
- package/docs/mcp-tool-manifest.generated.json +90 -11
- package/docs/specs/oauth-mcp-spec.md +2 -2
- package/package.json +1 -1
- package/dist/chunk-5IU5ZFVO.js.map +0 -1
- package/dist/chunk-7LO6KLKD.js.map +0 -1
- package/dist/chunk-H7APH5UG.js.map +0 -1
- package/dist/chunk-OBNKOYOE.js.map +0 -1
- package/dist/chunk-VYUWWIVR.js +0 -7
- package/dist/chunk-VYUWWIVR.js.map +0 -1
- package/dist/server-PBBRSLBP.js.map +0 -1
- package/dist/worker-BXJQ7J4N.js.map +0 -1
- /package/dist/{chunk-JJ5RMEGX.js.map → chunk-E5J4HJBO.js.map} +0 -0
- /package/dist/{chunk-XDRHBGJR.js.map → chunk-PNBYYAEK.js.map} +0 -0
- /package/dist/{chunk-2P5VFIJM.js.map → chunk-SDHPQTFM.js.map} +0 -0
- /package/dist/{db-Z35S224E.js.map → db-WRILVOFY.js.map} +0 -0
- /package/dist/{extract-bundle-4RO3YU4Q.js.map → extract-bundle-NFFVMUMT.js.map} +0 -0
- /package/dist/{site-extract-repository-FDH6ZXKT.js.map → site-extract-repository-2QQSL2QC.js.map} +0 -0
package/dist/bin/paa-harvest.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
harvest
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-E5J4HJBO.js";
|
|
5
5
|
import {
|
|
6
6
|
browserServiceApiKey
|
|
7
|
-
} from "../chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
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
|
-
|
|
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-
|
|
2189
|
+
//# sourceMappingURL=chunk-ICT7DDHL.js.map
|