mcp-scraper 0.21.2 → 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 +1061 -308
- 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 +101 -34
- 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 +22 -0
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +3 -3
- package/dist/{chunk-ZJDANZTQ.js → chunk-E5J4HJBO.js} +3 -2
- package/dist/chunk-E5J4HJBO.js.map +1 -0
- package/dist/{chunk-DZBGULWW.js → chunk-ICT7DDHL.js} +399 -3
- package/dist/chunk-ICT7DDHL.js.map +1 -0
- package/dist/{chunk-2FF4GBQ4.js → chunk-JHI373VB.js} +49 -7
- package/dist/chunk-JHI373VB.js.map +1 -0
- package/dist/{chunk-GMZFQRH2.js → chunk-MTSBI7ZH.js} +23 -6
- package/dist/chunk-MTSBI7ZH.js.map +1 -0
- package/dist/{chunk-ZNNYPE2U.js → chunk-N4JHXPW6.js} +89 -27
- package/dist/chunk-N4JHXPW6.js.map +1 -0
- package/dist/{chunk-X3NIXHHG.js → chunk-PNBYYAEK.js} +3 -3
- package/dist/{chunk-JBME5WJR.js → chunk-SDHPQTFM.js} +2 -2
- package/dist/chunk-WQI7JIIN.js +7 -0
- package/dist/chunk-WQI7JIIN.js.map +1 -0
- package/dist/{db-EONGTNJM.js → db-WRILVOFY.js} +6 -2
- package/dist/{extract-bundle-HMCYUVBJ.js → extract-bundle-NFFVMUMT.js} +2 -2
- package/dist/index.cjs +22 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -3
- package/dist/{server-CHTX6YQA.js → server-OLOOFN5W.js} +499 -271
- package/dist/server-OLOOFN5W.js.map +1 -0
- package/dist/{site-extract-repository-5V5EEQ72.js → site-extract-repository-2QQSL2QC.js} +4 -4
- package/dist/{worker-GE5JMHE6.js → worker-RV2TYXE3.js} +15 -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-2FF4GBQ4.js.map +0 -1
- package/dist/chunk-DZBGULWW.js.map +0 -1
- package/dist/chunk-GMZFQRH2.js.map +0 -1
- package/dist/chunk-NBF4F4UT.js +0 -7
- package/dist/chunk-NBF4F4UT.js.map +0 -1
- package/dist/chunk-ZJDANZTQ.js.map +0 -1
- package/dist/chunk-ZNNYPE2U.js.map +0 -1
- package/dist/server-CHTX6YQA.js.map +0 -1
- package/dist/worker-GE5JMHE6.js.map +0 -1
- /package/dist/{chunk-X3NIXHHG.js.map → chunk-PNBYYAEK.js.map} +0 -0
- /package/dist/{chunk-JBME5WJR.js.map → chunk-SDHPQTFM.js.map} +0 -0
- /package/dist/{db-EONGTNJM.js.map → db-WRILVOFY.js.map} +0 -0
- /package/dist/{extract-bundle-HMCYUVBJ.js.map → extract-bundle-NFFVMUMT.js.map} +0 -0
- /package/dist/{site-extract-repository-5V5EEQ72.js.map → site-extract-repository-2QQSL2QC.js.map} +0 -0
|
@@ -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,
|
|
@@ -248,6 +375,19 @@ async function migrate() {
|
|
|
248
375
|
`);
|
|
249
376
|
await db.execute(`CREATE INDEX IF NOT EXISTS billing_events_user_created_at ON billing_events(user_id, created_at DESC)`);
|
|
250
377
|
await db.execute(`CREATE INDEX IF NOT EXISTS billing_events_authorization ON billing_events(authorization_id)`);
|
|
378
|
+
await db.execute(`
|
|
379
|
+
CREATE TABLE IF NOT EXISTS billing_debits (
|
|
380
|
+
idempotency_key TEXT PRIMARY KEY,
|
|
381
|
+
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
382
|
+
amount_mc INTEGER NOT NULL,
|
|
383
|
+
operation TEXT NOT NULL,
|
|
384
|
+
description TEXT,
|
|
385
|
+
status TEXT NOT NULL DEFAULT 'claimed',
|
|
386
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
387
|
+
applied_at TEXT
|
|
388
|
+
)
|
|
389
|
+
`);
|
|
390
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS billing_debits_user_status ON billing_debits(user_id, status)`);
|
|
251
391
|
await db.execute(`
|
|
252
392
|
CREATE TABLE IF NOT EXISTS site_audit_jobs (
|
|
253
393
|
id TEXT PRIMARY KEY,
|
|
@@ -495,6 +635,13 @@ async function migrate() {
|
|
|
495
635
|
)
|
|
496
636
|
`);
|
|
497
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
|
+
});
|
|
498
645
|
}
|
|
499
646
|
async function registerClient(clientId, redirectUris, name) {
|
|
500
647
|
await getDb().execute({
|
|
@@ -957,6 +1104,18 @@ async function upsertPageSnapshot(userId, url, snapshot) {
|
|
|
957
1104
|
args: [userId, url, snapshot.contentHash, snapshot.content, snapshot.title, snapshot.contentBytes, snapshot.truncated ? 1 : 0]
|
|
958
1105
|
});
|
|
959
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
|
+
}
|
|
960
1119
|
function rowToRequestEvent(row) {
|
|
961
1120
|
const rawResult = row.result != null ? String(row.result) : null;
|
|
962
1121
|
return {
|
|
@@ -987,7 +1146,7 @@ async function logRequestEvent(input) {
|
|
|
987
1146
|
input.query,
|
|
988
1147
|
input.location ?? null,
|
|
989
1148
|
input.resultCount ?? null,
|
|
990
|
-
|
|
1149
|
+
serializeRequestEventResult(input.result),
|
|
991
1150
|
input.error ?? null
|
|
992
1151
|
]
|
|
993
1152
|
});
|
|
@@ -996,7 +1155,7 @@ async function logRequestEvent(input) {
|
|
|
996
1155
|
async function listRequestEvents(userId, limit = 100) {
|
|
997
1156
|
const res = await getDb().execute({
|
|
998
1157
|
sql: `
|
|
999
|
-
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
|
|
1000
1159
|
FROM request_events
|
|
1001
1160
|
WHERE user_id = ?
|
|
1002
1161
|
ORDER BY created_at DESC
|
|
@@ -1620,6 +1779,241 @@ async function debitMc(userId, mc, operation, description) {
|
|
|
1620
1779
|
const res = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
|
|
1621
1780
|
return { ok: true, balance_mc: Number(res.rows[0]?.balance_mc ?? 0) };
|
|
1622
1781
|
}
|
|
1782
|
+
async function debitMcIdempotent(userId, mc, operation, description, idempotencyKey) {
|
|
1783
|
+
if (!Number.isSafeInteger(mc) || mc <= 0) throw new Error("idempotent debit amount must be a positive integer");
|
|
1784
|
+
if (!idempotencyKey || idempotencyKey.length > 500) throw new Error("idempotent debit key is required and must be bounded");
|
|
1785
|
+
const db = getDb();
|
|
1786
|
+
await ensureMigrated(userId);
|
|
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 {
|
|
1847
|
+
}
|
|
1848
|
+
throw error;
|
|
1849
|
+
}
|
|
1850
|
+
const claim = await db.execute({
|
|
1851
|
+
sql: "SELECT user_id, amount_mc, operation, description, status FROM billing_debits WHERE idempotency_key = ? LIMIT 1",
|
|
1852
|
+
args: [idempotencyKey]
|
|
1853
|
+
});
|
|
1854
|
+
const row = claim.rows[0];
|
|
1855
|
+
if (!row || Number(row.user_id) !== userId || Number(row.amount_mc) !== mc || String(row.operation) !== operation || String(row.description) !== description) {
|
|
1856
|
+
throw new Error("idempotent debit key conflicts with another debit");
|
|
1857
|
+
}
|
|
1858
|
+
const balance = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
|
|
1859
|
+
return {
|
|
1860
|
+
ok: String(row.status) === "applied",
|
|
1861
|
+
balance_mc: Number(balance.rows[0]?.balance_mc ?? 0),
|
|
1862
|
+
duplicate: results[2]?.rowsAffected === 0 && String(row.status) === "applied"
|
|
1863
|
+
};
|
|
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
|
+
}
|
|
1623
2017
|
async function getLedger(userId, limit = 50) {
|
|
1624
2018
|
const res = await getDb().execute({
|
|
1625
2019
|
sql: "SELECT * FROM ledger WHERE user_id = ? ORDER BY created_at DESC LIMIT ?",
|
|
@@ -1784,10 +2178,12 @@ export {
|
|
|
1784
2178
|
ledgerExistsForOperation,
|
|
1785
2179
|
claimMonthlyFreeRefresh,
|
|
1786
2180
|
debitMc,
|
|
2181
|
+
debitMcIdempotent,
|
|
2182
|
+
settleDebitMcIdempotent,
|
|
1787
2183
|
getLedger,
|
|
1788
2184
|
ledgerExistsForStripePI,
|
|
1789
2185
|
reconcileBalanceMc,
|
|
1790
2186
|
SiteAuditJobRowSchema,
|
|
1791
2187
|
SiteAuditPhaseLogRowSchema
|
|
1792
2188
|
};
|
|
1793
|
-
//# sourceMappingURL=chunk-
|
|
2189
|
+
//# sourceMappingURL=chunk-ICT7DDHL.js.map
|