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
package/dist/bin/api-server.cjs
CHANGED
|
@@ -3598,10 +3598,20 @@ var init_url_utils = __esm({
|
|
|
3598
3598
|
}
|
|
3599
3599
|
});
|
|
3600
3600
|
|
|
3601
|
+
// src/api/credit-lot-integrity.ts
|
|
3602
|
+
var CREDIT_LOTS_EPOCH;
|
|
3603
|
+
var init_credit_lot_integrity = __esm({
|
|
3604
|
+
"src/api/credit-lot-integrity.ts"() {
|
|
3605
|
+
"use strict";
|
|
3606
|
+
CREDIT_LOTS_EPOCH = "2026-06-26 21:58:03";
|
|
3607
|
+
}
|
|
3608
|
+
});
|
|
3609
|
+
|
|
3601
3610
|
// src/api/db.ts
|
|
3602
3611
|
var db_exports = {};
|
|
3603
3612
|
__export(db_exports, {
|
|
3604
3613
|
CORE_SCHEMA_VERSION: () => CORE_SCHEMA_VERSION,
|
|
3614
|
+
CREDIT_LOTS_EPOCH: () => CREDIT_LOTS_EPOCH,
|
|
3605
3615
|
CREDIT_LOT_TTL: () => CREDIT_LOT_TTL,
|
|
3606
3616
|
SiteAuditJobRowSchema: () => SiteAuditJobRowSchema,
|
|
3607
3617
|
SiteAuditPhaseLogRowSchema: () => SiteAuditPhaseLogRowSchema,
|
|
@@ -3612,8 +3622,10 @@ __export(db_exports, {
|
|
|
3612
3622
|
claimMonthlyFreeRefresh: () => claimMonthlyFreeRefresh,
|
|
3613
3623
|
claimPendingJob: () => claimPendingJob,
|
|
3614
3624
|
claimPendingKpoJob: () => claimPendingKpoJob,
|
|
3625
|
+
claimStripeEvent: () => claimStripeEvent,
|
|
3615
3626
|
completeJob: () => completeJob,
|
|
3616
3627
|
completeKpoJob: () => completeKpoJob,
|
|
3628
|
+
completeStripeEvent: () => completeStripeEvent,
|
|
3617
3629
|
completeWorkflowRunRecord: () => completeWorkflowRunRecord,
|
|
3618
3630
|
consumeCode: () => consumeCode,
|
|
3619
3631
|
consumePasswordResetToken: () => consumePasswordResetToken,
|
|
@@ -3635,6 +3647,7 @@ __export(db_exports, {
|
|
|
3635
3647
|
expireOldLots: () => expireOldLots,
|
|
3636
3648
|
failJob: () => failJob,
|
|
3637
3649
|
failKpoJob: () => failKpoJob,
|
|
3650
|
+
failStripeEvent: () => failStripeEvent,
|
|
3638
3651
|
failWorkflowRunRecord: () => failWorkflowRunRecord,
|
|
3639
3652
|
finishHarvestAttempt: () => finishHarvestAttempt,
|
|
3640
3653
|
generateApiKey: () => generateApiKey,
|
|
@@ -3689,7 +3702,6 @@ __export(db_exports, {
|
|
|
3689
3702
|
putCode: () => putCode,
|
|
3690
3703
|
putRefresh: () => putRefresh,
|
|
3691
3704
|
reconcileBalanceMc: () => reconcileBalanceMc,
|
|
3692
|
-
recordStripeEvent: () => recordStripeEvent,
|
|
3693
3705
|
recordWorkflowWebhookDelivery: () => recordWorkflowWebhookDelivery,
|
|
3694
3706
|
registerClient: () => registerClient,
|
|
3695
3707
|
replaceWorkflowArtifacts: () => replaceWorkflowArtifacts,
|
|
@@ -3723,6 +3735,7 @@ async function hasCurrentCoreSchema(db) {
|
|
|
3723
3735
|
const checks = [
|
|
3724
3736
|
`(SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name IN (${sqlStringList(CORE_SCHEMA_TABLES)})) = ${CORE_SCHEMA_TABLES.length}`,
|
|
3725
3737
|
`(SELECT COUNT(*) FROM sqlite_master WHERE type = 'index' AND name IN (${sqlStringList(CORE_SCHEMA_INDEXES)})) = ${CORE_SCHEMA_INDEXES.length}`,
|
|
3738
|
+
`(SELECT COUNT(*) FROM sqlite_master WHERE type = 'trigger' AND name IN (${sqlStringList(CORE_SCHEMA_TRIGGERS)})) = ${CORE_SCHEMA_TRIGGERS.length}`,
|
|
3726
3739
|
...Object.entries(CORE_SCHEMA_COLUMNS).map(
|
|
3727
3740
|
([table, columns]) => `(SELECT COUNT(*) FROM pragma_table_info('${table}') WHERE name IN (${sqlStringList(columns)})) = ${columns.length}`
|
|
3728
3741
|
)
|
|
@@ -3998,6 +4011,76 @@ async function migrate() {
|
|
|
3998
4011
|
} catch {
|
|
3999
4012
|
}
|
|
4000
4013
|
await db.execute(`CREATE INDEX IF NOT EXISTS credit_lots_user_active ON credit_lots(user_id, expires_at, remaining_mc)`);
|
|
4014
|
+
await db.execute(`
|
|
4015
|
+
CREATE TABLE IF NOT EXISTS credit_lot_expirations (
|
|
4016
|
+
lot_id INTEGER PRIMARY KEY REFERENCES credit_lots(id),
|
|
4017
|
+
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
4018
|
+
amount_mc INTEGER NOT NULL,
|
|
4019
|
+
claim_token TEXT NOT NULL UNIQUE,
|
|
4020
|
+
expired_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4021
|
+
)
|
|
4022
|
+
`);
|
|
4023
|
+
await db.execute(`
|
|
4024
|
+
CREATE TRIGGER IF NOT EXISTS credit_lots_bounds_insert
|
|
4025
|
+
BEFORE INSERT ON credit_lots
|
|
4026
|
+
WHEN NEW.amount_mc < 0
|
|
4027
|
+
OR NEW.remaining_mc < 0
|
|
4028
|
+
OR NEW.remaining_mc > NEW.amount_mc
|
|
4029
|
+
BEGIN
|
|
4030
|
+
SELECT RAISE(ABORT, 'invalid credit lot bounds');
|
|
4031
|
+
END
|
|
4032
|
+
`);
|
|
4033
|
+
await db.execute(`
|
|
4034
|
+
CREATE TRIGGER IF NOT EXISTS credit_lots_bounds_update
|
|
4035
|
+
BEFORE UPDATE OF amount_mc, remaining_mc ON credit_lots
|
|
4036
|
+
WHEN NEW.amount_mc < 0
|
|
4037
|
+
OR NEW.remaining_mc < 0
|
|
4038
|
+
OR NEW.remaining_mc > NEW.amount_mc
|
|
4039
|
+
BEGIN
|
|
4040
|
+
SELECT RAISE(ABORT, 'invalid credit lot bounds');
|
|
4041
|
+
END
|
|
4042
|
+
`);
|
|
4043
|
+
await db.execute(`
|
|
4044
|
+
CREATE TRIGGER IF NOT EXISTS credit_lots_one_migration_insert
|
|
4045
|
+
BEFORE INSERT ON credit_lots
|
|
4046
|
+
WHEN NEW.source = 'migration'
|
|
4047
|
+
AND EXISTS (
|
|
4048
|
+
SELECT 1 FROM credit_lots
|
|
4049
|
+
WHERE user_id = NEW.user_id AND source = 'migration'
|
|
4050
|
+
)
|
|
4051
|
+
BEGIN
|
|
4052
|
+
SELECT RAISE(ABORT, 'duplicate migration lot');
|
|
4053
|
+
END
|
|
4054
|
+
`);
|
|
4055
|
+
await db.execute(`
|
|
4056
|
+
CREATE TRIGGER IF NOT EXISTS credit_lots_one_migration_update
|
|
4057
|
+
BEFORE UPDATE OF source, user_id ON credit_lots
|
|
4058
|
+
WHEN NEW.source = 'migration'
|
|
4059
|
+
AND EXISTS (
|
|
4060
|
+
SELECT 1 FROM credit_lots
|
|
4061
|
+
WHERE user_id = NEW.user_id
|
|
4062
|
+
AND source = 'migration'
|
|
4063
|
+
AND id != OLD.id
|
|
4064
|
+
)
|
|
4065
|
+
BEGIN
|
|
4066
|
+
SELECT RAISE(ABORT, 'duplicate migration lot');
|
|
4067
|
+
END
|
|
4068
|
+
`);
|
|
4069
|
+
await db.execute(`
|
|
4070
|
+
CREATE TABLE IF NOT EXISTS credit_lot_repairs (
|
|
4071
|
+
repair_id TEXT PRIMARY KEY,
|
|
4072
|
+
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
4073
|
+
report_sha256 TEXT NOT NULL,
|
|
4074
|
+
classification TEXT NOT NULL,
|
|
4075
|
+
before_json TEXT NOT NULL,
|
|
4076
|
+
plan_json TEXT NOT NULL,
|
|
4077
|
+
after_json TEXT,
|
|
4078
|
+
status TEXT NOT NULL DEFAULT 'claimed',
|
|
4079
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
4080
|
+
applied_at TEXT
|
|
4081
|
+
)
|
|
4082
|
+
`);
|
|
4083
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS credit_lot_repairs_user_applied ON credit_lot_repairs(user_id, applied_at)`);
|
|
4001
4084
|
await db.execute(`
|
|
4002
4085
|
CREATE TABLE IF NOT EXISTS free_credit_refreshes (
|
|
4003
4086
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
@@ -4372,10 +4455,37 @@ async function migrate() {
|
|
|
4372
4455
|
await db.execute(`CREATE INDEX IF NOT EXISTS workflow_webhook_deliveries_run ON workflow_webhook_deliveries(run_id)`);
|
|
4373
4456
|
await db.execute(`
|
|
4374
4457
|
CREATE TABLE IF NOT EXISTS stripe_events (
|
|
4375
|
-
event_id
|
|
4376
|
-
|
|
4458
|
+
event_id TEXT PRIMARY KEY,
|
|
4459
|
+
status TEXT NOT NULL DEFAULT 'processed',
|
|
4460
|
+
attempt_count INTEGER NOT NULL DEFAULT 1,
|
|
4461
|
+
claim_token TEXT,
|
|
4462
|
+
last_error TEXT,
|
|
4463
|
+
processed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
4464
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4377
4465
|
)
|
|
4378
4466
|
`);
|
|
4467
|
+
for (const col of [
|
|
4468
|
+
`status TEXT NOT NULL DEFAULT 'processed'`,
|
|
4469
|
+
"attempt_count INTEGER NOT NULL DEFAULT 1",
|
|
4470
|
+
"claim_token TEXT",
|
|
4471
|
+
"last_error TEXT",
|
|
4472
|
+
"updated_at TEXT"
|
|
4473
|
+
]) {
|
|
4474
|
+
try {
|
|
4475
|
+
await db.execute(`ALTER TABLE stripe_events ADD COLUMN ${col}`);
|
|
4476
|
+
} catch {
|
|
4477
|
+
}
|
|
4478
|
+
}
|
|
4479
|
+
await db.execute(`
|
|
4480
|
+
UPDATE stripe_events
|
|
4481
|
+
SET updated_at = COALESCE(updated_at, processed_at),
|
|
4482
|
+
status = COALESCE(status, 'processed'),
|
|
4483
|
+
attempt_count = MAX(1, COALESCE(attempt_count, 1))
|
|
4484
|
+
WHERE updated_at IS NULL
|
|
4485
|
+
OR status IS NULL
|
|
4486
|
+
OR attempt_count IS NULL
|
|
4487
|
+
OR attempt_count < 1
|
|
4488
|
+
`);
|
|
4379
4489
|
await db.execute(`
|
|
4380
4490
|
CREATE TABLE IF NOT EXISTS oauth_clients (
|
|
4381
4491
|
client_id TEXT PRIMARY KEY,
|
|
@@ -4541,16 +4651,60 @@ async function rotateRefresh(oldToken, next) {
|
|
|
4541
4651
|
args: [next.refresh_token, (/* @__PURE__ */ new Date()).toISOString(), oldToken]
|
|
4542
4652
|
});
|
|
4543
4653
|
}
|
|
4544
|
-
async function
|
|
4545
|
-
const
|
|
4546
|
-
|
|
4654
|
+
async function claimStripeEvent(eventId) {
|
|
4655
|
+
const token = (0, import_node_crypto.randomUUID)();
|
|
4656
|
+
const claimed = await getDb().execute({
|
|
4657
|
+
sql: `INSERT INTO stripe_events
|
|
4658
|
+
(event_id, status, attempt_count, claim_token, last_error, processed_at, updated_at)
|
|
4659
|
+
VALUES (?, 'processing', 1, ?, NULL, datetime('now'), datetime('now'))
|
|
4660
|
+
ON CONFLICT(event_id) DO UPDATE SET
|
|
4661
|
+
status = 'processing',
|
|
4662
|
+
attempt_count = stripe_events.attempt_count + 1,
|
|
4663
|
+
claim_token = excluded.claim_token,
|
|
4664
|
+
last_error = NULL,
|
|
4665
|
+
updated_at = datetime('now')
|
|
4666
|
+
WHERE stripe_events.status = 'failed'
|
|
4667
|
+
OR (
|
|
4668
|
+
stripe_events.status = 'processing'
|
|
4669
|
+
AND stripe_events.updated_at <= datetime('now', '-10 minutes')
|
|
4670
|
+
)`,
|
|
4671
|
+
args: [eventId, token]
|
|
4672
|
+
});
|
|
4673
|
+
if (claimed.rowsAffected > 0) return { state: "claimed", token };
|
|
4674
|
+
const current = await getDb().execute({
|
|
4675
|
+
sql: "SELECT status FROM stripe_events WHERE event_id = ? LIMIT 1",
|
|
4547
4676
|
args: [eventId]
|
|
4548
4677
|
});
|
|
4549
|
-
return
|
|
4678
|
+
return { state: String(current.rows[0]?.status) === "processed" ? "processed" : "processing" };
|
|
4679
|
+
}
|
|
4680
|
+
async function completeStripeEvent(eventId, token) {
|
|
4681
|
+
const result = await getDb().execute({
|
|
4682
|
+
sql: `UPDATE stripe_events
|
|
4683
|
+
SET status = 'processed',
|
|
4684
|
+
claim_token = NULL,
|
|
4685
|
+
last_error = NULL,
|
|
4686
|
+
processed_at = datetime('now'),
|
|
4687
|
+
updated_at = datetime('now')
|
|
4688
|
+
WHERE event_id = ? AND status = 'processing' AND claim_token = ?`,
|
|
4689
|
+
args: [eventId, token]
|
|
4690
|
+
});
|
|
4691
|
+
return result.rowsAffected === 1;
|
|
4692
|
+
}
|
|
4693
|
+
async function failStripeEvent(eventId, token, error) {
|
|
4694
|
+
const result = await getDb().execute({
|
|
4695
|
+
sql: `UPDATE stripe_events
|
|
4696
|
+
SET status = 'failed',
|
|
4697
|
+
claim_token = NULL,
|
|
4698
|
+
last_error = ?,
|
|
4699
|
+
updated_at = datetime('now')
|
|
4700
|
+
WHERE event_id = ? AND status = 'processing' AND claim_token = ?`,
|
|
4701
|
+
args: [error.slice(0, 500), eventId, token]
|
|
4702
|
+
});
|
|
4703
|
+
return result.rowsAffected === 1;
|
|
4550
4704
|
}
|
|
4551
4705
|
async function stripeEventAlreadyProcessed(eventId) {
|
|
4552
4706
|
const res = await getDb().execute({
|
|
4553
|
-
sql:
|
|
4707
|
+
sql: `SELECT 1 FROM stripe_events WHERE event_id = ? AND status = 'processed' LIMIT 1`,
|
|
4554
4708
|
args: [eventId]
|
|
4555
4709
|
});
|
|
4556
4710
|
return res.rows.length > 0;
|
|
@@ -5530,74 +5684,114 @@ async function resolveUserByMemorySubscription(subscriptionId) {
|
|
|
5530
5684
|
}
|
|
5531
5685
|
async function ensureMigrated(userId) {
|
|
5532
5686
|
const db = getDb();
|
|
5533
|
-
const
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5687
|
+
const inserted = await db.execute({
|
|
5688
|
+
sql: `INSERT INTO credit_lots (user_id, amount_mc, remaining_mc, source, expires_at)
|
|
5689
|
+
SELECT ?, legacy.balance_mc, legacy.balance_mc, 'migration', datetime('now', ?)
|
|
5690
|
+
FROM (
|
|
5691
|
+
SELECT COALESCE(SUM(amount_mc), 0) AS balance_mc
|
|
5692
|
+
FROM ledger
|
|
5693
|
+
WHERE user_id = ?
|
|
5694
|
+
) AS legacy
|
|
5695
|
+
WHERE legacy.balance_mc > 0
|
|
5696
|
+
AND EXISTS (
|
|
5697
|
+
SELECT 1 FROM ledger
|
|
5698
|
+
WHERE user_id = ? AND created_at < ?
|
|
5699
|
+
)
|
|
5700
|
+
AND NOT EXISTS (
|
|
5701
|
+
SELECT 1 FROM credit_lots WHERE user_id = ?
|
|
5702
|
+
)`,
|
|
5703
|
+
args: [userId, CREDIT_LOT_TTL, userId, userId, CREDIT_LOTS_EPOCH, userId]
|
|
5549
5704
|
});
|
|
5550
|
-
|
|
5551
|
-
const updates = [];
|
|
5552
|
-
for (const lot of lots.rows) {
|
|
5553
|
-
if (need <= 0) break;
|
|
5554
|
-
const take = Math.min(Number(lot.remaining_mc), need);
|
|
5555
|
-
updates.push({ sql: "UPDATE credit_lots SET remaining_mc = remaining_mc - ? WHERE id = ?", args: [take, Number(lot.id)] });
|
|
5556
|
-
need -= take;
|
|
5557
|
-
}
|
|
5558
|
-
if (updates.length) await db.batch(updates, "write");
|
|
5705
|
+
return inserted.rowsAffected;
|
|
5559
5706
|
}
|
|
5560
5707
|
async function expireOldLots() {
|
|
5561
5708
|
const db = getDb();
|
|
5562
5709
|
const lots = await db.execute({
|
|
5563
|
-
sql: `SELECT id
|
|
5710
|
+
sql: `SELECT id
|
|
5711
|
+
FROM credit_lots
|
|
5712
|
+
WHERE remaining_mc > 0
|
|
5713
|
+
AND never_expires = 0
|
|
5714
|
+
AND expires_at <= datetime('now')
|
|
5715
|
+
LIMIT 500`
|
|
5564
5716
|
});
|
|
5565
5717
|
if (lots.rows.length === 0) return { expired_lots: 0, expired_mc: 0 };
|
|
5566
|
-
|
|
5567
|
-
const users = /* @__PURE__ */ new Set();
|
|
5718
|
+
const claims = [];
|
|
5568
5719
|
const stmts = [];
|
|
5569
5720
|
for (const lot of lots.rows) {
|
|
5570
|
-
const
|
|
5571
|
-
const
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5721
|
+
const lotId = Number(lot.id);
|
|
5722
|
+
const token = (0, import_node_crypto.randomUUID)();
|
|
5723
|
+
claims.push({ token, insertResultIndex: stmts.length });
|
|
5724
|
+
stmts.push({
|
|
5725
|
+
sql: `INSERT OR IGNORE INTO credit_lot_expirations (lot_id, user_id, amount_mc, claim_token)
|
|
5726
|
+
SELECT id, user_id, remaining_mc, ?
|
|
5727
|
+
FROM credit_lots
|
|
5728
|
+
WHERE id = ?
|
|
5729
|
+
AND remaining_mc > 0
|
|
5730
|
+
AND never_expires = 0
|
|
5731
|
+
AND expires_at <= datetime('now')`,
|
|
5732
|
+
args: [token, lotId]
|
|
5733
|
+
});
|
|
5734
|
+
stmts.push({
|
|
5735
|
+
sql: `UPDATE credit_lots
|
|
5736
|
+
SET remaining_mc = 0
|
|
5737
|
+
WHERE id = ?
|
|
5738
|
+
AND EXISTS (
|
|
5739
|
+
SELECT 1 FROM credit_lot_expirations
|
|
5740
|
+
WHERE lot_id = ? AND claim_token = ?
|
|
5741
|
+
)`,
|
|
5742
|
+
args: [lotId, lotId, token]
|
|
5743
|
+
});
|
|
5744
|
+
stmts.push({
|
|
5745
|
+
sql: `INSERT INTO ledger (user_id, amount_mc, operation, description)
|
|
5746
|
+
SELECT user_id, -amount_mc, 'credit_expired', 'credits expired'
|
|
5747
|
+
FROM credit_lot_expirations
|
|
5748
|
+
WHERE lot_id = ? AND claim_token = ?`,
|
|
5749
|
+
args: [lotId, token]
|
|
5750
|
+
});
|
|
5751
|
+
}
|
|
5752
|
+
const results = await db.batch(stmts, "write");
|
|
5753
|
+
const claimedTokens = claims.filter((claim) => results[claim.insertResultIndex]?.rowsAffected === 1).map((claim) => claim.token);
|
|
5754
|
+
if (claimedTokens.length === 0) return { expired_lots: 0, expired_mc: 0 };
|
|
5755
|
+
const placeholders = claimedTokens.map(() => "?").join(", ");
|
|
5756
|
+
const expired = await db.execute({
|
|
5757
|
+
sql: `SELECT user_id, amount_mc
|
|
5758
|
+
FROM credit_lot_expirations
|
|
5759
|
+
WHERE claim_token IN (${placeholders})`,
|
|
5760
|
+
args: claimedTokens
|
|
5761
|
+
});
|
|
5762
|
+
const users = new Set(expired.rows.map((row) => Number(row.user_id)));
|
|
5578
5763
|
for (const uid of users) await reconcileBalanceMc(uid);
|
|
5579
|
-
return {
|
|
5764
|
+
return {
|
|
5765
|
+
expired_lots: expired.rows.length,
|
|
5766
|
+
expired_mc: expired.rows.reduce((sum, row) => sum + Number(row.amount_mc), 0)
|
|
5767
|
+
};
|
|
5580
5768
|
}
|
|
5581
5769
|
async function migrateExistingBalancesToLots() {
|
|
5582
5770
|
const db = getDb();
|
|
5583
5771
|
const res = await db.execute({
|
|
5584
|
-
sql: `SELECT u.id AS user_id
|
|
5585
|
-
FROM users u
|
|
5586
|
-
|
|
5587
|
-
|
|
5772
|
+
sql: `SELECT DISTINCT u.id AS user_id
|
|
5773
|
+
FROM users u
|
|
5774
|
+
JOIN ledger l ON l.user_id = u.id
|
|
5775
|
+
WHERE l.created_at < ?
|
|
5776
|
+
AND NOT EXISTS (SELECT 1 FROM credit_lots WHERE user_id = u.id)`,
|
|
5777
|
+
args: [CREDIT_LOTS_EPOCH]
|
|
5588
5778
|
});
|
|
5589
5779
|
let migrated = 0;
|
|
5590
5780
|
let totalMc = 0;
|
|
5591
5781
|
for (const row of res.rows) {
|
|
5592
5782
|
const uid = Number(row.user_id);
|
|
5593
|
-
const
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5783
|
+
const inserted = await ensureMigrated(uid);
|
|
5784
|
+
if (inserted === 0) continue;
|
|
5785
|
+
const lot = await db.execute({
|
|
5786
|
+
sql: `SELECT amount_mc FROM credit_lots
|
|
5787
|
+
WHERE user_id = ? AND source = 'migration'
|
|
5788
|
+
ORDER BY id DESC LIMIT 1`,
|
|
5789
|
+
args: [uid]
|
|
5790
|
+
});
|
|
5791
|
+
const amountMc = Number(lot.rows[0]?.amount_mc ?? 0);
|
|
5598
5792
|
await reconcileBalanceMc(uid);
|
|
5599
|
-
migrated
|
|
5600
|
-
totalMc +=
|
|
5793
|
+
migrated += 1;
|
|
5794
|
+
totalMc += amountMc;
|
|
5601
5795
|
}
|
|
5602
5796
|
return { migrated, total_mc: totalMc };
|
|
5603
5797
|
}
|
|
@@ -5611,19 +5805,22 @@ async function creditMc(userId, mc, operation, description, stripePaymentIntent,
|
|
|
5611
5805
|
}
|
|
5612
5806
|
const expiry = neverExpires ? "9999-12-31 23:59:59" : null;
|
|
5613
5807
|
if (stripePaymentIntent) {
|
|
5614
|
-
const
|
|
5808
|
+
const stmts2 = [{
|
|
5615
5809
|
sql: "INSERT OR IGNORE INTO ledger (user_id, amount_mc, operation, description, stripe_pi) VALUES (?, ?, ?, ?, ?)",
|
|
5616
5810
|
args: [userId, mc, operation, description ?? null, stripePaymentIntent]
|
|
5617
|
-
}
|
|
5618
|
-
if (ledgerInsert.rowsAffected === 0) {
|
|
5619
|
-
const res3 = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
|
|
5620
|
-
return Number(res3.rows[0]?.balance_mc ?? 0);
|
|
5621
|
-
}
|
|
5622
|
-
const stmts2 = [];
|
|
5811
|
+
}];
|
|
5623
5812
|
if (mc > 0) {
|
|
5624
|
-
stmts2.push({
|
|
5813
|
+
stmts2.push({
|
|
5814
|
+
sql: `INSERT INTO credit_lots (user_id, amount_mc, remaining_mc, source, stripe_pi, expires_at, never_expires)
|
|
5815
|
+
SELECT ?, ?, ?, ?, ?, COALESCE(?, datetime('now', ?)), ?
|
|
5816
|
+
WHERE changes() = 1`,
|
|
5817
|
+
args: [userId, mc, mc, operation, stripePaymentIntent, expiry, CREDIT_LOT_TTL, neverExpires ? 1 : 0]
|
|
5818
|
+
});
|
|
5625
5819
|
}
|
|
5626
|
-
stmts2.push({
|
|
5820
|
+
stmts2.push({
|
|
5821
|
+
sql: "UPDATE users SET balance_mc = balance_mc + ? WHERE id = ? AND changes() = 1",
|
|
5822
|
+
args: [mc, userId]
|
|
5823
|
+
});
|
|
5627
5824
|
await db.batch(stmts2, "write");
|
|
5628
5825
|
const res2 = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
|
|
5629
5826
|
return Number(res2.rows[0]?.balance_mc ?? 0);
|
|
@@ -5731,23 +5928,14 @@ async function claimMonthlyFreeRefresh(userId, month) {
|
|
|
5731
5928
|
return res.rowsAffected > 0;
|
|
5732
5929
|
}
|
|
5733
5930
|
async function debitMc(userId, mc, operation, description) {
|
|
5734
|
-
const
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
return { ok: false, balance_mc: Number(res2.rows[0]?.balance_mc ?? 0) };
|
|
5743
|
-
}
|
|
5744
|
-
await drainLots(userId, mc);
|
|
5745
|
-
await db.execute({
|
|
5746
|
-
sql: "INSERT INTO ledger (user_id, amount_mc, operation, description) VALUES (?, ?, ?, ?)",
|
|
5747
|
-
args: [userId, -mc, operation, description ?? null]
|
|
5748
|
-
});
|
|
5749
|
-
const res = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
|
|
5750
|
-
return { ok: true, balance_mc: Number(res.rows[0]?.balance_mc ?? 0) };
|
|
5931
|
+
const debit = await debitMcIdempotent(
|
|
5932
|
+
userId,
|
|
5933
|
+
mc,
|
|
5934
|
+
operation,
|
|
5935
|
+
description ?? "",
|
|
5936
|
+
`debit:${(0, import_node_crypto.randomUUID)()}`
|
|
5937
|
+
);
|
|
5938
|
+
return { ok: debit.ok, balance_mc: debit.balance_mc };
|
|
5751
5939
|
}
|
|
5752
5940
|
async function debitMcIdempotent(userId, mc, operation, description, idempotencyKey2) {
|
|
5753
5941
|
if (!Number.isSafeInteger(mc) || mc <= 0) throw new Error("idempotent debit amount must be a positive integer");
|
|
@@ -6166,29 +6354,46 @@ async function ledgerExistsForStripePI(stripePI) {
|
|
|
6166
6354
|
}
|
|
6167
6355
|
async function reconcileBalanceMc(userId, knownBalanceMc) {
|
|
6168
6356
|
const db = getDb();
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6357
|
+
void knownBalanceMc;
|
|
6358
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
6359
|
+
const snapshot = await db.execute({
|
|
6360
|
+
sql: `SELECT
|
|
6361
|
+
u.balance_mc AS cached_balance_mc,
|
|
6362
|
+
COUNT(l.id) AS lot_count,
|
|
6363
|
+
COALESCE(SUM(CASE
|
|
6364
|
+
WHEN l.remaining_mc > 0
|
|
6365
|
+
AND (l.never_expires = 1 OR l.expires_at > datetime('now'))
|
|
6366
|
+
THEN l.remaining_mc ELSE 0 END), 0) AS projected_balance_mc
|
|
6367
|
+
FROM users u
|
|
6368
|
+
LEFT JOIN credit_lots l ON l.user_id = u.id
|
|
6369
|
+
WHERE u.id = ?
|
|
6370
|
+
GROUP BY u.id, u.balance_mc`,
|
|
6181
6371
|
args: [userId]
|
|
6182
6372
|
});
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6373
|
+
const row = snapshot.rows[0];
|
|
6374
|
+
if (!row) return 0;
|
|
6375
|
+
if (Number(row.lot_count ?? 0) === 0) {
|
|
6376
|
+
await ensureMigrated(Number(userId));
|
|
6377
|
+
const nowHasLots = await db.execute({
|
|
6378
|
+
sql: "SELECT 1 FROM credit_lots WHERE user_id = ? LIMIT 1",
|
|
6379
|
+
args: [userId]
|
|
6380
|
+
});
|
|
6381
|
+
if (nowHasLots.rows.length > 0) continue;
|
|
6382
|
+
}
|
|
6383
|
+
const cachedBalanceMc = Number(row.cached_balance_mc ?? 0);
|
|
6384
|
+
const projectedBalanceMc = Number(row.projected_balance_mc ?? 0);
|
|
6385
|
+
if (cachedBalanceMc === projectedBalanceMc) return projectedBalanceMc;
|
|
6386
|
+
const updated = await db.execute({
|
|
6387
|
+
sql: "UPDATE users SET balance_mc = ? WHERE id = ? AND balance_mc = ?",
|
|
6388
|
+
args: [projectedBalanceMc, userId, cachedBalanceMc]
|
|
6189
6389
|
});
|
|
6390
|
+
if (updated.rowsAffected === 1) return projectedBalanceMc;
|
|
6190
6391
|
}
|
|
6191
|
-
|
|
6392
|
+
const current = await db.execute({
|
|
6393
|
+
sql: "SELECT balance_mc FROM users WHERE id = ? LIMIT 1",
|
|
6394
|
+
args: [userId]
|
|
6395
|
+
});
|
|
6396
|
+
return Number(current.rows[0]?.balance_mc ?? 0);
|
|
6192
6397
|
}
|
|
6193
6398
|
async function listInboxMessages(userId) {
|
|
6194
6399
|
const res = await getDb().execute({
|
|
@@ -6251,18 +6456,20 @@ async function listActiveUserIds() {
|
|
|
6251
6456
|
const res = await getDb().execute("SELECT id FROM users WHERE active = 1");
|
|
6252
6457
|
return res.rows.map((r) => Number(r.id));
|
|
6253
6458
|
}
|
|
6254
|
-
var import_http, import_node_crypto, import_zod, DB_URL, DB_TOKEN, _db, _rateLimitSchemaReady, CORE_SCHEMA_VERSION, CORE_SCHEMA_TABLES, CORE_SCHEMA_INDEXES, CORE_SCHEMA_COLUMNS, REQUEST_EVENT_RESULT_MAX_BYTES, CREDIT_LOT_TTL, SiteAuditJobRowSchema, SiteAuditPhaseLogRowSchema;
|
|
6459
|
+
var import_http, import_node_crypto, import_zod, DB_URL, DB_TOKEN, _db, _rateLimitSchemaReady, CORE_SCHEMA_VERSION, CORE_SCHEMA_TABLES, CORE_SCHEMA_INDEXES, CORE_SCHEMA_TRIGGERS, CORE_SCHEMA_COLUMNS, REQUEST_EVENT_RESULT_MAX_BYTES, CREDIT_LOT_TTL, SiteAuditJobRowSchema, SiteAuditPhaseLogRowSchema;
|
|
6255
6460
|
var init_db = __esm({
|
|
6256
6461
|
"src/api/db.ts"() {
|
|
6257
6462
|
"use strict";
|
|
6258
6463
|
import_http = require("@libsql/client/http");
|
|
6259
6464
|
import_node_crypto = require("crypto");
|
|
6260
6465
|
import_zod = require("zod");
|
|
6466
|
+
init_credit_lot_integrity();
|
|
6467
|
+
init_credit_lot_integrity();
|
|
6261
6468
|
DB_URL = process.env.TURSO_DATABASE_URL ?? "file:./paa-api.db";
|
|
6262
6469
|
DB_TOKEN = process.env.TURSO_AUTH_TOKEN;
|
|
6263
6470
|
_db = null;
|
|
6264
6471
|
_rateLimitSchemaReady = false;
|
|
6265
|
-
CORE_SCHEMA_VERSION = "2026-07-28.
|
|
6472
|
+
CORE_SCHEMA_VERSION = "2026-07-28.3";
|
|
6266
6473
|
CORE_SCHEMA_TABLES = [
|
|
6267
6474
|
"admin_credit_adjustment_events",
|
|
6268
6475
|
"admin_credit_adjustments",
|
|
@@ -6274,7 +6481,9 @@ var init_db = __esm({
|
|
|
6274
6481
|
"browser_profile_labels",
|
|
6275
6482
|
"connected_account_billing",
|
|
6276
6483
|
"concurrency_locks",
|
|
6484
|
+
"credit_lot_expirations",
|
|
6277
6485
|
"credit_lots",
|
|
6486
|
+
"credit_lot_repairs",
|
|
6278
6487
|
"free_credit_refreshes",
|
|
6279
6488
|
"harvest_attempts",
|
|
6280
6489
|
"inbox_messages",
|
|
@@ -6311,6 +6520,7 @@ var init_db = __esm({
|
|
|
6311
6520
|
"concurrency_locks_operation",
|
|
6312
6521
|
"concurrency_locks_user_status",
|
|
6313
6522
|
"credit_lots_user_active",
|
|
6523
|
+
"credit_lot_repairs_user_applied",
|
|
6314
6524
|
"harvest_attempts_job_id",
|
|
6315
6525
|
"harvest_attempts_user_created_at",
|
|
6316
6526
|
"inbox_messages_user_sent_at",
|
|
@@ -6340,6 +6550,12 @@ var init_db = __esm({
|
|
|
6340
6550
|
"workflow_schedules_user_status",
|
|
6341
6551
|
"workflow_webhook_deliveries_run"
|
|
6342
6552
|
];
|
|
6553
|
+
CORE_SCHEMA_TRIGGERS = [
|
|
6554
|
+
"credit_lots_bounds_insert",
|
|
6555
|
+
"credit_lots_bounds_update",
|
|
6556
|
+
"credit_lots_one_migration_insert",
|
|
6557
|
+
"credit_lots_one_migration_update"
|
|
6558
|
+
];
|
|
6343
6559
|
CORE_SCHEMA_COLUMNS = {
|
|
6344
6560
|
credit_lots: ["never_expires"],
|
|
6345
6561
|
harvest_attempts: ["debug_json"],
|
|
@@ -6357,6 +6573,7 @@ var init_db = __esm({
|
|
|
6357
6573
|
"next_settlement_at",
|
|
6358
6574
|
"settlement_error"
|
|
6359
6575
|
],
|
|
6576
|
+
stripe_events: ["attempt_count", "claim_token", "last_error", "status", "updated_at"],
|
|
6360
6577
|
users: [
|
|
6361
6578
|
"balance_mc",
|
|
6362
6579
|
"concurrency_stripe_sub_id",
|
|
@@ -6448,7 +6665,7 @@ var init_cost_rates = __esm({
|
|
|
6448
6665
|
"use strict";
|
|
6449
6666
|
KERNEL_HEADLESS_USD_PER_SEC = 166667e-10;
|
|
6450
6667
|
KERNEL_HEADFUL_USD_PER_SEC = 1333336e-10;
|
|
6451
|
-
HEADLESS_OPS = /* @__PURE__ */ new Set(["serp", "
|
|
6668
|
+
HEADLESS_OPS = /* @__PURE__ */ new Set(["serp", "fb_search", "fb_ad", "instagram"]);
|
|
6452
6669
|
FAL_WIZPER_USD_PER_COMPUTE_SEC = 111e-5;
|
|
6453
6670
|
FAL_WIZPER_WALLCLOCK_TO_COMPUTE_SEC_FACTOR = 4;
|
|
6454
6671
|
DEEPINFRA_QWEN_USD_PER_1K_OUT_TOKENS = 4e-4;
|
|
@@ -8044,12 +8261,15 @@ async function spiderSite(opts) {
|
|
|
8044
8261
|
const discovered = { url, status, linkedFrom: from, via: "fetch" };
|
|
8045
8262
|
const children = [];
|
|
8046
8263
|
if (html) {
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
visited.
|
|
8051
|
-
|
|
8264
|
+
try {
|
|
8265
|
+
for (const link of extractLinks(html, url)) {
|
|
8266
|
+
if (baseDomain(link) !== startDomain) continue;
|
|
8267
|
+
if (!visited.has(link)) {
|
|
8268
|
+
visited.add(link);
|
|
8269
|
+
children.push({ url: link, from: url });
|
|
8270
|
+
}
|
|
8052
8271
|
}
|
|
8272
|
+
} catch {
|
|
8053
8273
|
}
|
|
8054
8274
|
}
|
|
8055
8275
|
return { discovered, children };
|
|
@@ -10029,67 +10249,78 @@ async function extractSite(opts) {
|
|
|
10029
10249
|
let truncated = false;
|
|
10030
10250
|
let spider;
|
|
10031
10251
|
let sitemapUrls = [];
|
|
10032
|
-
|
|
10033
|
-
|
|
10034
|
-
|
|
10035
|
-
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
const
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
if (seeded) {
|
|
10046
|
-
for (const u of opts.seedUrls) enqueue(u);
|
|
10047
|
-
} else {
|
|
10048
|
-
enqueue(opts.startUrl);
|
|
10049
|
-
sitemapUrls = await discoverSitemapUrls2(opts.startUrl, maxPages);
|
|
10050
|
-
for (const u of sitemapUrls) enqueue(u);
|
|
10051
|
-
}
|
|
10052
|
-
const waveCap = Math.max(browserConcurrency * urlsPerBrowser, 30);
|
|
10053
|
-
let done = 0;
|
|
10054
|
-
while (frontier.length > 0 && pages.length < maxPages) {
|
|
10055
|
-
const capacity = maxPages - pages.length;
|
|
10056
|
-
const wave = frontier.splice(0, Math.min(capacity, waveCap));
|
|
10057
|
-
const fetched = await crawlWithRotation(wave, {
|
|
10058
|
-
apiKey: opts.kernelApiKey,
|
|
10059
|
-
concurrency: browserConcurrency,
|
|
10060
|
-
urlsPerBrowser,
|
|
10061
|
-
onPage: (p) => {
|
|
10062
|
-
done++;
|
|
10063
|
-
opts.onProgress?.({ done, discovered: seen.size, frontier: frontier.length, lastOk: p.ok ? 1 : 0, lastFail: p.ok ? 0 : 1, phase: p.phase });
|
|
10252
|
+
let stoppedEarly = null;
|
|
10253
|
+
try {
|
|
10254
|
+
if (rotating) {
|
|
10255
|
+
const browserConcurrency = Math.max(1, Math.min(opts.parallelism ?? 1, 8));
|
|
10256
|
+
const urlsPerBrowser = opts.rotateProxyEvery ?? 10;
|
|
10257
|
+
const followLinks = !seeded;
|
|
10258
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10259
|
+
const frontier = [];
|
|
10260
|
+
const enqueue = (u) => {
|
|
10261
|
+
const n = normalizeUrl(u, opts.startUrl);
|
|
10262
|
+
if (n && !seen.has(n)) {
|
|
10263
|
+
seen.add(n);
|
|
10264
|
+
frontier.push(n);
|
|
10064
10265
|
}
|
|
10065
|
-
}
|
|
10066
|
-
|
|
10067
|
-
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10266
|
+
};
|
|
10267
|
+
if (seeded) {
|
|
10268
|
+
for (const u of opts.seedUrls) enqueue(u);
|
|
10269
|
+
} else {
|
|
10270
|
+
enqueue(opts.startUrl);
|
|
10271
|
+
sitemapUrls = await discoverSitemapUrls2(opts.startUrl, maxPages);
|
|
10272
|
+
for (const u of sitemapUrls) enqueue(u);
|
|
10273
|
+
}
|
|
10274
|
+
const waveCap = Math.max(browserConcurrency * urlsPerBrowser, 30);
|
|
10275
|
+
let done = 0;
|
|
10276
|
+
while (frontier.length > 0 && pages.length < maxPages) {
|
|
10277
|
+
const capacity = maxPages - pages.length;
|
|
10278
|
+
const wave = frontier.splice(0, Math.min(capacity, waveCap));
|
|
10279
|
+
const fetched = await crawlWithRotation(wave, {
|
|
10280
|
+
apiKey: opts.kernelApiKey,
|
|
10281
|
+
concurrency: browserConcurrency,
|
|
10282
|
+
urlsPerBrowser,
|
|
10283
|
+
onPage: (p) => {
|
|
10284
|
+
done++;
|
|
10285
|
+
opts.onProgress?.({ done, discovered: seen.size, frontier: frontier.length, lastOk: p.ok ? 1 : 0, lastFail: p.ok ? 0 : 1, phase: p.phase });
|
|
10286
|
+
}
|
|
10071
10287
|
});
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10288
|
+
for (const r of fetched) {
|
|
10289
|
+
const page = r.html && r.status >= 200 && r.status < 300 ? parsePageData(r.url, r.html, r.status, "browser", { headers: r.headers, responseTimeMs: r.responseTimeMs, redirectUrl: r.redirectUrl }, [successfulAttempt("browser", r.status, r.responseTimeMs ?? null)]) : emptyPageData(r.url, r.status || null, "browser", {
|
|
10290
|
+
code: r.errorCode ?? "browser_empty_response",
|
|
10291
|
+
message: r.error ?? "Browser fetch returned no usable response.",
|
|
10292
|
+
attempts: [failedAttempt("browser", r.status || null, r.responseTimeMs ?? null, r.errorCode ?? "browser_empty_response", r.error ?? "Browser fetch returned no usable response.")]
|
|
10293
|
+
});
|
|
10294
|
+
pages.push(page);
|
|
10295
|
+
browserRetries++;
|
|
10296
|
+
if (followLinks && isPageExtractionSuccessful(page)) {
|
|
10297
|
+
for (const href of page.discoveryLinks ?? page.outlinks.filter((link) => link.internal).map((link) => link.href)) enqueue(href);
|
|
10298
|
+
}
|
|
10076
10299
|
}
|
|
10077
10300
|
}
|
|
10301
|
+
truncated = frontier.length > 0;
|
|
10302
|
+
spider = makeSeedSpider(opts.startUrl, [...seen]);
|
|
10303
|
+
} else {
|
|
10304
|
+
spider = seeded ? makeSeedSpider(opts.startUrl, opts.seedUrls) : await spiderSite({ startUrl: opts.startUrl, maxUrls: maxPages, concurrency: 12, kernelApiKey: opts.kernelApiKey });
|
|
10305
|
+
sitemapUrls = spider.sitemapUrls;
|
|
10306
|
+
const urlsToExtract = spider.urls.filter((u) => u.status === 200 || u.status === null).slice(0, maxPages).map((u) => u.url);
|
|
10307
|
+
browserRetries = spider.browserRetries;
|
|
10308
|
+
await runWithConcurrency(urlsToExtract, concurrency, async (url) => {
|
|
10309
|
+
const data = await fetchAndParse(url, opts.kernelApiKey);
|
|
10310
|
+
if (data.via === "browser") browserRetries++;
|
|
10311
|
+
pages.push(data);
|
|
10312
|
+
});
|
|
10313
|
+
truncated = spider.truncated || pages.length >= maxPages;
|
|
10078
10314
|
}
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
await runWithConcurrency(urlsToExtract, concurrency, async (url) => {
|
|
10087
|
-
const data = await fetchAndParse(url, opts.kernelApiKey);
|
|
10088
|
-
if (data.via === "browser") browserRetries++;
|
|
10089
|
-
pages.push(data);
|
|
10090
|
-
});
|
|
10091
|
-
truncated = spider.truncated || pages.length >= maxPages;
|
|
10315
|
+
} catch (err) {
|
|
10316
|
+
if (pages.length === 0) throw err;
|
|
10317
|
+
stoppedEarly = {
|
|
10318
|
+
code: "crawl_stopped_early",
|
|
10319
|
+
message: err instanceof Error ? err.message : String(err)
|
|
10320
|
+
};
|
|
10321
|
+
truncated = true;
|
|
10092
10322
|
}
|
|
10323
|
+
if (!spider) spider = makeSeedSpider(opts.startUrl, pages.map((p) => p.url));
|
|
10093
10324
|
const sitemapSet = sitemapUrls.length > 0 ? new Set(sitemapUrls.map((u) => normalizeUrl(u, opts.startUrl) ?? u)) : null;
|
|
10094
10325
|
for (const p of pages) p.inSitemap = sitemapSet ? sitemapSet.has(normalizeUrl(p.url, opts.startUrl) ?? p.url) : null;
|
|
10095
10326
|
let branding = null;
|
|
@@ -10099,10 +10330,11 @@ async function extractSite(opts) {
|
|
|
10099
10330
|
const seoAudit = opts.formats?.includes("issues") ? buildSeoAudit(pages, opts.startUrl) : null;
|
|
10100
10331
|
let imageAudit = null;
|
|
10101
10332
|
if (opts.formats?.includes("images")) {
|
|
10102
|
-
imageAudit = await auditImages(pages, { concurrency: 12, timeoutMs: 12e3 });
|
|
10333
|
+
imageAudit = await auditImages(pages, { concurrency: 12, timeoutMs: 12e3 }).catch(() => null);
|
|
10103
10334
|
}
|
|
10104
10335
|
return {
|
|
10105
10336
|
startUrl: opts.startUrl,
|
|
10337
|
+
stoppedEarly,
|
|
10106
10338
|
pages,
|
|
10107
10339
|
totalPages: pages.length,
|
|
10108
10340
|
spider,
|
|
@@ -10906,11 +11138,14 @@ var init_rates = __esm({
|
|
|
10906
11138
|
url_map: 500,
|
|
10907
11139
|
yt_channel: 200,
|
|
10908
11140
|
yt_transcription: 200,
|
|
10909
|
-
fb_ad:
|
|
11141
|
+
fb_ad: 700,
|
|
10910
11142
|
maps_search: 500,
|
|
10911
11143
|
maps_place: 6e3,
|
|
10912
11144
|
maps_review: 100,
|
|
10913
|
-
fb_search:
|
|
11145
|
+
fb_search: 800,
|
|
11146
|
+
fb_search_result: 100,
|
|
11147
|
+
fb_page_intel: 700,
|
|
11148
|
+
fb_page_intel_ad: 50,
|
|
10914
11149
|
fb_transcribe: 33300,
|
|
10915
11150
|
google_ads_search: 600,
|
|
10916
11151
|
google_ads_intel: 200,
|
|
@@ -11289,6 +11524,12 @@ var init_rates = __esm({
|
|
|
11289
11524
|
FB_SEARCH: "fb_search",
|
|
11290
11525
|
FB_TRANSCRIBE: "fb_transcribe",
|
|
11291
11526
|
FB_SEARCH_REFUND: "fb_search_refund",
|
|
11527
|
+
FB_SEARCH_RESULT: "fb_search_result",
|
|
11528
|
+
FB_SEARCH_RESULT_REFUND: "fb_search_result_refund",
|
|
11529
|
+
FB_PAGE_INTEL: "fb_page_intel",
|
|
11530
|
+
FB_PAGE_INTEL_REFUND: "fb_page_intel_refund",
|
|
11531
|
+
FB_PAGE_INTEL_AD: "fb_page_intel_ad",
|
|
11532
|
+
FB_PAGE_INTEL_AD_REFUND: "fb_page_intel_ad_refund",
|
|
11292
11533
|
FB_TRANSCRIBE_REFUND: "fb_transcribe_refund",
|
|
11293
11534
|
GOOGLE_ADS_SEARCH: "google_ads_search",
|
|
11294
11535
|
GOOGLE_ADS_SEARCH_REFUND: "google_ads_search_refund",
|
|
@@ -16240,8 +16481,12 @@ async function formatExtractSite(raw, input, ctx) {
|
|
|
16240
16481
|
schemaTypes: schemaTypesOf(p)
|
|
16241
16482
|
})));
|
|
16242
16483
|
const preview = pages.slice(0, BULK_PAGE_THRESHOLD).map(pageRow).join("\n");
|
|
16484
|
+
const stoppedEarly = d.stoppedEarly;
|
|
16243
16485
|
const summaryHeader = [
|
|
16244
16486
|
`# Site Extract: ${input.url}`,
|
|
16487
|
+
stoppedEarly ? `
|
|
16488
|
+
> **Partial crawl.** The crawl stopped before finishing, so this covers ${pages.length} page(s) rather than the whole site. You were only charged for the pages returned. Reason: ${stoppedEarly.message ?? "unknown"}
|
|
16489
|
+
` : "",
|
|
16245
16490
|
durationLine,
|
|
16246
16491
|
`
|
|
16247
16492
|
## Pages (first ${Math.min(BULK_PAGE_THRESHOLD, pages.length)} of ${pages.length})
|
|
@@ -18783,8 +19028,10 @@ var init_BrowserDriver = __esm({
|
|
|
18783
19028
|
return this.kernelClient ? "Google returned a CAPTCHA on this session \u2014 retrying with a fresh session." : RECAPTCHA_INSTRUCTIONS;
|
|
18784
19029
|
}
|
|
18785
19030
|
async navigateTo(url) {
|
|
19031
|
+
const arrivalBase = process.env.ADS_ARRIVAL_REDIRECT_BASE?.trim();
|
|
19032
|
+
const navUrl = arrivalBase ? serpArrivalUrl(url, arrivalBase) : url;
|
|
18786
19033
|
try {
|
|
18787
|
-
await this.page.goto(
|
|
19034
|
+
await this.page.goto(navUrl, { waitUntil: "domcontentloaded", timeout: 45e3 });
|
|
18788
19035
|
if (/google\.[^/]+\/search\?/i.test(url) && await this.isSerpChallengeVisible()) {
|
|
18789
19036
|
throw new CaptchaError(this.captchaMessage());
|
|
18790
19037
|
}
|
|
@@ -19000,6 +19247,30 @@ function closeOpenBusinessPanelDialogInBrowser(expectedHeading) {
|
|
|
19000
19247
|
close?.click();
|
|
19001
19248
|
return Boolean(close);
|
|
19002
19249
|
}
|
|
19250
|
+
function readExpandedPanelRowInBrowser(input) {
|
|
19251
|
+
const normalize4 = (value) => (value ?? "").replace(/\s+/g, " ").trim();
|
|
19252
|
+
const expectedBusiness = normalize4(input.businessName).toLowerCase();
|
|
19253
|
+
const visible = (element) => element.getAttribute("aria-hidden") !== "true" && (element.offsetParent !== null || element.getClientRects().length > 0);
|
|
19254
|
+
const readText = (element) => normalize4(element.innerText || element.textContent);
|
|
19255
|
+
const dialogs = Array.from(document.querySelectorAll('dialog, [role="dialog"]')).filter((dialog) => visible(dialog) && readText(dialog).toLowerCase().includes(expectedBusiness)).reverse();
|
|
19256
|
+
for (const dialog of dialogs) {
|
|
19257
|
+
const row = Array.from(
|
|
19258
|
+
dialog.querySelectorAll('button, a, [role="button"]')
|
|
19259
|
+
).find((element) => visible(element) && readText(element).startsWith(input.prefix));
|
|
19260
|
+
if (!row) continue;
|
|
19261
|
+
const body = readText(row).slice(input.prefix.length);
|
|
19262
|
+
const seen = /* @__PURE__ */ new Set();
|
|
19263
|
+
const items = [];
|
|
19264
|
+
for (const part of body.split(/\s*[,\u00b7]\s*/)) {
|
|
19265
|
+
const value = part.trim();
|
|
19266
|
+
if (!value || seen.has(value)) continue;
|
|
19267
|
+
seen.add(value);
|
|
19268
|
+
items.push(value);
|
|
19269
|
+
}
|
|
19270
|
+
if (items.length > 0) return items;
|
|
19271
|
+
}
|
|
19272
|
+
return [];
|
|
19273
|
+
}
|
|
19003
19274
|
async function clickPanelRowWithRetry(page, businessName, prefix) {
|
|
19004
19275
|
const input = { businessName, prefix };
|
|
19005
19276
|
if (await page.evaluate(clickBusinessPanelRowInBrowser, input)) return true;
|
|
@@ -19013,9 +19284,12 @@ async function extractExpandedList(page, businessName, rowPrefix, dialogHeading)
|
|
|
19013
19284
|
extractOpenBusinessPanelDialogItemsInBrowser,
|
|
19014
19285
|
dialogHeading
|
|
19015
19286
|
);
|
|
19016
|
-
|
|
19017
|
-
|
|
19018
|
-
|
|
19287
|
+
if (items.length > 0) {
|
|
19288
|
+
await page.evaluate(closeOpenBusinessPanelDialogInBrowser, dialogHeading);
|
|
19289
|
+
await page.waitForTimeout(250);
|
|
19290
|
+
return items;
|
|
19291
|
+
}
|
|
19292
|
+
return page.evaluate(readExpandedPanelRowInBrowser, { businessName, prefix: rowPrefix });
|
|
19019
19293
|
}
|
|
19020
19294
|
async function extractExpandedGoogleBusinessPanelServices(page, businessName) {
|
|
19021
19295
|
const services = await extractExpandedList(page, businessName, "Services:", "Services");
|
|
@@ -19092,7 +19366,7 @@ var init_MapsSearchExtractor = __esm({
|
|
|
19092
19366
|
await page.waitForTimeout(LOCAL_RESULTS_WAIT_MS);
|
|
19093
19367
|
if (await this.detectBlock(page)) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
|
|
19094
19368
|
const reachedFinder = await this.clickIntoLocalFinder(page);
|
|
19095
|
-
if (!reachedFinder) throw new
|
|
19369
|
+
if (!reachedFinder) throw new MapsSearchIncompletePageError('Google "More businesses" control was not found on the organic results page');
|
|
19096
19370
|
await page.waitForTimeout(LOCAL_RESULTS_WAIT_MS);
|
|
19097
19371
|
if (await this.detectBlock(page)) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
|
|
19098
19372
|
const results = await this.collectResults(page, options);
|
|
@@ -20078,7 +20352,8 @@ function shouldRetryMapsSearchError(input) {
|
|
|
20078
20352
|
if (input.attemptIndex >= input.maxAttempts - 1) return false;
|
|
20079
20353
|
if (!retryableMapsSearchError(input.err, input.proxyMode)) return false;
|
|
20080
20354
|
if (input.err instanceof MapsSearchIncompletePageError) {
|
|
20081
|
-
|
|
20355
|
+
const unconfirmed = input.priorAttempts.filter((attempt) => attempt.outcome === "results_unconfirmed").length;
|
|
20356
|
+
return unconfirmed < UNCONFIRMED_PAGE_MAX_RETRIES;
|
|
20082
20357
|
}
|
|
20083
20358
|
return true;
|
|
20084
20359
|
}
|
|
@@ -20259,7 +20534,7 @@ async function runMapsSearchWithRotation(options) {
|
|
|
20259
20534
|
true
|
|
20260
20535
|
);
|
|
20261
20536
|
}
|
|
20262
|
-
var MAPS_SEARCH_MAX_ATTEMPTS, MAPS_SEARCH_NONE_MAX_ATTEMPTS, MAPS_SEARCH_LOCATION_PROXY_MAX_ATTEMPTS, MapsSearchRotationError;
|
|
20537
|
+
var UNCONFIRMED_PAGE_MAX_RETRIES, MAPS_SEARCH_MAX_ATTEMPTS, MAPS_SEARCH_NONE_MAX_ATTEMPTS, MAPS_SEARCH_LOCATION_PROXY_MAX_ATTEMPTS, MapsSearchRotationError;
|
|
20263
20538
|
var init_maps_search_rotation = __esm({
|
|
20264
20539
|
"src/maps-search-rotation.ts"() {
|
|
20265
20540
|
"use strict";
|
|
@@ -20269,6 +20544,7 @@ var init_maps_search_rotation = __esm({
|
|
|
20269
20544
|
init_kernel_proxy_resolver();
|
|
20270
20545
|
init_backup_proxy();
|
|
20271
20546
|
init_cost_context();
|
|
20547
|
+
UNCONFIRMED_PAGE_MAX_RETRIES = 5;
|
|
20272
20548
|
MAPS_SEARCH_MAX_ATTEMPTS = 3;
|
|
20273
20549
|
MAPS_SEARCH_NONE_MAX_ATTEMPTS = 7;
|
|
20274
20550
|
MAPS_SEARCH_LOCATION_PROXY_MAX_ATTEMPTS = 5;
|
|
@@ -25801,6 +26077,7 @@ async function resolveYouTubeAudio(videoId) {
|
|
|
25801
26077
|
if (!base || !secret2) return null;
|
|
25802
26078
|
const controller = new AbortController();
|
|
25803
26079
|
const timer = setTimeout(() => controller.abort(), RESOLVE_TIMEOUT_MS);
|
|
26080
|
+
const startedMs = Date.now();
|
|
25804
26081
|
try {
|
|
25805
26082
|
const resp = await fetch(`${base.replace(/\/+$/, "")}/resolve-audio`, {
|
|
25806
26083
|
method: "POST",
|
|
@@ -25818,6 +26095,12 @@ async function resolveYouTubeAudio(videoId) {
|
|
|
25818
26095
|
console.warn(`[audio-resolver] ${videoId}: response missing mediaUrl`);
|
|
25819
26096
|
return null;
|
|
25820
26097
|
}
|
|
26098
|
+
void recordVendorUsage({
|
|
26099
|
+
vendor: "frames_audio_resolve",
|
|
26100
|
+
model: null,
|
|
26101
|
+
units: (Date.now() - startedMs) / 1e3,
|
|
26102
|
+
unitType: "compute_sec"
|
|
26103
|
+
});
|
|
25821
26104
|
return {
|
|
25822
26105
|
mediaUrl: data.mediaUrl,
|
|
25823
26106
|
durationS: Number(data.durationS) || 0,
|
|
@@ -25836,6 +26119,7 @@ var RESOLVE_TIMEOUT_MS;
|
|
|
25836
26119
|
var init_audio_resolver = __esm({
|
|
25837
26120
|
"src/youtube/audio-resolver.ts"() {
|
|
25838
26121
|
"use strict";
|
|
26122
|
+
init_cost_telemetry();
|
|
25839
26123
|
RESOLVE_TIMEOUT_MS = 24e4;
|
|
25840
26124
|
}
|
|
25841
26125
|
});
|
|
@@ -27408,10 +27692,20 @@ function isAllowedFacebookOrganicUrl(url) {
|
|
|
27408
27692
|
async function transcribeFacebookVideoUrl(videoUrl, markdownTitle = "# Facebook Video Transcript") {
|
|
27409
27693
|
return transcribeMediaUrl(videoUrl, markdownTitle);
|
|
27410
27694
|
}
|
|
27695
|
+
function classifyAdLibraryPage(bodyText, finalUrl) {
|
|
27696
|
+
if (AUTH_WALL_URL.test(finalUrl)) return "blocked";
|
|
27697
|
+
if (bodyText.length < MIN_RENDERED_BODY_CHARS) return "blocked";
|
|
27698
|
+
return AD_LIBRARY_APP_CHROME.test(bodyText) ? "reached_app" : "blocked";
|
|
27699
|
+
}
|
|
27700
|
+
async function refundEmptyPageIntel(c, userId, subject, blocked) {
|
|
27701
|
+
await creditMc(userId, MC_COSTS.fb_page_intel, LedgerOperation.FB_PAGE_INTEL_REFUND, blocked ? "soft-block empty result" : "empty result");
|
|
27702
|
+
await logRequestEvent({ userId, source: "facebook_page_intel", status: "failed", query: subject, error: blocked ? "soft-block: empty result refunded" : "empty result refunded" });
|
|
27703
|
+
return blocked ? c.json({ error: "soft-block: no ads returned (refunded)" }, 503) : c.json({ error: `No ads in Meta's Ad Library match "${subject}" (refunded). This is a real empty result, not a block.` }, 404);
|
|
27704
|
+
}
|
|
27411
27705
|
async function detectSoftBlock(driver) {
|
|
27412
27706
|
const page = driver.getPage();
|
|
27413
27707
|
const bodyText = await page.evaluate(() => document.body?.innerText ?? "").catch(() => "");
|
|
27414
|
-
return bodyText.
|
|
27708
|
+
return classifyAdLibraryPage(bodyText, page.url()) === "blocked";
|
|
27415
27709
|
}
|
|
27416
27710
|
function buildPageIntelUrl(body, country) {
|
|
27417
27711
|
if (body.libraryId?.trim()) return `https://www.facebook.com/ads/library/?id=${FacebookAdExtractor.resolveLibraryId(body.libraryId.trim()) ?? body.libraryId.trim()}`;
|
|
@@ -27487,7 +27781,7 @@ async function kernelLaunchOptsResidential() {
|
|
|
27487
27781
|
}
|
|
27488
27782
|
return { headless: true, kernelApiKey: browserServiceApiKey(), kernelProxyId: proxyId, viewport: { width: 1280, height: 900 }, locale: "en-US" };
|
|
27489
27783
|
}
|
|
27490
|
-
var import_hono7, import_zod21, import_client5, FacebookAdBodySchema, FacebookPageIntelBodySchema, FacebookTranscribeBodySchema, FacebookVideoTranscribeBodySchema, FacebookSearchBodySchema, FacebookMediaBodySchema, facebookAdApp, ALLOWED_MEDIA_HOSTS;
|
|
27784
|
+
var import_hono7, import_zod21, import_client5, FacebookAdBodySchema, FacebookPageIntelBodySchema, FacebookTranscribeBodySchema, FacebookVideoTranscribeBodySchema, FacebookSearchBodySchema, FacebookMediaBodySchema, AD_LIBRARY_APP_CHROME, AUTH_WALL_URL, MIN_RENDERED_BODY_CHARS, facebookAdApp, ALLOWED_MEDIA_HOSTS;
|
|
27491
27785
|
var init_facebook_ad_routes = __esm({
|
|
27492
27786
|
"src/api/facebook-ad-routes.ts"() {
|
|
27493
27787
|
"use strict";
|
|
@@ -27537,6 +27831,9 @@ var init_facebook_ad_routes = __esm({
|
|
|
27537
27831
|
url: import_zod21.z.string().trim().min(1, "url is required"),
|
|
27538
27832
|
filename: import_zod21.z.string().trim().optional()
|
|
27539
27833
|
});
|
|
27834
|
+
AD_LIBRARY_APP_CHROME = /Ad Library|Search by keyword|Branded Content/i;
|
|
27835
|
+
AUTH_WALL_URL = /facebook\.com\/(login|checkpoint|authentication)/i;
|
|
27836
|
+
MIN_RENDERED_BODY_CHARS = 200;
|
|
27540
27837
|
facebookAdApp = new import_hono7.Hono();
|
|
27541
27838
|
facebookAdApp.post("/ad", createApiKeyAuth(), async (c) => {
|
|
27542
27839
|
const raw = await c.req.json().catch(() => ({}));
|
|
@@ -27597,26 +27894,27 @@ var init_facebook_ad_routes = __esm({
|
|
|
27597
27894
|
const driver = new BrowserDriver();
|
|
27598
27895
|
let refunded = false, debited = false;
|
|
27599
27896
|
try {
|
|
27600
|
-
const { ok: fbOk, balance_mc: fbBal } = await debitMc(fbUser.id, MC_COSTS.
|
|
27601
|
-
if (!fbOk) return c.json(insufficientBalanceResponse(fbBal, MC_COSTS.
|
|
27897
|
+
const { ok: fbOk, balance_mc: fbBal } = await debitMc(fbUser.id, MC_COSTS.fb_page_intel, LedgerOperation.FB_PAGE_INTEL, subject);
|
|
27898
|
+
if (!fbOk) return c.json(insufficientBalanceResponse(fbBal, MC_COSTS.fb_page_intel), 402);
|
|
27602
27899
|
debited = true;
|
|
27603
27900
|
await driver.launch(await kernelLaunchOptsResidential());
|
|
27604
27901
|
await driver.navigateTo(listingUrl);
|
|
27605
27902
|
const extractor = new FacebookAdExtractor(driver);
|
|
27606
27903
|
const result = await extractor.extractPageIntel(listingUrl, maxAds);
|
|
27607
|
-
if (result.ads.length === 0
|
|
27608
|
-
await creditMc(fbUser.id, MC_COSTS.fb_ad, LedgerOperation.FB_AD_REFUND, "soft-block empty result");
|
|
27904
|
+
if (result.ads.length === 0) {
|
|
27609
27905
|
refunded = true;
|
|
27610
|
-
await
|
|
27611
|
-
return c.json({ error: "soft-block: no ads returned (refunded)" }, 503);
|
|
27906
|
+
return await refundEmptyPageIntel(c, fbUser.id, subject, await detectSoftBlock(driver));
|
|
27612
27907
|
}
|
|
27908
|
+
const perAdMc = result.ads.length * MC_COSTS.fb_page_intel_ad;
|
|
27909
|
+
const adDebit = await debitMc(fbUser.id, perAdMc, LedgerOperation.FB_PAGE_INTEL_AD, subject);
|
|
27910
|
+
if (!adDebit.ok) return c.json(insufficientBalanceResponse(adDebit.balance_mc, perAdMc), 402);
|
|
27613
27911
|
const match = assessPageIntelMatch(body, result);
|
|
27614
27912
|
const output = { ...result, ...match };
|
|
27615
27913
|
await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "done", query: subject, resultCount: result.ads.length, result: output });
|
|
27616
27914
|
return c.json(output);
|
|
27617
27915
|
} catch (err) {
|
|
27618
27916
|
const msg = err instanceof Error ? err.message : String(err);
|
|
27619
|
-
if (debited && !refunded) await creditMc(fbUser.id, MC_COSTS.
|
|
27917
|
+
if (debited && !refunded) await creditMc(fbUser.id, MC_COSTS.fb_page_intel, LedgerOperation.FB_PAGE_INTEL_REFUND, "failed call");
|
|
27620
27918
|
await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "failed", query: subject, error: msg });
|
|
27621
27919
|
if (msg.toLowerCase().includes("blocked") || msg.toLowerCase().includes("captcha")) {
|
|
27622
27920
|
return c.json({ error: msg }, 503);
|
|
@@ -27757,10 +28055,16 @@ var init_facebook_ad_routes = __esm({
|
|
|
27757
28055
|
debited = true;
|
|
27758
28056
|
await driver.launch(await kernelLaunchOptsResidential());
|
|
27759
28057
|
const page = driver.getPage();
|
|
27760
|
-
|
|
28058
|
+
let collated = await collectAdLibraryResults(page, searchUrl, Math.max(maxResults * 4, 40));
|
|
28059
|
+
if (collated.length === 0) {
|
|
28060
|
+
collated = await collectAdLibraryResults(page, searchUrl, Math.max(maxResults * 4, 40));
|
|
28061
|
+
}
|
|
27761
28062
|
const gqlAdvertisers = advertisersFromResults(collated, maxResults);
|
|
27762
28063
|
if (gqlAdvertisers.length > 0) {
|
|
27763
28064
|
const results2 = gqlAdvertisers.map((a) => ({ name: a.pageName, pageName: a.pageName, pageId: a.pageId, libraryId: a.sampleLibraryId, sampleLibraryId: a.sampleLibraryId, adCount: a.adCount }));
|
|
28065
|
+
const gqlPerResultMc = results2.length * MC_COSTS.fb_search_result;
|
|
28066
|
+
const gqlResultDebit = await debitMc(fbUser.id, gqlPerResultMc, LedgerOperation.FB_SEARCH_RESULT, body.query.trim());
|
|
28067
|
+
if (!gqlResultDebit.ok) return c.json(insufficientBalanceResponse(gqlResultDebit.balance_mc, gqlPerResultMc), 402);
|
|
27764
28068
|
const searchResult2 = { query: body.query.trim(), searchUrl, results: results2, via: "graphql" };
|
|
27765
28069
|
await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "done", query: body.query.trim(), resultCount: results2.length, result: searchResult2 });
|
|
27766
28070
|
return c.json(searchResult2);
|
|
@@ -27773,7 +28077,7 @@ var init_facebook_ad_routes = __esm({
|
|
|
27773
28077
|
await page.waitForTimeout(1e3);
|
|
27774
28078
|
}
|
|
27775
28079
|
const rawBodyText = await page.evaluate(() => document.body?.innerText ?? "");
|
|
27776
|
-
if (rawBodyText.
|
|
28080
|
+
if (classifyAdLibraryPage(rawBodyText, page.url()) === "blocked") {
|
|
27777
28081
|
await creditMc(fbUser.id, MC_COSTS.fb_search, LedgerOperation.FB_SEARCH_REFUND, "soft-block empty result");
|
|
27778
28082
|
searchRefunded = true;
|
|
27779
28083
|
await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "failed", query: body.query.trim(), error: "soft-block: empty result refunded" });
|
|
@@ -27814,8 +28118,11 @@ var init_facebook_ad_routes = __esm({
|
|
|
27814
28118
|
await creditMc(fbUser.id, MC_COSTS.fb_search, LedgerOperation.FB_SEARCH_REFUND, "empty result");
|
|
27815
28119
|
searchRefunded = true;
|
|
27816
28120
|
await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "failed", query: body.query.trim(), error: "empty result refunded" });
|
|
27817
|
-
return c.json({ error:
|
|
28121
|
+
return c.json({ error: `No advertisers in Meta's Ad Library match "${body.query.trim()}" (refunded). This is a real empty result, not a block \u2014 try a shorter or broader query, or a brand/page name.` }, 404);
|
|
27818
28122
|
}
|
|
28123
|
+
const perResultMc = results.length * MC_COSTS.fb_search_result;
|
|
28124
|
+
const resultDebit = await debitMc(fbUser.id, perResultMc, LedgerOperation.FB_SEARCH_RESULT, body.query.trim());
|
|
28125
|
+
if (!resultDebit.ok) return c.json(insufficientBalanceResponse(resultDebit.balance_mc, perResultMc), 402);
|
|
27819
28126
|
const searchResult = { query: body.query.trim(), searchUrl, results };
|
|
27820
28127
|
await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "done", query: body.query.trim(), resultCount: results.length, result: searchResult });
|
|
27821
28128
|
return c.json(searchResult);
|
|
@@ -27881,9 +28188,6 @@ var init_facebook_ad_routes = __esm({
|
|
|
27881
28188
|
});
|
|
27882
28189
|
|
|
27883
28190
|
// src/extractor/GoogleAdsExtractor.ts
|
|
27884
|
-
function looksLikeDomain(q) {
|
|
27885
|
-
return /^[a-z0-9.-]+\.[a-z]{2,}$/i.test(q.trim()) && !q.includes(" ");
|
|
27886
|
-
}
|
|
27887
28191
|
var ORIGIN, GoogleAdsExtractor;
|
|
27888
28192
|
var init_GoogleAdsExtractor = __esm({
|
|
27889
28193
|
"src/extractor/GoogleAdsExtractor.ts"() {
|
|
@@ -27899,50 +28203,108 @@ var init_GoogleAdsExtractor = __esm({
|
|
|
27899
28203
|
}
|
|
27900
28204
|
async search(query, region, maxResults) {
|
|
27901
28205
|
const q = query.trim();
|
|
27902
|
-
|
|
27903
|
-
await this.driver.navigateTo(url);
|
|
28206
|
+
await this.driver.navigateTo(`${ORIGIN}/?region=${encodeURIComponent(region)}`);
|
|
27904
28207
|
const page = this.page();
|
|
27905
|
-
await page.waitForTimeout(
|
|
27906
|
-
await
|
|
27907
|
-
|
|
27908
|
-
|
|
27909
|
-
|
|
27910
|
-
|
|
27911
|
-
|
|
27912
|
-
|
|
27913
|
-
|
|
27914
|
-
const
|
|
27915
|
-
const
|
|
27916
|
-
|
|
27917
|
-
|
|
27918
|
-
|
|
27919
|
-
|
|
27920
|
-
|
|
27921
|
-
|
|
27922
|
-
|
|
27923
|
-
|
|
27924
|
-
|
|
27925
|
-
|
|
27926
|
-
|
|
28208
|
+
await page.waitForTimeout(2500);
|
|
28209
|
+
const box = await page.waitForSelector('input[type="text"], input[aria-label*="Search" i], input', { timeout: 2e4 });
|
|
28210
|
+
await box.click();
|
|
28211
|
+
await box.fill("");
|
|
28212
|
+
await box.type(q, { delay: 110 });
|
|
28213
|
+
await page.waitForSelector('[role="option"]', { timeout: 2e4 }).catch(() => null);
|
|
28214
|
+
await page.waitForTimeout(2200);
|
|
28215
|
+
const raw = await page.evaluate(() => {
|
|
28216
|
+
const rows = Array.from(document.querySelectorAll('[role="option"]')).map((el2) => {
|
|
28217
|
+
const parts = Array.from(el2.querySelectorAll("*")).filter((n) => n.children.length === 0).map((n) => (n.textContent ?? "").replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
28218
|
+
const flat = el2.innerText.replace(/\s+/g, " ").trim();
|
|
28219
|
+
return { parts: parts.length ? parts : [flat], flat };
|
|
28220
|
+
}).filter((r) => r.flat);
|
|
28221
|
+
return { rows };
|
|
28222
|
+
});
|
|
28223
|
+
const domainRe = /^[a-z0-9][a-z0-9-]*(\.[a-z0-9-]+)+$/i;
|
|
28224
|
+
const websiteRows = raw.rows.filter((r) => domainRe.test(r.flat));
|
|
28225
|
+
const advertiserRows = raw.rows.filter((r) => !domainRe.test(r.flat));
|
|
28226
|
+
const advertisers = advertiserRows.slice(0, maxResults).map((row) => {
|
|
28227
|
+
const segments = row.parts.filter((p) => !/^Verified$/i.test(p));
|
|
28228
|
+
const adSeg = segments.find((p) => /~\s*[\d,]+\s*ads?/i.test(p));
|
|
28229
|
+
const adCount = adSeg?.match(/~\s*([\d,]+)\s*ads?/i);
|
|
28230
|
+
const rest = segments.filter((p) => p !== adSeg);
|
|
28231
|
+
return {
|
|
28232
|
+
advertiserId: null,
|
|
28233
|
+
name: rest[0] ?? null,
|
|
28234
|
+
basedIn: rest.length > 1 ? rest[rest.length - 1] : null,
|
|
28235
|
+
domain: null,
|
|
28236
|
+
approxAdCount: adCount ? Number(adCount[1].replace(/,/g, "")) : null,
|
|
28237
|
+
detailUrl: null
|
|
28238
|
+
};
|
|
28239
|
+
});
|
|
28240
|
+
const websites = websiteRows.slice(0, maxResults).map((row) => ({
|
|
28241
|
+
domain: row.flat,
|
|
28242
|
+
detailUrl: `${ORIGIN}/?region=${encodeURIComponent(region)}&domain=${encodeURIComponent(row.flat)}`
|
|
28243
|
+
}));
|
|
27927
28244
|
return {
|
|
27928
28245
|
query: q,
|
|
27929
28246
|
region,
|
|
27930
28247
|
advertiserCount: advertisers.length,
|
|
27931
|
-
advertisers
|
|
27932
|
-
|
|
27933
|
-
|
|
27934
|
-
domain: looksLikeDomain(q) ? q : null,
|
|
27935
|
-
approxAdCount: a.approxAdCount,
|
|
27936
|
-
detailUrl: `${ORIGIN}/advertiser/${a.advertiserId}?region=${region}`
|
|
27937
|
-
}))
|
|
28248
|
+
advertisers,
|
|
28249
|
+
websiteCount: websites.length,
|
|
28250
|
+
websites
|
|
27938
28251
|
};
|
|
27939
28252
|
}
|
|
28253
|
+
async advertiserIdOnPage() {
|
|
28254
|
+
const page = this.page();
|
|
28255
|
+
const fromUrl = page.url().match(/\/advertiser\/(AR[0-9]+)/);
|
|
28256
|
+
if (fromUrl) return fromUrl[1];
|
|
28257
|
+
return await page.evaluate(() => {
|
|
28258
|
+
for (const a of Array.from(document.querySelectorAll('a[href*="/advertiser/"]'))) {
|
|
28259
|
+
const m = a.getAttribute("href")?.match(/\/advertiser\/(AR[0-9]+)/);
|
|
28260
|
+
if (m) return m[1];
|
|
28261
|
+
}
|
|
28262
|
+
return null;
|
|
28263
|
+
});
|
|
28264
|
+
}
|
|
28265
|
+
async resolveAdvertiserIdByDomain(domain, region) {
|
|
28266
|
+
await this.driver.navigateTo(`${ORIGIN}/?region=${encodeURIComponent(region)}&domain=${encodeURIComponent(domain.trim())}`);
|
|
28267
|
+
await this.page().waitForTimeout(3500);
|
|
28268
|
+
return await this.advertiserIdOnPage();
|
|
28269
|
+
}
|
|
28270
|
+
async resolveAdvertiserIdByTypeahead(term, region) {
|
|
28271
|
+
const page = this.page();
|
|
28272
|
+
await this.driver.navigateTo(`${ORIGIN}/?region=${encodeURIComponent(region)}`);
|
|
28273
|
+
await page.waitForTimeout(2500);
|
|
28274
|
+
const box = await page.waitForSelector('input[type="text"], input[aria-label*="Search" i], input', { timeout: 2e4 });
|
|
28275
|
+
await box.click();
|
|
28276
|
+
await box.fill("");
|
|
28277
|
+
await box.type(term.trim(), { delay: 110 });
|
|
28278
|
+
await page.waitForSelector('[role="option"]', { timeout: 2e4 }).catch(() => null);
|
|
28279
|
+
await page.waitForTimeout(2e3);
|
|
28280
|
+
const optionCount = (await page.$$('[role="option"]')).length;
|
|
28281
|
+
for (let i = 0; i < Math.min(optionCount, 3); i++) {
|
|
28282
|
+
const options = await page.$$('[role="option"]');
|
|
28283
|
+
if (!options[i]) break;
|
|
28284
|
+
await options[i].click().catch(() => null);
|
|
28285
|
+
await page.waitForTimeout(3e3);
|
|
28286
|
+
const found = await this.advertiserIdOnPage();
|
|
28287
|
+
if (found) return found;
|
|
28288
|
+
await this.driver.navigateTo(`${ORIGIN}/?region=${encodeURIComponent(region)}`);
|
|
28289
|
+
await page.waitForTimeout(1500);
|
|
28290
|
+
const retryBox = await page.waitForSelector('input[type="text"], input', { timeout: 15e3 }).catch(() => null);
|
|
28291
|
+
if (!retryBox) break;
|
|
28292
|
+
await retryBox.click();
|
|
28293
|
+
await retryBox.fill("");
|
|
28294
|
+
await retryBox.type(term.trim(), { delay: 110 });
|
|
28295
|
+
await page.waitForSelector('[role="option"]', { timeout: 15e3 }).catch(() => null);
|
|
28296
|
+
await page.waitForTimeout(1800);
|
|
28297
|
+
}
|
|
28298
|
+
return null;
|
|
28299
|
+
}
|
|
27940
28300
|
async pageIntel(opts) {
|
|
27941
28301
|
const { region, maxAds } = opts;
|
|
27942
28302
|
let advertiserId = opts.advertiserId?.trim() || null;
|
|
27943
|
-
if (!advertiserId && opts.domain) {
|
|
27944
|
-
|
|
27945
|
-
|
|
28303
|
+
if (!advertiserId && opts.domain?.trim()) {
|
|
28304
|
+
advertiserId = await this.resolveAdvertiserIdByDomain(opts.domain, region);
|
|
28305
|
+
}
|
|
28306
|
+
if (!advertiserId && opts.advertiserName?.trim()) {
|
|
28307
|
+
advertiserId = await this.resolveAdvertiserIdByTypeahead(opts.advertiserName, region);
|
|
27946
28308
|
}
|
|
27947
28309
|
if (!advertiserId) {
|
|
27948
28310
|
return { advertiserId: null, advertiserName: null, domain: opts.domain ?? null, region, totalCreatives: 0, videoCount: 0, imageCount: 0, textCount: 0, ads: [] };
|
|
@@ -28103,10 +28465,11 @@ var init_google_ads_routes = __esm({
|
|
|
28103
28465
|
});
|
|
28104
28466
|
GoogleAdsPageIntelBodySchema = import_zod22.z.object({
|
|
28105
28467
|
advertiserId: import_zod22.z.string().trim().optional(),
|
|
28468
|
+
advertiserName: import_zod22.z.string().trim().optional(),
|
|
28106
28469
|
domain: import_zod22.z.string().trim().optional(),
|
|
28107
28470
|
region: import_zod22.z.string().trim().toUpperCase().length(2).optional(),
|
|
28108
28471
|
maxAds: import_zod22.z.number().int().min(1).max(200).optional()
|
|
28109
|
-
}).refine((d) => !!d.advertiserId || !!d.domain, { message: "advertiserId or domain is required" });
|
|
28472
|
+
}).refine((d) => !!d.advertiserId || !!d.domain || !!d.advertiserName, { message: "advertiserId, advertiserName, or domain is required (advertiserName and domain both come straight from google_ads_search)" });
|
|
28110
28473
|
GoogleAdsTranscribeBodySchema = import_zod22.z.object({
|
|
28111
28474
|
videoUrl: import_zod22.z.string().trim().min(1, "videoUrl is required")
|
|
28112
28475
|
});
|
|
@@ -28132,13 +28495,13 @@ var init_google_ads_routes = __esm({
|
|
|
28132
28495
|
const extractor = new GoogleAdsExtractor(driver);
|
|
28133
28496
|
return extractor.search(body.query.trim(), region, maxResults);
|
|
28134
28497
|
});
|
|
28135
|
-
if (result.advertisers.length === 0) {
|
|
28498
|
+
if (result.advertisers.length === 0 && result.websites.length === 0) {
|
|
28136
28499
|
await creditMc(user.id, MC_COSTS.google_ads_search, LedgerOperation.GOOGLE_ADS_SEARCH_REFUND, "empty result");
|
|
28137
28500
|
refunded = true;
|
|
28138
28501
|
await logRequestEvent({ userId: user.id, source: "google_ads_search", status: "failed", query: body.query.trim(), error: "empty result refunded" });
|
|
28139
28502
|
return c.json({ error: "no advertisers found (refunded)" }, 503);
|
|
28140
28503
|
}
|
|
28141
|
-
await logRequestEvent({ userId: user.id, source: "google_ads_search", status: "done", query: body.query.trim(), resultCount: result.advertisers.length, result });
|
|
28504
|
+
await logRequestEvent({ userId: user.id, source: "google_ads_search", status: "done", query: body.query.trim(), resultCount: result.advertisers.length + result.websites.length, result });
|
|
28142
28505
|
return c.json(result);
|
|
28143
28506
|
} catch (err) {
|
|
28144
28507
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -28157,7 +28520,7 @@ var init_google_ads_routes = __esm({
|
|
|
28157
28520
|
const body = parsed.data;
|
|
28158
28521
|
const region = body.region ?? "US";
|
|
28159
28522
|
const maxAds = Math.min(200, Math.max(1, body.maxAds ?? 50));
|
|
28160
|
-
const queryLabel = body.advertiserId ?? body.domain ?? "";
|
|
28523
|
+
const queryLabel = body.advertiserId ?? body.domain ?? body.advertiserName ?? "";
|
|
28161
28524
|
const user = c.get("user");
|
|
28162
28525
|
const gate = await acquireConcurrencyGate(user, "google_ads_page_intel", { reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"), metadata: { advertiserId: body.advertiserId, domain: body.domain, region } });
|
|
28163
28526
|
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
@@ -28170,7 +28533,7 @@ var init_google_ads_routes = __esm({
|
|
|
28170
28533
|
const result = await runWithCostContext({ ...currentCostContext(), op: "google_ads_intel", userId: user.id }, async () => {
|
|
28171
28534
|
await driver.launch(await kernelLaunchOptsResidential2());
|
|
28172
28535
|
const extractor = new GoogleAdsExtractor(driver);
|
|
28173
|
-
return extractor.pageIntel({ advertiserId: body.advertiserId, domain: body.domain, region, maxAds });
|
|
28536
|
+
return extractor.pageIntel({ advertiserId: body.advertiserId, advertiserName: body.advertiserName, domain: body.domain, region, maxAds });
|
|
28174
28537
|
});
|
|
28175
28538
|
if (result.ads.length === 0) {
|
|
28176
28539
|
await creditMc(user.id, MC_COSTS.google_ads_intel, LedgerOperation.GOOGLE_ADS_INTEL_REFUND, "empty result");
|
|
@@ -29118,6 +29481,7 @@ var init_instagram_routes = __esm({
|
|
|
29118
29481
|
init_db();
|
|
29119
29482
|
init_rates();
|
|
29120
29483
|
init_concurrency_gates();
|
|
29484
|
+
init_cost_context();
|
|
29121
29485
|
init_blob_store();
|
|
29122
29486
|
init_InstagramContentExtractor();
|
|
29123
29487
|
init_media_transcription();
|
|
@@ -29181,6 +29545,12 @@ var init_instagram_routes = __esm({
|
|
|
29181
29545
|
scrollDelayMs: body.scrollDelayMs,
|
|
29182
29546
|
stableScrollLimit: body.stableScrollLimit
|
|
29183
29547
|
});
|
|
29548
|
+
if (result.items.length === 0) {
|
|
29549
|
+
await creditMc(user.id, MC_COSTS.instagram_profile, LedgerOperation.INSTAGRAM_PROFILE_REFUND, "empty result");
|
|
29550
|
+
debited = false;
|
|
29551
|
+
await logRequestEvent({ userId: user.id, source: "instagram_profile_content", status: "failed", query: target.profileUrl, error: "empty result refunded" });
|
|
29552
|
+
return c.json({ error: `No public posts were readable on ${target.profileUrl} (refunded). The profile may be private, empty, or require a logged-in session.` }, 404);
|
|
29553
|
+
}
|
|
29184
29554
|
const response = { ...result, browser: launch.browser };
|
|
29185
29555
|
await logRequestEvent({ userId: user.id, source: "instagram_profile_content", status: "done", query: target.profileUrl, resultCount: result.items.length, result: response });
|
|
29186
29556
|
return c.json(response);
|
|
@@ -29304,7 +29674,10 @@ var init_instagram_routes = __esm({
|
|
|
29304
29674
|
const tx = await debitMc(user.id, holdMc, LedgerOperation.INSTAGRAM_TRANSCRIBE, sourceUrl.href);
|
|
29305
29675
|
if (!tx.ok) return c.json(insufficientBalanceResponse(tx.balance_mc, holdMc), 402);
|
|
29306
29676
|
transcriptDebited = true;
|
|
29307
|
-
transcript = await
|
|
29677
|
+
transcript = await runWithCostContext(
|
|
29678
|
+
{ ...currentCostContext(), op: "instagram_transcribe", userId: user.id },
|
|
29679
|
+
() => transcribeMediaUrl(audioUrl, "# Instagram Media Transcript")
|
|
29680
|
+
);
|
|
29308
29681
|
const settleMc = mediaTranscribeChargeMc(transcript.durationMs / 1e3);
|
|
29309
29682
|
const diff = holdMc - settleMc;
|
|
29310
29683
|
if (diff > 0) await creditMc(user.id, diff, LedgerOperation.INSTAGRAM_TRANSCRIBE_REFUND, "real-cost reconcile");
|
|
@@ -30006,6 +30379,8 @@ var init_PAAExtractor = __esm({
|
|
|
30006
30379
|
aioShareCaptured: null
|
|
30007
30380
|
};
|
|
30008
30381
|
aioShareUrlEarly = null;
|
|
30382
|
+
collectedRows = [];
|
|
30383
|
+
partialContext = null;
|
|
30009
30384
|
normalizeQuestion(q) {
|
|
30010
30385
|
return q.toLowerCase().replace(/[^\w\s]/g, "").replace(/\s+/g, " ").trim();
|
|
30011
30386
|
}
|
|
@@ -30110,11 +30485,50 @@ var init_PAAExtractor = __esm({
|
|
|
30110
30485
|
extracted_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
30111
30486
|
};
|
|
30112
30487
|
}
|
|
30488
|
+
getPartialResult() {
|
|
30489
|
+
const ctx = this.partialContext;
|
|
30490
|
+
if (!ctx || this.collectedRows.length === 0) return null;
|
|
30491
|
+
const flat = [...this.collectedRows];
|
|
30492
|
+
const stats = {
|
|
30493
|
+
seed: ctx.seed,
|
|
30494
|
+
totalQuestions: flat.length,
|
|
30495
|
+
maxDepthReached: flat.reduce((m, r) => Math.max(m, r.depth), 0),
|
|
30496
|
+
durationMs: Date.now() - ctx.startMs,
|
|
30497
|
+
errorCount: 0
|
|
30498
|
+
};
|
|
30499
|
+
return {
|
|
30500
|
+
seed: ctx.seed,
|
|
30501
|
+
location: ctx.location,
|
|
30502
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
30503
|
+
diagnostics: {
|
|
30504
|
+
completionStatus: "paa_found",
|
|
30505
|
+
problem: null,
|
|
30506
|
+
completeness: { ...this.completeness },
|
|
30507
|
+
warnings: [{
|
|
30508
|
+
code: "paa_partial",
|
|
30509
|
+
surface: "paa",
|
|
30510
|
+
message: `Harvest stopped before finishing; returning the ${flat.length} question(s) collected up to that point.`,
|
|
30511
|
+
retryable: true
|
|
30512
|
+
}]
|
|
30513
|
+
},
|
|
30514
|
+
totalQuestions: flat.length,
|
|
30515
|
+
whatPeopleSaying: [],
|
|
30516
|
+
tree: this.buildTree(flat, ctx.seed),
|
|
30517
|
+
flat,
|
|
30518
|
+
videos: [],
|
|
30519
|
+
forums: [],
|
|
30520
|
+
organicResults: [],
|
|
30521
|
+
localPack: [],
|
|
30522
|
+
entityIds: {},
|
|
30523
|
+
stats
|
|
30524
|
+
};
|
|
30525
|
+
}
|
|
30113
30526
|
async runBFS(page, options, signal) {
|
|
30114
30527
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
30115
30528
|
const seenQs = /* @__PURE__ */ new Set();
|
|
30116
30529
|
const orderedQs = [];
|
|
30117
|
-
|
|
30530
|
+
this.collectedRows = [];
|
|
30531
|
+
const results = this.collectedRows;
|
|
30118
30532
|
const abandonedNeverReclick = /* @__PURE__ */ new Set();
|
|
30119
30533
|
const clickedOnceEver = /* @__PURE__ */ new Set();
|
|
30120
30534
|
const itemArgs = {
|
|
@@ -30726,6 +31140,8 @@ var init_PAAExtractor = __esm({
|
|
|
30726
31140
|
const startMs = Date.now();
|
|
30727
31141
|
this.completeness = { paaWithoutAnswer: 0, paaWithoutSource: 0, paaAnswersRecovered: 0, aioShareCaptured: null };
|
|
30728
31142
|
this.aioShareUrlEarly = null;
|
|
31143
|
+
this.collectedRows = [];
|
|
31144
|
+
this.partialContext = { seed: options.query, location: options.location ?? null, startMs };
|
|
30729
31145
|
const isMobile = options.device === "mobile";
|
|
30730
31146
|
const config = {
|
|
30731
31147
|
headless: options.headless,
|
|
@@ -31273,7 +31689,23 @@ async function extractOnce2(options, signal) {
|
|
|
31273
31689
|
} : null);
|
|
31274
31690
|
cleanup = await driver.close();
|
|
31275
31691
|
}
|
|
31276
|
-
|
|
31692
|
+
if (error) {
|
|
31693
|
+
const outcome = classifyAttemptError(error);
|
|
31694
|
+
const budgetExhausted = outcome === "timeout" || outcome === "request_aborted";
|
|
31695
|
+
let salvaged = null;
|
|
31696
|
+
if (budgetExhausted) {
|
|
31697
|
+
try {
|
|
31698
|
+
salvaged = typeof extractor.getPartialResult === "function" ? extractor.getPartialResult() : null;
|
|
31699
|
+
} catch {
|
|
31700
|
+
salvaged = null;
|
|
31701
|
+
}
|
|
31702
|
+
}
|
|
31703
|
+
if (salvaged) {
|
|
31704
|
+
return { result: salvaged, error: null, cleanup, debug, salvagedFrom: error };
|
|
31705
|
+
}
|
|
31706
|
+
return { result: null, error, cleanup, debug };
|
|
31707
|
+
}
|
|
31708
|
+
return { result, error: null, cleanup, debug };
|
|
31277
31709
|
}
|
|
31278
31710
|
async function harvest(rawOptions) {
|
|
31279
31711
|
const raw = typeof rawOptions === "object" && rawOptions !== null ? rawOptions : {};
|
|
@@ -32891,6 +33323,7 @@ var init_trustpilot_routes = __esm({
|
|
|
32891
33323
|
const driver = new BrowserDriver();
|
|
32892
33324
|
const allReviews = [];
|
|
32893
33325
|
let pagesFetched = 0;
|
|
33326
|
+
let stoppedEarly = null;
|
|
32894
33327
|
try {
|
|
32895
33328
|
await driver.launch({
|
|
32896
33329
|
headless: false,
|
|
@@ -32908,6 +33341,12 @@ var init_trustpilot_routes = __esm({
|
|
|
32908
33341
|
if (cards.length === 0) break;
|
|
32909
33342
|
allReviews.push(...cards);
|
|
32910
33343
|
}
|
|
33344
|
+
} catch (err) {
|
|
33345
|
+
if (allReviews.length === 0) throw err;
|
|
33346
|
+
stoppedEarly = {
|
|
33347
|
+
code: "review_scrape_stopped_early",
|
|
33348
|
+
message: err instanceof Error ? err.message : String(err)
|
|
33349
|
+
};
|
|
32911
33350
|
} finally {
|
|
32912
33351
|
await driver.close();
|
|
32913
33352
|
}
|
|
@@ -32933,6 +33372,7 @@ var init_trustpilot_routes = __esm({
|
|
|
32933
33372
|
pagesFetched,
|
|
32934
33373
|
reviewCount: allReviews.length,
|
|
32935
33374
|
reviews: allReviews,
|
|
33375
|
+
stoppedEarly,
|
|
32936
33376
|
durationMs: Date.now() - startMs
|
|
32937
33377
|
};
|
|
32938
33378
|
await logRequestEvent({ userId: user.id, source: "trustpilot_reviews", status: "done", query: domain, resultCount: allReviews.length, result });
|
|
@@ -37478,7 +37918,7 @@ var PACKAGE_VERSION;
|
|
|
37478
37918
|
var init_version = __esm({
|
|
37479
37919
|
"src/version.ts"() {
|
|
37480
37920
|
"use strict";
|
|
37481
|
-
PACKAGE_VERSION = "0.38.
|
|
37921
|
+
PACKAGE_VERSION = "0.38.2";
|
|
37482
37922
|
}
|
|
37483
37923
|
});
|
|
37484
37924
|
|
|
@@ -37975,9 +38415,9 @@ var init_mcp_tool_schemas = __esm({
|
|
|
37975
38415
|
const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
37976
38416
|
const looksLikeIpv4 = /^\d{1,3}(?:\.\d{1,3}){3}$/.test(hostname);
|
|
37977
38417
|
const looksLikeIpv6 = hostname.includes(":");
|
|
37978
|
-
const
|
|
38418
|
+
const looksLikeDomain = hostname.includes(".");
|
|
37979
38419
|
const looksLikeLocalhost = hostname === "localhost";
|
|
37980
|
-
if (!looksLikeIpv4 && !looksLikeIpv6 && !
|
|
38420
|
+
if (!looksLikeIpv4 && !looksLikeIpv6 && !looksLikeDomain && !looksLikeLocalhost) {
|
|
37981
38421
|
ctx.addIssue({ code: import_zod38.z.ZodIssueCode.custom, message: WEBSITE_URL_OR_DOMAIN_ERROR });
|
|
37982
38422
|
return import_zod38.z.NEVER;
|
|
37983
38423
|
}
|
|
@@ -38113,8 +38553,9 @@ var init_mcp_tool_schemas = __esm({
|
|
|
38113
38553
|
maxResults: import_zod38.z.number().int().min(1).max(20).default(10).describe("Maximum advertisers to return. Default 10, maximum 20.")
|
|
38114
38554
|
};
|
|
38115
38555
|
GoogleAdsPageIntelInputSchema = {
|
|
38116
|
-
|
|
38117
|
-
|
|
38556
|
+
domain: import_zod38.z.string().optional().describe("A website from google_ads_search.websites (e.g. getviktor.com). Preferred input: people know the website, not the advertiser's registered name. Resolves to whichever advertiser account runs ads for that domain."),
|
|
38557
|
+
advertiserName: import_zod38.z.string().optional().describe('An advertiser name exactly as returned in google_ads_search.advertisers[].name (e.g. "PPS Plumbing Services"). Use when the user picked an advertiser rather than a website.'),
|
|
38558
|
+
advertiserId: import_zod38.z.string().optional().describe("Google Ads Transparency advertiser ID (starts with AR...). Only available if you already have one from a prior page-intel result \u2014 google_ads_search does NOT return advertiser IDs. Do not construct one yourself."),
|
|
38118
38559
|
region: import_zod38.z.string().length(2).default("US").describe("Two-letter region code for where the ads are shown. Default US."),
|
|
38119
38560
|
maxAds: import_zod38.z.number().int().min(1).max(200).default(50).describe("Maximum creatives to inspect and hydrate. Default 50, maximum 200. Prefer 25-50 for focused scans.")
|
|
38120
38561
|
};
|
|
@@ -38569,6 +39010,10 @@ var init_mcp_tool_schemas = __esm({
|
|
|
38569
39010
|
};
|
|
38570
39011
|
ExtractSiteOutputSchema = {
|
|
38571
39012
|
url: import_zod38.z.string(),
|
|
39013
|
+
stoppedEarly: import_zod38.z.object({
|
|
39014
|
+
code: import_zod38.z.string(),
|
|
39015
|
+
message: import_zod38.z.string()
|
|
39016
|
+
}).nullish().describe("Present when the crawl stopped before finishing. The returned pages are everything that was captured, and billing covers only those pages - treat the result as partial, not as the whole site."),
|
|
38572
39017
|
pageCount: import_zod38.z.number().int().min(0).optional().describe("Absent when background is true \u2014 the crawl has not finished yet."),
|
|
38573
39018
|
pages: import_zod38.z.array(import_zod38.z.object({
|
|
38574
39019
|
url: import_zod38.z.string(),
|
|
@@ -38948,9 +39393,15 @@ var init_mcp_tool_schemas = __esm({
|
|
|
38948
39393
|
advertisers: import_zod38.z.array(import_zod38.z.object({
|
|
38949
39394
|
advertiserId: NullableString,
|
|
38950
39395
|
name: NullableString,
|
|
39396
|
+
basedIn: NullableString,
|
|
38951
39397
|
domain: NullableString,
|
|
38952
39398
|
approxAdCount: import_zod38.z.number().int().nullable(),
|
|
38953
39399
|
detailUrl: NullableString
|
|
39400
|
+
})),
|
|
39401
|
+
websiteCount: import_zod38.z.number().int().min(0),
|
|
39402
|
+
websites: import_zod38.z.array(import_zod38.z.object({
|
|
39403
|
+
domain: import_zod38.z.string(),
|
|
39404
|
+
detailUrl: import_zod38.z.string()
|
|
38954
39405
|
}))
|
|
38955
39406
|
};
|
|
38956
39407
|
GoogleAdsPageIntelOutputSchema = {
|
|
@@ -40267,14 +40718,14 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
|
|
|
40267
40718
|
}, async (input) => formatFacebookAdTranscribe(await executor.facebookAdTranscribe(input), input));
|
|
40268
40719
|
server.registerTool("google_ads_search", {
|
|
40269
40720
|
title: "Google Ads Transparency Search",
|
|
40270
|
-
description: "Search the Google Ads Transparency Center
|
|
40721
|
+
description: "Search the Google Ads Transparency Center by brand, person, or website. Returns two lists: advertisers (name, country, approximate ad count) and websites (domains). Neither carries an advertiser ID. Pass a websites[].domain (preferred - users know the website, not the registered advertiser name) or an advertisers[].name to google_ads_page_intel to pull that advertiser's actual ads.",
|
|
40271
40722
|
inputSchema: GoogleAdsSearchInputSchema,
|
|
40272
40723
|
outputSchema: recordOutputSchema("google_ads_search", GoogleAdsSearchOutputSchema),
|
|
40273
40724
|
annotations: liveWebToolAnnotations("Google Ads Transparency Search")
|
|
40274
40725
|
}, async (input) => formatGoogleAdsSearch(await executor.googleAdsSearch(input), input));
|
|
40275
40726
|
server.registerTool("google_ads_page_intel", {
|
|
40276
40727
|
title: "Google Ads Advertiser Intel",
|
|
40277
|
-
description: "Harvest an advertiser's ad creatives from the Google Ads Transparency Center: format, image URLs, and \u2014 for video ads \u2014 a YouTube video ID or direct video URL. Accepts
|
|
40728
|
+
description: "Harvest an advertiser's ad creatives from the Google Ads Transparency Center: format, image URLs, and \u2014 for video ads \u2014 a YouTube video ID or direct video URL. Accepts a domain or advertiserName from google_ads_search (google_ads_search does not return advertiser IDs). A domain may map to several advertiser accounts; this returns the primary one.",
|
|
40278
40729
|
inputSchema: GoogleAdsPageIntelInputSchema,
|
|
40279
40730
|
outputSchema: recordOutputSchema("google_ads_page_intel", GoogleAdsPageIntelOutputSchema),
|
|
40280
40731
|
annotations: liveWebToolAnnotations("Google Ads Advertiser Intel")
|
|
@@ -48348,6 +48799,16 @@ var init_browser_agent_console = __esm({
|
|
|
48348
48799
|
}
|
|
48349
48800
|
});
|
|
48350
48801
|
|
|
48802
|
+
// src/api/stripe-credit-policy.ts
|
|
48803
|
+
function shouldGrantScraperSubscriptionCredits(invoiceHasTierLine, invoiceTotal) {
|
|
48804
|
+
return invoiceHasTierLine && (invoiceTotal ?? 0) > 0;
|
|
48805
|
+
}
|
|
48806
|
+
var init_stripe_credit_policy = __esm({
|
|
48807
|
+
"src/api/stripe-credit-policy.ts"() {
|
|
48808
|
+
"use strict";
|
|
48809
|
+
}
|
|
48810
|
+
});
|
|
48811
|
+
|
|
48351
48812
|
// src/api/stripe-routes.ts
|
|
48352
48813
|
async function cancelStandaloneMemorySub(memSubId, baseSubId) {
|
|
48353
48814
|
if (!memSubId || memSubId === baseSubId) return;
|
|
@@ -48362,6 +48823,106 @@ async function cancelStandaloneMemorySub(memSubId, baseSubId) {
|
|
|
48362
48823
|
console.error("[stripe] could not wind down standalone memory subscription", memSubId, err instanceof Error ? err.message : err);
|
|
48363
48824
|
}
|
|
48364
48825
|
}
|
|
48826
|
+
async function processStripeEvent(event) {
|
|
48827
|
+
if (event.type === "invoice.paid" || event.type === "invoice.payment_succeeded") {
|
|
48828
|
+
const invoice = event.data.object;
|
|
48829
|
+
const memLineId = invoice.lines.data.map(linePriceId).find((id) => id && id in MEMORY_PLANS);
|
|
48830
|
+
if (memLineId) return;
|
|
48831
|
+
const invoiceHasTierLine = invoice.lines.data.some((l) => {
|
|
48832
|
+
const id = linePriceId(l);
|
|
48833
|
+
return id && id in SUBSCRIPTION_TIERS;
|
|
48834
|
+
});
|
|
48835
|
+
if (shouldGrantScraperSubscriptionCredits(invoiceHasTierLine, invoice.total)) {
|
|
48836
|
+
const subId = invoice.subscription;
|
|
48837
|
+
const liveBasePlanPriceId = subId ? findBasePlanItem(await stripe.subscriptions.retrieve(subId))?.price?.id : void 0;
|
|
48838
|
+
const tierPriceId = liveBasePlanPriceId ?? invoice.lines.data.map(linePriceId).find((id) => id && id in SUBSCRIPTION_TIERS);
|
|
48839
|
+
const tier = tierPriceId ? SUBSCRIPTION_TIERS[tierPriceId] : void 0;
|
|
48840
|
+
if (tier) {
|
|
48841
|
+
const user = await resolveUser3(invoice.customer, invoice.customer_email ?? void 0);
|
|
48842
|
+
if (user && invoice.id && !await ledgerExistsForStripePI(invoice.id)) {
|
|
48843
|
+
await creditMc(user.id, tier.credits_mc, LedgerOperation.SUBSCRIPTION, `${tier.label} subscription credits`, invoice.id, { neverExpires: tier.credits_never_expire });
|
|
48844
|
+
await setSubscriptionTier(user.id, tier.tier, tier.concurrency, subId ?? user.subscription_id);
|
|
48845
|
+
if (tier.includes_memory) {
|
|
48846
|
+
await cancelStandaloneMemorySub(user.memory_subscription_id, subId ?? user.subscription_id);
|
|
48847
|
+
await setMemoryPlan(user.id, "pro", null);
|
|
48848
|
+
await syncMemoryKeyPlan(user, resolveEffectiveMemoryPlan({ ...user, memory_plan: "pro", subscription_tier: tier.tier }));
|
|
48849
|
+
}
|
|
48850
|
+
const credentials = await syncScheduledActionCredentials(user);
|
|
48851
|
+
if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
|
|
48852
|
+
}
|
|
48853
|
+
}
|
|
48854
|
+
}
|
|
48855
|
+
}
|
|
48856
|
+
if (event.type === "customer.subscription.created" || event.type === "customer.subscription.updated") {
|
|
48857
|
+
const sub = event.data.object;
|
|
48858
|
+
const priceId = findBasePlanItem(sub)?.price?.id ?? sub.items.data[0]?.price?.id;
|
|
48859
|
+
if (priceId && priceId in MEMORY_PLANS) {
|
|
48860
|
+
const mem = MEMORY_PLANS[priceId];
|
|
48861
|
+
const user2 = await resolveUser3(sub.customer, sub.customer_email);
|
|
48862
|
+
if (user2) {
|
|
48863
|
+
const live = sub.status === "active" || sub.status === "trialing";
|
|
48864
|
+
const newMemoryPlan = live ? mem.plan : "free";
|
|
48865
|
+
await setMemoryPlan(user2.id, newMemoryPlan, live ? sub.id : null);
|
|
48866
|
+
await syncMemoryKeyPlan(user2, resolveEffectiveMemoryPlan({ ...user2, memory_plan: newMemoryPlan }));
|
|
48867
|
+
}
|
|
48868
|
+
return;
|
|
48869
|
+
}
|
|
48870
|
+
const user = await getUserByStripeCustomerId(sub.customer);
|
|
48871
|
+
if (!user) return;
|
|
48872
|
+
if (priceId && priceId in SUBSCRIPTION_TIERS) {
|
|
48873
|
+
const tier = SUBSCRIPTION_TIERS[priceId];
|
|
48874
|
+
const live = sub.status === "active" || sub.status === "trialing";
|
|
48875
|
+
const newTier = live ? tier.tier : null;
|
|
48876
|
+
await setSubscriptionTier(user.id, newTier, live ? tier.concurrency : 0, live ? sub.id : null);
|
|
48877
|
+
if (tier.includes_memory) {
|
|
48878
|
+
const newMemoryPlan = live ? "pro" : "free";
|
|
48879
|
+
if (live) await cancelStandaloneMemorySub(user.memory_subscription_id, sub.id);
|
|
48880
|
+
await setMemoryPlan(user.id, newMemoryPlan, live ? sub.id : null);
|
|
48881
|
+
await syncMemoryKeyPlan(user, resolveEffectiveMemoryPlan({ ...user, memory_plan: newMemoryPlan }));
|
|
48882
|
+
} else if (user.memory_subscription_id === sub.id) {
|
|
48883
|
+
await setMemoryPlan(user.id, "free", null);
|
|
48884
|
+
await syncMemoryKeyPlan(user, resolveEffectiveMemoryPlan({ ...user, memory_plan: "free" }));
|
|
48885
|
+
}
|
|
48886
|
+
if (live) {
|
|
48887
|
+
const credentials = await syncScheduledActionCredentials(user);
|
|
48888
|
+
if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
|
|
48889
|
+
}
|
|
48890
|
+
} else if (priceId === CONCURRENCY_PRICE_ID && event.type === "customer.subscription.created") {
|
|
48891
|
+
await setExtraConcurrencySlots(user.id, 1);
|
|
48892
|
+
await setConcurrencySubId(user.id, sub.id);
|
|
48893
|
+
}
|
|
48894
|
+
}
|
|
48895
|
+
if (event.type === "customer.subscription.deleted") {
|
|
48896
|
+
const sub = event.data.object;
|
|
48897
|
+
const priceId = findBasePlanItem(sub)?.price?.id ?? sub.items.data[0]?.price?.id;
|
|
48898
|
+
if (priceId && priceId in SUBSCRIPTION_TIERS) {
|
|
48899
|
+
const tier = SUBSCRIPTION_TIERS[priceId];
|
|
48900
|
+
const user2 = await getUserByStripeCustomerId(sub.customer);
|
|
48901
|
+
if (user2) {
|
|
48902
|
+
await setSubscriptionTier(user2.id, null, 0, null);
|
|
48903
|
+
if (tier.includes_memory || user2.memory_subscription_id === sub.id) {
|
|
48904
|
+
await setMemoryPlan(user2.id, "free", null);
|
|
48905
|
+
await syncMemoryKeyPlan(user2, resolveEffectiveMemoryPlan({ ...user2, memory_plan: "free" }));
|
|
48906
|
+
}
|
|
48907
|
+
}
|
|
48908
|
+
return;
|
|
48909
|
+
}
|
|
48910
|
+
if (priceId && priceId in MEMORY_PLANS || await resolveUserByMemorySubscription(sub.id)) {
|
|
48911
|
+
const user2 = await resolveUserByMemorySubscription(sub.id) ?? await getUserByStripeCustomerId(sub.customer);
|
|
48912
|
+
if (user2) {
|
|
48913
|
+
await setMemoryPlan(user2.id, "free", null);
|
|
48914
|
+
await syncMemoryKeyPlan(user2, resolveEffectiveMemoryPlan({ ...user2, memory_plan: "free" }));
|
|
48915
|
+
}
|
|
48916
|
+
return;
|
|
48917
|
+
}
|
|
48918
|
+
const user = await getUserByStripeCustomerId(sub.customer);
|
|
48919
|
+
if (!user) return;
|
|
48920
|
+
if (priceId === CONCURRENCY_PRICE_ID) {
|
|
48921
|
+
await setExtraConcurrencySlots(user.id, 0);
|
|
48922
|
+
await setConcurrencySubId(user.id, null);
|
|
48923
|
+
}
|
|
48924
|
+
}
|
|
48925
|
+
}
|
|
48365
48926
|
function linePriceId(line) {
|
|
48366
48927
|
const l = line;
|
|
48367
48928
|
return l?.price?.id ?? l?.pricing?.price_details?.price ?? l?.plan?.id;
|
|
@@ -48384,6 +48945,7 @@ var init_stripe_routes = __esm({
|
|
|
48384
48945
|
init_rates();
|
|
48385
48946
|
init_memory();
|
|
48386
48947
|
init_connected_account_billing();
|
|
48948
|
+
init_stripe_credit_policy();
|
|
48387
48949
|
stripe = new import_stripe.default(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
|
|
48388
48950
|
stripeApp = new import_hono22.Hono();
|
|
48389
48951
|
stripeApp.post("/webhooks", async (c) => {
|
|
@@ -48395,107 +48957,19 @@ var init_stripe_routes = __esm({
|
|
|
48395
48957
|
} catch {
|
|
48396
48958
|
return c.json({ error: "Invalid signature" }, 400);
|
|
48397
48959
|
}
|
|
48398
|
-
const
|
|
48399
|
-
if (
|
|
48400
|
-
|
|
48401
|
-
|
|
48402
|
-
const
|
|
48403
|
-
if (
|
|
48404
|
-
|
|
48405
|
-
|
|
48406
|
-
|
|
48407
|
-
|
|
48408
|
-
|
|
48409
|
-
|
|
48410
|
-
const liveBasePlanPriceId = subId ? findBasePlanItem(await stripe.subscriptions.retrieve(subId))?.price?.id : void 0;
|
|
48411
|
-
const tierPriceId = liveBasePlanPriceId ?? invoice.lines.data.map(linePriceId).find((id) => id && id in SUBSCRIPTION_TIERS);
|
|
48412
|
-
const tier = tierPriceId ? SUBSCRIPTION_TIERS[tierPriceId] : void 0;
|
|
48413
|
-
if (tier) {
|
|
48414
|
-
const user = await resolveUser3(invoice.customer, invoice.customer_email ?? void 0);
|
|
48415
|
-
if (user && invoice.id && !await ledgerExistsForStripePI(invoice.id)) {
|
|
48416
|
-
await creditMc(user.id, tier.credits_mc, LedgerOperation.SUBSCRIPTION, `${tier.label} subscription credits`, invoice.id, { neverExpires: tier.credits_never_expire });
|
|
48417
|
-
await setSubscriptionTier(user.id, tier.tier, tier.concurrency, subId ?? user.subscription_id);
|
|
48418
|
-
if (tier.includes_memory) {
|
|
48419
|
-
await cancelStandaloneMemorySub(user.memory_subscription_id, subId ?? user.subscription_id);
|
|
48420
|
-
await setMemoryPlan(user.id, "pro", null);
|
|
48421
|
-
await syncMemoryKeyPlan(user, resolveEffectiveMemoryPlan({ ...user, memory_plan: "pro", subscription_tier: tier.tier }));
|
|
48422
|
-
}
|
|
48423
|
-
const credentials = await syncScheduledActionCredentials(user);
|
|
48424
|
-
if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
|
|
48425
|
-
}
|
|
48426
|
-
}
|
|
48427
|
-
}
|
|
48428
|
-
}
|
|
48429
|
-
if (event.type === "customer.subscription.created" || event.type === "customer.subscription.updated") {
|
|
48430
|
-
const sub = event.data.object;
|
|
48431
|
-
const priceId = findBasePlanItem(sub)?.price?.id ?? sub.items.data[0]?.price?.id;
|
|
48432
|
-
if (priceId && priceId in MEMORY_PLANS) {
|
|
48433
|
-
const mem = MEMORY_PLANS[priceId];
|
|
48434
|
-
const user2 = await resolveUser3(sub.customer, sub.customer_email);
|
|
48435
|
-
if (user2) {
|
|
48436
|
-
const live = sub.status === "active" || sub.status === "trialing";
|
|
48437
|
-
const newMemoryPlan = live ? mem.plan : "free";
|
|
48438
|
-
await setMemoryPlan(user2.id, newMemoryPlan, live ? sub.id : null);
|
|
48439
|
-
await syncMemoryKeyPlan(user2, resolveEffectiveMemoryPlan({ ...user2, memory_plan: newMemoryPlan }));
|
|
48440
|
-
}
|
|
48441
|
-
return c.json({ received: true });
|
|
48442
|
-
}
|
|
48443
|
-
const user = await getUserByStripeCustomerId(sub.customer);
|
|
48444
|
-
if (!user) return c.json({ received: true });
|
|
48445
|
-
if (priceId && priceId in SUBSCRIPTION_TIERS) {
|
|
48446
|
-
const tier = SUBSCRIPTION_TIERS[priceId];
|
|
48447
|
-
const live = sub.status === "active" || sub.status === "trialing";
|
|
48448
|
-
const newTier = live ? tier.tier : null;
|
|
48449
|
-
await setSubscriptionTier(user.id, newTier, live ? tier.concurrency : 0, live ? sub.id : null);
|
|
48450
|
-
if (tier.includes_memory) {
|
|
48451
|
-
const newMemoryPlan = live ? "pro" : "free";
|
|
48452
|
-
if (live) await cancelStandaloneMemorySub(user.memory_subscription_id, sub.id);
|
|
48453
|
-
await setMemoryPlan(user.id, newMemoryPlan, live ? sub.id : null);
|
|
48454
|
-
await syncMemoryKeyPlan(user, resolveEffectiveMemoryPlan({ ...user, memory_plan: newMemoryPlan }));
|
|
48455
|
-
} else if (user.memory_subscription_id === sub.id) {
|
|
48456
|
-
await setMemoryPlan(user.id, "free", null);
|
|
48457
|
-
await syncMemoryKeyPlan(user, resolveEffectiveMemoryPlan({ ...user, memory_plan: "free" }));
|
|
48458
|
-
}
|
|
48459
|
-
if (live) {
|
|
48460
|
-
const credentials = await syncScheduledActionCredentials(user);
|
|
48461
|
-
if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
|
|
48462
|
-
}
|
|
48463
|
-
} else if (priceId === CONCURRENCY_PRICE_ID && event.type === "customer.subscription.created") {
|
|
48464
|
-
await setExtraConcurrencySlots(user.id, user.extra_concurrency_slots + 1);
|
|
48465
|
-
await setConcurrencySubId(user.id, sub.id);
|
|
48466
|
-
}
|
|
48467
|
-
}
|
|
48468
|
-
if (event.type === "customer.subscription.deleted") {
|
|
48469
|
-
const sub = event.data.object;
|
|
48470
|
-
const priceId = findBasePlanItem(sub)?.price?.id ?? sub.items.data[0]?.price?.id;
|
|
48471
|
-
if (priceId && priceId in SUBSCRIPTION_TIERS) {
|
|
48472
|
-
const tier = SUBSCRIPTION_TIERS[priceId];
|
|
48473
|
-
const user2 = await getUserByStripeCustomerId(sub.customer);
|
|
48474
|
-
if (user2) {
|
|
48475
|
-
await setSubscriptionTier(user2.id, null, 0, null);
|
|
48476
|
-
if (tier.includes_memory || user2.memory_subscription_id === sub.id) {
|
|
48477
|
-
await setMemoryPlan(user2.id, "free", null);
|
|
48478
|
-
await syncMemoryKeyPlan(user2, resolveEffectiveMemoryPlan({ ...user2, memory_plan: "free" }));
|
|
48479
|
-
}
|
|
48480
|
-
}
|
|
48481
|
-
return c.json({ received: true });
|
|
48482
|
-
}
|
|
48483
|
-
if (priceId && priceId in MEMORY_PLANS || await resolveUserByMemorySubscription(sub.id)) {
|
|
48484
|
-
const user2 = await resolveUserByMemorySubscription(sub.id) ?? await getUserByStripeCustomerId(sub.customer);
|
|
48485
|
-
if (user2) {
|
|
48486
|
-
await setMemoryPlan(user2.id, "free", null);
|
|
48487
|
-
await syncMemoryKeyPlan(user2, resolveEffectiveMemoryPlan({ ...user2, memory_plan: "free" }));
|
|
48488
|
-
}
|
|
48489
|
-
return c.json({ received: true });
|
|
48490
|
-
}
|
|
48491
|
-
const user = await getUserByStripeCustomerId(sub.customer);
|
|
48492
|
-
if (!user) return c.json({ received: true });
|
|
48493
|
-
if (priceId === CONCURRENCY_PRICE_ID) {
|
|
48494
|
-
await setExtraConcurrencySlots(user.id, Math.max(0, user.extra_concurrency_slots - 1));
|
|
48495
|
-
await setConcurrencySubId(user.id, null);
|
|
48496
|
-
}
|
|
48960
|
+
const claim = await claimStripeEvent(event.id);
|
|
48961
|
+
if (claim.state !== "claimed") return c.json({ received: true });
|
|
48962
|
+
try {
|
|
48963
|
+
await processStripeEvent(event);
|
|
48964
|
+
const completed = await completeStripeEvent(event.id, claim.token);
|
|
48965
|
+
if (!completed) throw new Error("Stripe event claim was superseded before completion");
|
|
48966
|
+
return c.json({ received: true });
|
|
48967
|
+
} catch (error) {
|
|
48968
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
48969
|
+
await failStripeEvent(event.id, claim.token, message).catch(() => false);
|
|
48970
|
+
console.error("[stripe] webhook processing failed", event.id, message);
|
|
48971
|
+
return c.json({ error: "Webhook processing failed" }, 500);
|
|
48497
48972
|
}
|
|
48498
|
-
return c.json({ received: true });
|
|
48499
48973
|
});
|
|
48500
48974
|
}
|
|
48501
48975
|
});
|
|
@@ -55168,6 +55642,7 @@ function opForCostPath(p) {
|
|
|
55168
55642
|
if (p.startsWith("/facebook/")) {
|
|
55169
55643
|
if (p.endsWith("transcribe")) return "fb_transcribe";
|
|
55170
55644
|
if (p.endsWith("/search")) return "fb_search";
|
|
55645
|
+
if (p.endsWith("/page-intel")) return "fb_page_intel";
|
|
55171
55646
|
return "fb_ad";
|
|
55172
55647
|
}
|
|
55173
55648
|
if (p.startsWith("/instagram/")) return "instagram";
|