mcp-scraper 0.38.0 → 0.38.2
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 +813 -338
- 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 +25 -7
- 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 +65 -4
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +3 -3
- package/dist/{chunk-PWPUKR5U.js → chunk-AZRPG43B.js} +12 -3
- package/dist/chunk-AZRPG43B.js.map +1 -0
- package/dist/{chunk-MA5JBAUZ.js → chunk-EGJKUB4Q.js} +3 -3
- package/dist/{chunk-MA5JBAUZ.js.map → chunk-EGJKUB4Q.js.map} +1 -1
- package/dist/{chunk-PUJFYJXB.js → chunk-FRYT3ID4.js} +2 -2
- package/dist/{chunk-CSCD2HNS.js → chunk-N7KUTTCC.js} +312 -101
- package/dist/chunk-N7KUTTCC.js.map +1 -0
- package/dist/{chunk-LOPKN3YL.js → chunk-NGM237OO.js} +66 -5
- package/dist/chunk-NGM237OO.js.map +1 -0
- package/dist/{chunk-P64PZUCA.js → chunk-NKCCGADE.js} +23 -8
- package/dist/chunk-NKCCGADE.js.map +1 -0
- package/dist/{chunk-FSAXLDB3.js → chunk-SFRMFGQ6.js} +2 -2
- package/dist/{chunk-Q35WZJJK.js → chunk-YCI2PNCS.js} +3 -3
- package/dist/chunk-YODBNTTN.js +7 -0
- package/dist/chunk-YODBNTTN.js.map +1 -0
- package/dist/{db-N3YECFWR.js → db-C5KVCOYT.js} +10 -4
- package/dist/{extract-bundle-346R6MXD.js → extract-bundle-KUBX6N6Z.js} +4 -4
- package/dist/index.cjs +65 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -3
- package/dist/{location-data-repository-O2VII3ON.js → location-data-repository-TTWF3OTM.js} +3 -3
- package/dist/{server-3Q6XIJPF.js → server-5EX6XBIA.js} +320 -152
- package/dist/server-5EX6XBIA.js.map +1 -0
- package/dist/{site-extract-repository-I3VM6WXN.js → site-extract-repository-XSPJTCIL.js} +4 -4
- package/dist/{worker-TTFXPPDK.js → worker-XCPU4YSN.js} +6 -6
- package/docs/mcp-tool-manifest.generated.json +66 -9
- package/package.json +4 -2
- package/dist/chunk-CSCD2HNS.js.map +0 -1
- package/dist/chunk-H3M3O457.js +0 -7
- package/dist/chunk-H3M3O457.js.map +0 -1
- package/dist/chunk-LOPKN3YL.js.map +0 -1
- package/dist/chunk-P64PZUCA.js.map +0 -1
- package/dist/chunk-PWPUKR5U.js.map +0 -1
- package/dist/server-3Q6XIJPF.js.map +0 -1
- /package/dist/{chunk-PUJFYJXB.js.map → chunk-FRYT3ID4.js.map} +0 -0
- /package/dist/{chunk-FSAXLDB3.js.map → chunk-SFRMFGQ6.js.map} +0 -0
- /package/dist/{chunk-Q35WZJJK.js.map → chunk-YCI2PNCS.js.map} +0 -0
- /package/dist/{db-N3YECFWR.js.map → db-C5KVCOYT.js.map} +0 -0
- /package/dist/{extract-bundle-346R6MXD.js.map → extract-bundle-KUBX6N6Z.js.map} +0 -0
- /package/dist/{location-data-repository-O2VII3ON.js.map → location-data-repository-TTWF3OTM.js.map} +0 -0
- /package/dist/{site-extract-repository-I3VM6WXN.js.map → site-extract-repository-XSPJTCIL.js.map} +0 -0
- /package/dist/{worker-TTFXPPDK.js.map → worker-XCPU4YSN.js.map} +0 -0
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
import { createClient } from "@libsql/client/http";
|
|
3
3
|
import { createHash, randomBytes, randomUUID, scryptSync, timingSafeEqual } from "crypto";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
// src/api/credit-lot-integrity.ts
|
|
7
|
+
var CREDIT_LOTS_EPOCH = "2026-06-26 21:58:03";
|
|
8
|
+
|
|
9
|
+
// src/api/db.ts
|
|
5
10
|
var DB_URL = process.env.TURSO_DATABASE_URL ?? "file:./paa-api.db";
|
|
6
11
|
var DB_TOKEN = process.env.TURSO_AUTH_TOKEN;
|
|
7
12
|
var _db = null;
|
|
8
13
|
var _rateLimitSchemaReady = false;
|
|
9
|
-
var CORE_SCHEMA_VERSION = "2026-07-28.
|
|
14
|
+
var CORE_SCHEMA_VERSION = "2026-07-28.3";
|
|
10
15
|
var CORE_SCHEMA_TABLES = [
|
|
11
16
|
"admin_credit_adjustment_events",
|
|
12
17
|
"admin_credit_adjustments",
|
|
@@ -18,7 +23,9 @@ var CORE_SCHEMA_TABLES = [
|
|
|
18
23
|
"browser_profile_labels",
|
|
19
24
|
"connected_account_billing",
|
|
20
25
|
"concurrency_locks",
|
|
26
|
+
"credit_lot_expirations",
|
|
21
27
|
"credit_lots",
|
|
28
|
+
"credit_lot_repairs",
|
|
22
29
|
"free_credit_refreshes",
|
|
23
30
|
"harvest_attempts",
|
|
24
31
|
"inbox_messages",
|
|
@@ -55,6 +62,7 @@ var CORE_SCHEMA_INDEXES = [
|
|
|
55
62
|
"concurrency_locks_operation",
|
|
56
63
|
"concurrency_locks_user_status",
|
|
57
64
|
"credit_lots_user_active",
|
|
65
|
+
"credit_lot_repairs_user_applied",
|
|
58
66
|
"harvest_attempts_job_id",
|
|
59
67
|
"harvest_attempts_user_created_at",
|
|
60
68
|
"inbox_messages_user_sent_at",
|
|
@@ -84,6 +92,12 @@ var CORE_SCHEMA_INDEXES = [
|
|
|
84
92
|
"workflow_schedules_user_status",
|
|
85
93
|
"workflow_webhook_deliveries_run"
|
|
86
94
|
];
|
|
95
|
+
var CORE_SCHEMA_TRIGGERS = [
|
|
96
|
+
"credit_lots_bounds_insert",
|
|
97
|
+
"credit_lots_bounds_update",
|
|
98
|
+
"credit_lots_one_migration_insert",
|
|
99
|
+
"credit_lots_one_migration_update"
|
|
100
|
+
];
|
|
87
101
|
var CORE_SCHEMA_COLUMNS = {
|
|
88
102
|
credit_lots: ["never_expires"],
|
|
89
103
|
harvest_attempts: ["debug_json"],
|
|
@@ -101,6 +115,7 @@ var CORE_SCHEMA_COLUMNS = {
|
|
|
101
115
|
"next_settlement_at",
|
|
102
116
|
"settlement_error"
|
|
103
117
|
],
|
|
118
|
+
stripe_events: ["attempt_count", "claim_token", "last_error", "status", "updated_at"],
|
|
104
119
|
users: [
|
|
105
120
|
"balance_mc",
|
|
106
121
|
"concurrency_stripe_sub_id",
|
|
@@ -127,6 +142,7 @@ async function hasCurrentCoreSchema(db) {
|
|
|
127
142
|
const checks = [
|
|
128
143
|
`(SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name IN (${sqlStringList(CORE_SCHEMA_TABLES)})) = ${CORE_SCHEMA_TABLES.length}`,
|
|
129
144
|
`(SELECT COUNT(*) FROM sqlite_master WHERE type = 'index' AND name IN (${sqlStringList(CORE_SCHEMA_INDEXES)})) = ${CORE_SCHEMA_INDEXES.length}`,
|
|
145
|
+
`(SELECT COUNT(*) FROM sqlite_master WHERE type = 'trigger' AND name IN (${sqlStringList(CORE_SCHEMA_TRIGGERS)})) = ${CORE_SCHEMA_TRIGGERS.length}`,
|
|
130
146
|
...Object.entries(CORE_SCHEMA_COLUMNS).map(
|
|
131
147
|
([table, columns]) => `(SELECT COUNT(*) FROM pragma_table_info('${table}') WHERE name IN (${sqlStringList(columns)})) = ${columns.length}`
|
|
132
148
|
)
|
|
@@ -402,6 +418,76 @@ async function migrate() {
|
|
|
402
418
|
} catch {
|
|
403
419
|
}
|
|
404
420
|
await db.execute(`CREATE INDEX IF NOT EXISTS credit_lots_user_active ON credit_lots(user_id, expires_at, remaining_mc)`);
|
|
421
|
+
await db.execute(`
|
|
422
|
+
CREATE TABLE IF NOT EXISTS credit_lot_expirations (
|
|
423
|
+
lot_id INTEGER PRIMARY KEY REFERENCES credit_lots(id),
|
|
424
|
+
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
425
|
+
amount_mc INTEGER NOT NULL,
|
|
426
|
+
claim_token TEXT NOT NULL UNIQUE,
|
|
427
|
+
expired_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
428
|
+
)
|
|
429
|
+
`);
|
|
430
|
+
await db.execute(`
|
|
431
|
+
CREATE TRIGGER IF NOT EXISTS credit_lots_bounds_insert
|
|
432
|
+
BEFORE INSERT ON credit_lots
|
|
433
|
+
WHEN NEW.amount_mc < 0
|
|
434
|
+
OR NEW.remaining_mc < 0
|
|
435
|
+
OR NEW.remaining_mc > NEW.amount_mc
|
|
436
|
+
BEGIN
|
|
437
|
+
SELECT RAISE(ABORT, 'invalid credit lot bounds');
|
|
438
|
+
END
|
|
439
|
+
`);
|
|
440
|
+
await db.execute(`
|
|
441
|
+
CREATE TRIGGER IF NOT EXISTS credit_lots_bounds_update
|
|
442
|
+
BEFORE UPDATE OF amount_mc, remaining_mc ON credit_lots
|
|
443
|
+
WHEN NEW.amount_mc < 0
|
|
444
|
+
OR NEW.remaining_mc < 0
|
|
445
|
+
OR NEW.remaining_mc > NEW.amount_mc
|
|
446
|
+
BEGIN
|
|
447
|
+
SELECT RAISE(ABORT, 'invalid credit lot bounds');
|
|
448
|
+
END
|
|
449
|
+
`);
|
|
450
|
+
await db.execute(`
|
|
451
|
+
CREATE TRIGGER IF NOT EXISTS credit_lots_one_migration_insert
|
|
452
|
+
BEFORE INSERT ON credit_lots
|
|
453
|
+
WHEN NEW.source = 'migration'
|
|
454
|
+
AND EXISTS (
|
|
455
|
+
SELECT 1 FROM credit_lots
|
|
456
|
+
WHERE user_id = NEW.user_id AND source = 'migration'
|
|
457
|
+
)
|
|
458
|
+
BEGIN
|
|
459
|
+
SELECT RAISE(ABORT, 'duplicate migration lot');
|
|
460
|
+
END
|
|
461
|
+
`);
|
|
462
|
+
await db.execute(`
|
|
463
|
+
CREATE TRIGGER IF NOT EXISTS credit_lots_one_migration_update
|
|
464
|
+
BEFORE UPDATE OF source, user_id ON credit_lots
|
|
465
|
+
WHEN NEW.source = 'migration'
|
|
466
|
+
AND EXISTS (
|
|
467
|
+
SELECT 1 FROM credit_lots
|
|
468
|
+
WHERE user_id = NEW.user_id
|
|
469
|
+
AND source = 'migration'
|
|
470
|
+
AND id != OLD.id
|
|
471
|
+
)
|
|
472
|
+
BEGIN
|
|
473
|
+
SELECT RAISE(ABORT, 'duplicate migration lot');
|
|
474
|
+
END
|
|
475
|
+
`);
|
|
476
|
+
await db.execute(`
|
|
477
|
+
CREATE TABLE IF NOT EXISTS credit_lot_repairs (
|
|
478
|
+
repair_id TEXT PRIMARY KEY,
|
|
479
|
+
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
480
|
+
report_sha256 TEXT NOT NULL,
|
|
481
|
+
classification TEXT NOT NULL,
|
|
482
|
+
before_json TEXT NOT NULL,
|
|
483
|
+
plan_json TEXT NOT NULL,
|
|
484
|
+
after_json TEXT,
|
|
485
|
+
status TEXT NOT NULL DEFAULT 'claimed',
|
|
486
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
487
|
+
applied_at TEXT
|
|
488
|
+
)
|
|
489
|
+
`);
|
|
490
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS credit_lot_repairs_user_applied ON credit_lot_repairs(user_id, applied_at)`);
|
|
405
491
|
await db.execute(`
|
|
406
492
|
CREATE TABLE IF NOT EXISTS free_credit_refreshes (
|
|
407
493
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
@@ -776,10 +862,37 @@ async function migrate() {
|
|
|
776
862
|
await db.execute(`CREATE INDEX IF NOT EXISTS workflow_webhook_deliveries_run ON workflow_webhook_deliveries(run_id)`);
|
|
777
863
|
await db.execute(`
|
|
778
864
|
CREATE TABLE IF NOT EXISTS stripe_events (
|
|
779
|
-
event_id
|
|
780
|
-
|
|
865
|
+
event_id TEXT PRIMARY KEY,
|
|
866
|
+
status TEXT NOT NULL DEFAULT 'processed',
|
|
867
|
+
attempt_count INTEGER NOT NULL DEFAULT 1,
|
|
868
|
+
claim_token TEXT,
|
|
869
|
+
last_error TEXT,
|
|
870
|
+
processed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
871
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
781
872
|
)
|
|
782
873
|
`);
|
|
874
|
+
for (const col of [
|
|
875
|
+
`status TEXT NOT NULL DEFAULT 'processed'`,
|
|
876
|
+
"attempt_count INTEGER NOT NULL DEFAULT 1",
|
|
877
|
+
"claim_token TEXT",
|
|
878
|
+
"last_error TEXT",
|
|
879
|
+
"updated_at TEXT"
|
|
880
|
+
]) {
|
|
881
|
+
try {
|
|
882
|
+
await db.execute(`ALTER TABLE stripe_events ADD COLUMN ${col}`);
|
|
883
|
+
} catch {
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
await db.execute(`
|
|
887
|
+
UPDATE stripe_events
|
|
888
|
+
SET updated_at = COALESCE(updated_at, processed_at),
|
|
889
|
+
status = COALESCE(status, 'processed'),
|
|
890
|
+
attempt_count = MAX(1, COALESCE(attempt_count, 1))
|
|
891
|
+
WHERE updated_at IS NULL
|
|
892
|
+
OR status IS NULL
|
|
893
|
+
OR attempt_count IS NULL
|
|
894
|
+
OR attempt_count < 1
|
|
895
|
+
`);
|
|
783
896
|
await db.execute(`
|
|
784
897
|
CREATE TABLE IF NOT EXISTS oauth_clients (
|
|
785
898
|
client_id TEXT PRIMARY KEY,
|
|
@@ -945,16 +1058,60 @@ async function rotateRefresh(oldToken, next) {
|
|
|
945
1058
|
args: [next.refresh_token, (/* @__PURE__ */ new Date()).toISOString(), oldToken]
|
|
946
1059
|
});
|
|
947
1060
|
}
|
|
948
|
-
async function
|
|
949
|
-
const
|
|
950
|
-
|
|
1061
|
+
async function claimStripeEvent(eventId) {
|
|
1062
|
+
const token = randomUUID();
|
|
1063
|
+
const claimed = await getDb().execute({
|
|
1064
|
+
sql: `INSERT INTO stripe_events
|
|
1065
|
+
(event_id, status, attempt_count, claim_token, last_error, processed_at, updated_at)
|
|
1066
|
+
VALUES (?, 'processing', 1, ?, NULL, datetime('now'), datetime('now'))
|
|
1067
|
+
ON CONFLICT(event_id) DO UPDATE SET
|
|
1068
|
+
status = 'processing',
|
|
1069
|
+
attempt_count = stripe_events.attempt_count + 1,
|
|
1070
|
+
claim_token = excluded.claim_token,
|
|
1071
|
+
last_error = NULL,
|
|
1072
|
+
updated_at = datetime('now')
|
|
1073
|
+
WHERE stripe_events.status = 'failed'
|
|
1074
|
+
OR (
|
|
1075
|
+
stripe_events.status = 'processing'
|
|
1076
|
+
AND stripe_events.updated_at <= datetime('now', '-10 minutes')
|
|
1077
|
+
)`,
|
|
1078
|
+
args: [eventId, token]
|
|
1079
|
+
});
|
|
1080
|
+
if (claimed.rowsAffected > 0) return { state: "claimed", token };
|
|
1081
|
+
const current = await getDb().execute({
|
|
1082
|
+
sql: "SELECT status FROM stripe_events WHERE event_id = ? LIMIT 1",
|
|
951
1083
|
args: [eventId]
|
|
952
1084
|
});
|
|
953
|
-
return
|
|
1085
|
+
return { state: String(current.rows[0]?.status) === "processed" ? "processed" : "processing" };
|
|
1086
|
+
}
|
|
1087
|
+
async function completeStripeEvent(eventId, token) {
|
|
1088
|
+
const result = await getDb().execute({
|
|
1089
|
+
sql: `UPDATE stripe_events
|
|
1090
|
+
SET status = 'processed',
|
|
1091
|
+
claim_token = NULL,
|
|
1092
|
+
last_error = NULL,
|
|
1093
|
+
processed_at = datetime('now'),
|
|
1094
|
+
updated_at = datetime('now')
|
|
1095
|
+
WHERE event_id = ? AND status = 'processing' AND claim_token = ?`,
|
|
1096
|
+
args: [eventId, token]
|
|
1097
|
+
});
|
|
1098
|
+
return result.rowsAffected === 1;
|
|
1099
|
+
}
|
|
1100
|
+
async function failStripeEvent(eventId, token, error) {
|
|
1101
|
+
const result = await getDb().execute({
|
|
1102
|
+
sql: `UPDATE stripe_events
|
|
1103
|
+
SET status = 'failed',
|
|
1104
|
+
claim_token = NULL,
|
|
1105
|
+
last_error = ?,
|
|
1106
|
+
updated_at = datetime('now')
|
|
1107
|
+
WHERE event_id = ? AND status = 'processing' AND claim_token = ?`,
|
|
1108
|
+
args: [error.slice(0, 500), eventId, token]
|
|
1109
|
+
});
|
|
1110
|
+
return result.rowsAffected === 1;
|
|
954
1111
|
}
|
|
955
1112
|
async function stripeEventAlreadyProcessed(eventId) {
|
|
956
1113
|
const res = await getDb().execute({
|
|
957
|
-
sql:
|
|
1114
|
+
sql: `SELECT 1 FROM stripe_events WHERE event_id = ? AND status = 'processed' LIMIT 1`,
|
|
958
1115
|
args: [eventId]
|
|
959
1116
|
});
|
|
960
1117
|
return res.rows.length > 0;
|
|
@@ -1936,74 +2093,114 @@ async function resolveUserByMemorySubscription(subscriptionId) {
|
|
|
1936
2093
|
var CREDIT_LOT_TTL = "+3 months";
|
|
1937
2094
|
async function ensureMigrated(userId) {
|
|
1938
2095
|
const db = getDb();
|
|
1939
|
-
const
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
2096
|
+
const inserted = await db.execute({
|
|
2097
|
+
sql: `INSERT INTO credit_lots (user_id, amount_mc, remaining_mc, source, expires_at)
|
|
2098
|
+
SELECT ?, legacy.balance_mc, legacy.balance_mc, 'migration', datetime('now', ?)
|
|
2099
|
+
FROM (
|
|
2100
|
+
SELECT COALESCE(SUM(amount_mc), 0) AS balance_mc
|
|
2101
|
+
FROM ledger
|
|
2102
|
+
WHERE user_id = ?
|
|
2103
|
+
) AS legacy
|
|
2104
|
+
WHERE legacy.balance_mc > 0
|
|
2105
|
+
AND EXISTS (
|
|
2106
|
+
SELECT 1 FROM ledger
|
|
2107
|
+
WHERE user_id = ? AND created_at < ?
|
|
2108
|
+
)
|
|
2109
|
+
AND NOT EXISTS (
|
|
2110
|
+
SELECT 1 FROM credit_lots WHERE user_id = ?
|
|
2111
|
+
)`,
|
|
2112
|
+
args: [userId, CREDIT_LOT_TTL, userId, userId, CREDIT_LOTS_EPOCH, userId]
|
|
1955
2113
|
});
|
|
1956
|
-
|
|
1957
|
-
const updates = [];
|
|
1958
|
-
for (const lot of lots.rows) {
|
|
1959
|
-
if (need <= 0) break;
|
|
1960
|
-
const take = Math.min(Number(lot.remaining_mc), need);
|
|
1961
|
-
updates.push({ sql: "UPDATE credit_lots SET remaining_mc = remaining_mc - ? WHERE id = ?", args: [take, Number(lot.id)] });
|
|
1962
|
-
need -= take;
|
|
1963
|
-
}
|
|
1964
|
-
if (updates.length) await db.batch(updates, "write");
|
|
2114
|
+
return inserted.rowsAffected;
|
|
1965
2115
|
}
|
|
1966
2116
|
async function expireOldLots() {
|
|
1967
2117
|
const db = getDb();
|
|
1968
2118
|
const lots = await db.execute({
|
|
1969
|
-
sql: `SELECT id
|
|
2119
|
+
sql: `SELECT id
|
|
2120
|
+
FROM credit_lots
|
|
2121
|
+
WHERE remaining_mc > 0
|
|
2122
|
+
AND never_expires = 0
|
|
2123
|
+
AND expires_at <= datetime('now')
|
|
2124
|
+
LIMIT 500`
|
|
1970
2125
|
});
|
|
1971
2126
|
if (lots.rows.length === 0) return { expired_lots: 0, expired_mc: 0 };
|
|
1972
|
-
|
|
1973
|
-
const users = /* @__PURE__ */ new Set();
|
|
2127
|
+
const claims = [];
|
|
1974
2128
|
const stmts = [];
|
|
1975
2129
|
for (const lot of lots.rows) {
|
|
1976
|
-
const
|
|
1977
|
-
const
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
2130
|
+
const lotId = Number(lot.id);
|
|
2131
|
+
const token = randomUUID();
|
|
2132
|
+
claims.push({ token, insertResultIndex: stmts.length });
|
|
2133
|
+
stmts.push({
|
|
2134
|
+
sql: `INSERT OR IGNORE INTO credit_lot_expirations (lot_id, user_id, amount_mc, claim_token)
|
|
2135
|
+
SELECT id, user_id, remaining_mc, ?
|
|
2136
|
+
FROM credit_lots
|
|
2137
|
+
WHERE id = ?
|
|
2138
|
+
AND remaining_mc > 0
|
|
2139
|
+
AND never_expires = 0
|
|
2140
|
+
AND expires_at <= datetime('now')`,
|
|
2141
|
+
args: [token, lotId]
|
|
2142
|
+
});
|
|
2143
|
+
stmts.push({
|
|
2144
|
+
sql: `UPDATE credit_lots
|
|
2145
|
+
SET remaining_mc = 0
|
|
2146
|
+
WHERE id = ?
|
|
2147
|
+
AND EXISTS (
|
|
2148
|
+
SELECT 1 FROM credit_lot_expirations
|
|
2149
|
+
WHERE lot_id = ? AND claim_token = ?
|
|
2150
|
+
)`,
|
|
2151
|
+
args: [lotId, lotId, token]
|
|
2152
|
+
});
|
|
2153
|
+
stmts.push({
|
|
2154
|
+
sql: `INSERT INTO ledger (user_id, amount_mc, operation, description)
|
|
2155
|
+
SELECT user_id, -amount_mc, 'credit_expired', 'credits expired'
|
|
2156
|
+
FROM credit_lot_expirations
|
|
2157
|
+
WHERE lot_id = ? AND claim_token = ?`,
|
|
2158
|
+
args: [lotId, token]
|
|
2159
|
+
});
|
|
1982
2160
|
}
|
|
1983
|
-
await db.batch(stmts, "write");
|
|
2161
|
+
const results = await db.batch(stmts, "write");
|
|
2162
|
+
const claimedTokens = claims.filter((claim) => results[claim.insertResultIndex]?.rowsAffected === 1).map((claim) => claim.token);
|
|
2163
|
+
if (claimedTokens.length === 0) return { expired_lots: 0, expired_mc: 0 };
|
|
2164
|
+
const placeholders = claimedTokens.map(() => "?").join(", ");
|
|
2165
|
+
const expired = await db.execute({
|
|
2166
|
+
sql: `SELECT user_id, amount_mc
|
|
2167
|
+
FROM credit_lot_expirations
|
|
2168
|
+
WHERE claim_token IN (${placeholders})`,
|
|
2169
|
+
args: claimedTokens
|
|
2170
|
+
});
|
|
2171
|
+
const users = new Set(expired.rows.map((row) => Number(row.user_id)));
|
|
1984
2172
|
for (const uid of users) await reconcileBalanceMc(uid);
|
|
1985
|
-
return {
|
|
2173
|
+
return {
|
|
2174
|
+
expired_lots: expired.rows.length,
|
|
2175
|
+
expired_mc: expired.rows.reduce((sum, row) => sum + Number(row.amount_mc), 0)
|
|
2176
|
+
};
|
|
1986
2177
|
}
|
|
1987
2178
|
async function migrateExistingBalancesToLots() {
|
|
1988
2179
|
const db = getDb();
|
|
1989
2180
|
const res = await db.execute({
|
|
1990
|
-
sql: `SELECT u.id AS user_id
|
|
1991
|
-
FROM users u
|
|
1992
|
-
|
|
1993
|
-
|
|
2181
|
+
sql: `SELECT DISTINCT u.id AS user_id
|
|
2182
|
+
FROM users u
|
|
2183
|
+
JOIN ledger l ON l.user_id = u.id
|
|
2184
|
+
WHERE l.created_at < ?
|
|
2185
|
+
AND NOT EXISTS (SELECT 1 FROM credit_lots WHERE user_id = u.id)`,
|
|
2186
|
+
args: [CREDIT_LOTS_EPOCH]
|
|
1994
2187
|
});
|
|
1995
2188
|
let migrated = 0;
|
|
1996
2189
|
let totalMc = 0;
|
|
1997
2190
|
for (const row of res.rows) {
|
|
1998
2191
|
const uid = Number(row.user_id);
|
|
1999
|
-
const
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2192
|
+
const inserted = await ensureMigrated(uid);
|
|
2193
|
+
if (inserted === 0) continue;
|
|
2194
|
+
const lot = await db.execute({
|
|
2195
|
+
sql: `SELECT amount_mc FROM credit_lots
|
|
2196
|
+
WHERE user_id = ? AND source = 'migration'
|
|
2197
|
+
ORDER BY id DESC LIMIT 1`,
|
|
2198
|
+
args: [uid]
|
|
2003
2199
|
});
|
|
2200
|
+
const amountMc = Number(lot.rows[0]?.amount_mc ?? 0);
|
|
2004
2201
|
await reconcileBalanceMc(uid);
|
|
2005
|
-
migrated
|
|
2006
|
-
totalMc +=
|
|
2202
|
+
migrated += 1;
|
|
2203
|
+
totalMc += amountMc;
|
|
2007
2204
|
}
|
|
2008
2205
|
return { migrated, total_mc: totalMc };
|
|
2009
2206
|
}
|
|
@@ -2017,19 +2214,22 @@ async function creditMc(userId, mc, operation, description, stripePaymentIntent,
|
|
|
2017
2214
|
}
|
|
2018
2215
|
const expiry = neverExpires ? "9999-12-31 23:59:59" : null;
|
|
2019
2216
|
if (stripePaymentIntent) {
|
|
2020
|
-
const
|
|
2217
|
+
const stmts2 = [{
|
|
2021
2218
|
sql: "INSERT OR IGNORE INTO ledger (user_id, amount_mc, operation, description, stripe_pi) VALUES (?, ?, ?, ?, ?)",
|
|
2022
2219
|
args: [userId, mc, operation, description ?? null, stripePaymentIntent]
|
|
2023
|
-
}
|
|
2024
|
-
if (ledgerInsert.rowsAffected === 0) {
|
|
2025
|
-
const res3 = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
|
|
2026
|
-
return Number(res3.rows[0]?.balance_mc ?? 0);
|
|
2027
|
-
}
|
|
2028
|
-
const stmts2 = [];
|
|
2220
|
+
}];
|
|
2029
2221
|
if (mc > 0) {
|
|
2030
|
-
stmts2.push({
|
|
2222
|
+
stmts2.push({
|
|
2223
|
+
sql: `INSERT INTO credit_lots (user_id, amount_mc, remaining_mc, source, stripe_pi, expires_at, never_expires)
|
|
2224
|
+
SELECT ?, ?, ?, ?, ?, COALESCE(?, datetime('now', ?)), ?
|
|
2225
|
+
WHERE changes() = 1`,
|
|
2226
|
+
args: [userId, mc, mc, operation, stripePaymentIntent, expiry, CREDIT_LOT_TTL, neverExpires ? 1 : 0]
|
|
2227
|
+
});
|
|
2031
2228
|
}
|
|
2032
|
-
stmts2.push({
|
|
2229
|
+
stmts2.push({
|
|
2230
|
+
sql: "UPDATE users SET balance_mc = balance_mc + ? WHERE id = ? AND changes() = 1",
|
|
2231
|
+
args: [mc, userId]
|
|
2232
|
+
});
|
|
2033
2233
|
await db.batch(stmts2, "write");
|
|
2034
2234
|
const res2 = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
|
|
2035
2235
|
return Number(res2.rows[0]?.balance_mc ?? 0);
|
|
@@ -2137,23 +2337,14 @@ async function claimMonthlyFreeRefresh(userId, month) {
|
|
|
2137
2337
|
return res.rowsAffected > 0;
|
|
2138
2338
|
}
|
|
2139
2339
|
async function debitMc(userId, mc, operation, description) {
|
|
2140
|
-
const
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
return { ok: false, balance_mc: Number(res2.rows[0]?.balance_mc ?? 0) };
|
|
2149
|
-
}
|
|
2150
|
-
await drainLots(userId, mc);
|
|
2151
|
-
await db.execute({
|
|
2152
|
-
sql: "INSERT INTO ledger (user_id, amount_mc, operation, description) VALUES (?, ?, ?, ?)",
|
|
2153
|
-
args: [userId, -mc, operation, description ?? null]
|
|
2154
|
-
});
|
|
2155
|
-
const res = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
|
|
2156
|
-
return { ok: true, balance_mc: Number(res.rows[0]?.balance_mc ?? 0) };
|
|
2340
|
+
const debit = await debitMcIdempotent(
|
|
2341
|
+
userId,
|
|
2342
|
+
mc,
|
|
2343
|
+
operation,
|
|
2344
|
+
description ?? "",
|
|
2345
|
+
`debit:${randomUUID()}`
|
|
2346
|
+
);
|
|
2347
|
+
return { ok: debit.ok, balance_mc: debit.balance_mc };
|
|
2157
2348
|
}
|
|
2158
2349
|
async function debitMcIdempotent(userId, mc, operation, description, idempotencyKey) {
|
|
2159
2350
|
if (!Number.isSafeInteger(mc) || mc <= 0) throw new Error("idempotent debit amount must be a positive integer");
|
|
@@ -2572,29 +2763,46 @@ async function ledgerExistsForStripePI(stripePI) {
|
|
|
2572
2763
|
}
|
|
2573
2764
|
async function reconcileBalanceMc(userId, knownBalanceMc) {
|
|
2574
2765
|
const db = getDb();
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2766
|
+
void knownBalanceMc;
|
|
2767
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
2768
|
+
const snapshot = await db.execute({
|
|
2769
|
+
sql: `SELECT
|
|
2770
|
+
u.balance_mc AS cached_balance_mc,
|
|
2771
|
+
COUNT(l.id) AS lot_count,
|
|
2772
|
+
COALESCE(SUM(CASE
|
|
2773
|
+
WHEN l.remaining_mc > 0
|
|
2774
|
+
AND (l.never_expires = 1 OR l.expires_at > datetime('now'))
|
|
2775
|
+
THEN l.remaining_mc ELSE 0 END), 0) AS projected_balance_mc
|
|
2776
|
+
FROM users u
|
|
2777
|
+
LEFT JOIN credit_lots l ON l.user_id = u.id
|
|
2778
|
+
WHERE u.id = ?
|
|
2779
|
+
GROUP BY u.id, u.balance_mc`,
|
|
2587
2780
|
args: [userId]
|
|
2588
2781
|
});
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2782
|
+
const row = snapshot.rows[0];
|
|
2783
|
+
if (!row) return 0;
|
|
2784
|
+
if (Number(row.lot_count ?? 0) === 0) {
|
|
2785
|
+
await ensureMigrated(Number(userId));
|
|
2786
|
+
const nowHasLots = await db.execute({
|
|
2787
|
+
sql: "SELECT 1 FROM credit_lots WHERE user_id = ? LIMIT 1",
|
|
2788
|
+
args: [userId]
|
|
2789
|
+
});
|
|
2790
|
+
if (nowHasLots.rows.length > 0) continue;
|
|
2791
|
+
}
|
|
2792
|
+
const cachedBalanceMc = Number(row.cached_balance_mc ?? 0);
|
|
2793
|
+
const projectedBalanceMc = Number(row.projected_balance_mc ?? 0);
|
|
2794
|
+
if (cachedBalanceMc === projectedBalanceMc) return projectedBalanceMc;
|
|
2795
|
+
const updated = await db.execute({
|
|
2796
|
+
sql: "UPDATE users SET balance_mc = ? WHERE id = ? AND balance_mc = ?",
|
|
2797
|
+
args: [projectedBalanceMc, userId, cachedBalanceMc]
|
|
2595
2798
|
});
|
|
2799
|
+
if (updated.rowsAffected === 1) return projectedBalanceMc;
|
|
2596
2800
|
}
|
|
2597
|
-
|
|
2801
|
+
const current = await db.execute({
|
|
2802
|
+
sql: "SELECT balance_mc FROM users WHERE id = ? LIMIT 1",
|
|
2803
|
+
args: [userId]
|
|
2804
|
+
});
|
|
2805
|
+
return Number(current.rows[0]?.balance_mc ?? 0);
|
|
2598
2806
|
}
|
|
2599
2807
|
var SiteAuditJobRowSchema = z.object({
|
|
2600
2808
|
id: z.string(),
|
|
@@ -2678,6 +2886,7 @@ async function listActiveUserIds() {
|
|
|
2678
2886
|
}
|
|
2679
2887
|
|
|
2680
2888
|
export {
|
|
2889
|
+
CREDIT_LOTS_EPOCH,
|
|
2681
2890
|
CORE_SCHEMA_VERSION,
|
|
2682
2891
|
getDb,
|
|
2683
2892
|
setDbForTesting,
|
|
@@ -2690,7 +2899,9 @@ export {
|
|
|
2690
2899
|
getRefresh,
|
|
2691
2900
|
revokeRefresh,
|
|
2692
2901
|
rotateRefresh,
|
|
2693
|
-
|
|
2902
|
+
claimStripeEvent,
|
|
2903
|
+
completeStripeEvent,
|
|
2904
|
+
failStripeEvent,
|
|
2694
2905
|
stripeEventAlreadyProcessed,
|
|
2695
2906
|
generateApiKey,
|
|
2696
2907
|
hashPassword,
|
|
@@ -2793,4 +3004,4 @@ export {
|
|
|
2793
3004
|
insertInboxMessage,
|
|
2794
3005
|
listActiveUserIds
|
|
2795
3006
|
};
|
|
2796
|
-
//# sourceMappingURL=chunk-
|
|
3007
|
+
//# sourceMappingURL=chunk-N7KUTTCC.js.map
|