mcp-scraper 0.38.1 → 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 +718 -276
- 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-C5UGV7RM.js → chunk-EGJKUB4Q.js} +3 -3
- package/dist/{chunk-C5UGV7RM.js.map → chunk-EGJKUB4Q.js.map} +1 -1
- package/dist/{chunk-WS2E7HA5.js → chunk-FRYT3ID4.js} +2 -2
- package/dist/{chunk-XKCZOPZN.js → chunk-N7KUTTCC.js} +235 -40
- package/dist/chunk-N7KUTTCC.js.map +1 -0
- package/dist/{chunk-BSD2Z5A6.js → chunk-NGM237OO.js} +66 -5
- package/dist/chunk-NGM237OO.js.map +1 -0
- package/dist/{chunk-YAW3AUTP.js → chunk-NKCCGADE.js} +23 -8
- package/dist/chunk-NKCCGADE.js.map +1 -0
- package/dist/{chunk-OLRRIA4H.js → chunk-SFRMFGQ6.js} +2 -2
- package/dist/{chunk-4YAFJWFG.js → chunk-YCI2PNCS.js} +3 -3
- package/dist/chunk-YODBNTTN.js +7 -0
- package/dist/chunk-YODBNTTN.js.map +1 -0
- package/dist/{db-JRFMBTRD.js → db-C5KVCOYT.js} +8 -4
- package/dist/{extract-bundle-DSFOHMAT.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-OBJYZEGE.js → location-data-repository-TTWF3OTM.js} +3 -3
- package/dist/{server-2UDB6PTC.js → server-5EX6XBIA.js} +312 -151
- package/dist/server-5EX6XBIA.js.map +1 -0
- package/dist/{site-extract-repository-2YJ33J5C.js → site-extract-repository-XSPJTCIL.js} +4 -4
- package/dist/{worker-ESF3HCNJ.js → worker-XCPU4YSN.js} +6 -6
- package/docs/mcp-tool-manifest.generated.json +66 -9
- package/package.json +1 -1
- package/dist/chunk-BSD2Z5A6.js.map +0 -1
- package/dist/chunk-FFBPWVWC.js +0 -7
- package/dist/chunk-FFBPWVWC.js.map +0 -1
- package/dist/chunk-PWPUKR5U.js.map +0 -1
- package/dist/chunk-XKCZOPZN.js.map +0 -1
- package/dist/chunk-YAW3AUTP.js.map +0 -1
- package/dist/server-2UDB6PTC.js.map +0 -1
- /package/dist/{chunk-WS2E7HA5.js.map → chunk-FRYT3ID4.js.map} +0 -0
- /package/dist/{chunk-OLRRIA4H.js.map → chunk-SFRMFGQ6.js.map} +0 -0
- /package/dist/{chunk-4YAFJWFG.js.map → chunk-YCI2PNCS.js.map} +0 -0
- /package/dist/{db-JRFMBTRD.js.map → db-C5KVCOYT.js.map} +0 -0
- /package/dist/{extract-bundle-DSFOHMAT.js.map → extract-bundle-KUBX6N6Z.js.map} +0 -0
- /package/dist/{location-data-repository-OBJYZEGE.js.map → location-data-repository-TTWF3OTM.js.map} +0 -0
- /package/dist/{site-extract-repository-2YJ33J5C.js.map → site-extract-repository-XSPJTCIL.js.map} +0 -0
- /package/dist/{worker-ESF3HCNJ.js.map → worker-XCPU4YSN.js.map} +0 -0
package/dist/bin/api-server.cjs
CHANGED
|
@@ -3622,8 +3622,10 @@ __export(db_exports, {
|
|
|
3622
3622
|
claimMonthlyFreeRefresh: () => claimMonthlyFreeRefresh,
|
|
3623
3623
|
claimPendingJob: () => claimPendingJob,
|
|
3624
3624
|
claimPendingKpoJob: () => claimPendingKpoJob,
|
|
3625
|
+
claimStripeEvent: () => claimStripeEvent,
|
|
3625
3626
|
completeJob: () => completeJob,
|
|
3626
3627
|
completeKpoJob: () => completeKpoJob,
|
|
3628
|
+
completeStripeEvent: () => completeStripeEvent,
|
|
3627
3629
|
completeWorkflowRunRecord: () => completeWorkflowRunRecord,
|
|
3628
3630
|
consumeCode: () => consumeCode,
|
|
3629
3631
|
consumePasswordResetToken: () => consumePasswordResetToken,
|
|
@@ -3645,6 +3647,7 @@ __export(db_exports, {
|
|
|
3645
3647
|
expireOldLots: () => expireOldLots,
|
|
3646
3648
|
failJob: () => failJob,
|
|
3647
3649
|
failKpoJob: () => failKpoJob,
|
|
3650
|
+
failStripeEvent: () => failStripeEvent,
|
|
3648
3651
|
failWorkflowRunRecord: () => failWorkflowRunRecord,
|
|
3649
3652
|
finishHarvestAttempt: () => finishHarvestAttempt,
|
|
3650
3653
|
generateApiKey: () => generateApiKey,
|
|
@@ -3699,7 +3702,6 @@ __export(db_exports, {
|
|
|
3699
3702
|
putCode: () => putCode,
|
|
3700
3703
|
putRefresh: () => putRefresh,
|
|
3701
3704
|
reconcileBalanceMc: () => reconcileBalanceMc,
|
|
3702
|
-
recordStripeEvent: () => recordStripeEvent,
|
|
3703
3705
|
recordWorkflowWebhookDelivery: () => recordWorkflowWebhookDelivery,
|
|
3704
3706
|
registerClient: () => registerClient,
|
|
3705
3707
|
replaceWorkflowArtifacts: () => replaceWorkflowArtifacts,
|
|
@@ -3733,6 +3735,7 @@ async function hasCurrentCoreSchema(db) {
|
|
|
3733
3735
|
const checks = [
|
|
3734
3736
|
`(SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name IN (${sqlStringList(CORE_SCHEMA_TABLES)})) = ${CORE_SCHEMA_TABLES.length}`,
|
|
3735
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}`,
|
|
3736
3739
|
...Object.entries(CORE_SCHEMA_COLUMNS).map(
|
|
3737
3740
|
([table, columns]) => `(SELECT COUNT(*) FROM pragma_table_info('${table}') WHERE name IN (${sqlStringList(columns)})) = ${columns.length}`
|
|
3738
3741
|
)
|
|
@@ -4008,6 +4011,61 @@ async function migrate() {
|
|
|
4008
4011
|
} catch {
|
|
4009
4012
|
}
|
|
4010
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
|
+
`);
|
|
4011
4069
|
await db.execute(`
|
|
4012
4070
|
CREATE TABLE IF NOT EXISTS credit_lot_repairs (
|
|
4013
4071
|
repair_id TEXT PRIMARY KEY,
|
|
@@ -4397,10 +4455,37 @@ async function migrate() {
|
|
|
4397
4455
|
await db.execute(`CREATE INDEX IF NOT EXISTS workflow_webhook_deliveries_run ON workflow_webhook_deliveries(run_id)`);
|
|
4398
4456
|
await db.execute(`
|
|
4399
4457
|
CREATE TABLE IF NOT EXISTS stripe_events (
|
|
4400
|
-
event_id
|
|
4401
|
-
|
|
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'))
|
|
4402
4465
|
)
|
|
4403
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
|
+
`);
|
|
4404
4489
|
await db.execute(`
|
|
4405
4490
|
CREATE TABLE IF NOT EXISTS oauth_clients (
|
|
4406
4491
|
client_id TEXT PRIMARY KEY,
|
|
@@ -4566,16 +4651,60 @@ async function rotateRefresh(oldToken, next) {
|
|
|
4566
4651
|
args: [next.refresh_token, (/* @__PURE__ */ new Date()).toISOString(), oldToken]
|
|
4567
4652
|
});
|
|
4568
4653
|
}
|
|
4569
|
-
async function
|
|
4570
|
-
const
|
|
4571
|
-
|
|
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",
|
|
4572
4676
|
args: [eventId]
|
|
4573
4677
|
});
|
|
4574
|
-
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;
|
|
4575
4704
|
}
|
|
4576
4705
|
async function stripeEventAlreadyProcessed(eventId) {
|
|
4577
4706
|
const res = await getDb().execute({
|
|
4578
|
-
sql:
|
|
4707
|
+
sql: `SELECT 1 FROM stripe_events WHERE event_id = ? AND status = 'processed' LIMIT 1`,
|
|
4579
4708
|
args: [eventId]
|
|
4580
4709
|
});
|
|
4581
4710
|
return res.rows.length > 0;
|
|
@@ -5578,23 +5707,64 @@ async function ensureMigrated(userId) {
|
|
|
5578
5707
|
async function expireOldLots() {
|
|
5579
5708
|
const db = getDb();
|
|
5580
5709
|
const lots = await db.execute({
|
|
5581
|
-
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`
|
|
5582
5716
|
});
|
|
5583
5717
|
if (lots.rows.length === 0) return { expired_lots: 0, expired_mc: 0 };
|
|
5584
|
-
|
|
5585
|
-
const users = /* @__PURE__ */ new Set();
|
|
5718
|
+
const claims = [];
|
|
5586
5719
|
const stmts = [];
|
|
5587
5720
|
for (const lot of lots.rows) {
|
|
5588
|
-
const
|
|
5589
|
-
const
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
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)));
|
|
5596
5763
|
for (const uid of users) await reconcileBalanceMc(uid);
|
|
5597
|
-
return {
|
|
5764
|
+
return {
|
|
5765
|
+
expired_lots: expired.rows.length,
|
|
5766
|
+
expired_mc: expired.rows.reduce((sum, row) => sum + Number(row.amount_mc), 0)
|
|
5767
|
+
};
|
|
5598
5768
|
}
|
|
5599
5769
|
async function migrateExistingBalancesToLots() {
|
|
5600
5770
|
const db = getDb();
|
|
@@ -6184,29 +6354,46 @@ async function ledgerExistsForStripePI(stripePI) {
|
|
|
6184
6354
|
}
|
|
6185
6355
|
async function reconcileBalanceMc(userId, knownBalanceMc) {
|
|
6186
6356
|
const db = getDb();
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
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`,
|
|
6199
6371
|
args: [userId]
|
|
6200
6372
|
});
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
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]
|
|
6207
6389
|
});
|
|
6390
|
+
if (updated.rowsAffected === 1) return projectedBalanceMc;
|
|
6208
6391
|
}
|
|
6209
|
-
|
|
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);
|
|
6210
6397
|
}
|
|
6211
6398
|
async function listInboxMessages(userId) {
|
|
6212
6399
|
const res = await getDb().execute({
|
|
@@ -6269,7 +6456,7 @@ async function listActiveUserIds() {
|
|
|
6269
6456
|
const res = await getDb().execute("SELECT id FROM users WHERE active = 1");
|
|
6270
6457
|
return res.rows.map((r) => Number(r.id));
|
|
6271
6458
|
}
|
|
6272
|
-
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;
|
|
6273
6460
|
var init_db = __esm({
|
|
6274
6461
|
"src/api/db.ts"() {
|
|
6275
6462
|
"use strict";
|
|
@@ -6282,7 +6469,7 @@ var init_db = __esm({
|
|
|
6282
6469
|
DB_TOKEN = process.env.TURSO_AUTH_TOKEN;
|
|
6283
6470
|
_db = null;
|
|
6284
6471
|
_rateLimitSchemaReady = false;
|
|
6285
|
-
CORE_SCHEMA_VERSION = "2026-07-28.
|
|
6472
|
+
CORE_SCHEMA_VERSION = "2026-07-28.3";
|
|
6286
6473
|
CORE_SCHEMA_TABLES = [
|
|
6287
6474
|
"admin_credit_adjustment_events",
|
|
6288
6475
|
"admin_credit_adjustments",
|
|
@@ -6294,6 +6481,7 @@ var init_db = __esm({
|
|
|
6294
6481
|
"browser_profile_labels",
|
|
6295
6482
|
"connected_account_billing",
|
|
6296
6483
|
"concurrency_locks",
|
|
6484
|
+
"credit_lot_expirations",
|
|
6297
6485
|
"credit_lots",
|
|
6298
6486
|
"credit_lot_repairs",
|
|
6299
6487
|
"free_credit_refreshes",
|
|
@@ -6362,6 +6550,12 @@ var init_db = __esm({
|
|
|
6362
6550
|
"workflow_schedules_user_status",
|
|
6363
6551
|
"workflow_webhook_deliveries_run"
|
|
6364
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
|
+
];
|
|
6365
6559
|
CORE_SCHEMA_COLUMNS = {
|
|
6366
6560
|
credit_lots: ["never_expires"],
|
|
6367
6561
|
harvest_attempts: ["debug_json"],
|
|
@@ -6379,6 +6573,7 @@ var init_db = __esm({
|
|
|
6379
6573
|
"next_settlement_at",
|
|
6380
6574
|
"settlement_error"
|
|
6381
6575
|
],
|
|
6576
|
+
stripe_events: ["attempt_count", "claim_token", "last_error", "status", "updated_at"],
|
|
6382
6577
|
users: [
|
|
6383
6578
|
"balance_mc",
|
|
6384
6579
|
"concurrency_stripe_sub_id",
|
|
@@ -6470,7 +6665,7 @@ var init_cost_rates = __esm({
|
|
|
6470
6665
|
"use strict";
|
|
6471
6666
|
KERNEL_HEADLESS_USD_PER_SEC = 166667e-10;
|
|
6472
6667
|
KERNEL_HEADFUL_USD_PER_SEC = 1333336e-10;
|
|
6473
|
-
HEADLESS_OPS = /* @__PURE__ */ new Set(["serp", "
|
|
6668
|
+
HEADLESS_OPS = /* @__PURE__ */ new Set(["serp", "fb_search", "fb_ad", "instagram"]);
|
|
6474
6669
|
FAL_WIZPER_USD_PER_COMPUTE_SEC = 111e-5;
|
|
6475
6670
|
FAL_WIZPER_WALLCLOCK_TO_COMPUTE_SEC_FACTOR = 4;
|
|
6476
6671
|
DEEPINFRA_QWEN_USD_PER_1K_OUT_TOKENS = 4e-4;
|
|
@@ -8066,12 +8261,15 @@ async function spiderSite(opts) {
|
|
|
8066
8261
|
const discovered = { url, status, linkedFrom: from, via: "fetch" };
|
|
8067
8262
|
const children = [];
|
|
8068
8263
|
if (html) {
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
visited.
|
|
8073
|
-
|
|
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
|
+
}
|
|
8074
8271
|
}
|
|
8272
|
+
} catch {
|
|
8075
8273
|
}
|
|
8076
8274
|
}
|
|
8077
8275
|
return { discovered, children };
|
|
@@ -10051,67 +10249,78 @@ async function extractSite(opts) {
|
|
|
10051
10249
|
let truncated = false;
|
|
10052
10250
|
let spider;
|
|
10053
10251
|
let sitemapUrls = [];
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
10061
|
-
const
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
|
|
10067
|
-
if (seeded) {
|
|
10068
|
-
for (const u of opts.seedUrls) enqueue(u);
|
|
10069
|
-
} else {
|
|
10070
|
-
enqueue(opts.startUrl);
|
|
10071
|
-
sitemapUrls = await discoverSitemapUrls2(opts.startUrl, maxPages);
|
|
10072
|
-
for (const u of sitemapUrls) enqueue(u);
|
|
10073
|
-
}
|
|
10074
|
-
const waveCap = Math.max(browserConcurrency * urlsPerBrowser, 30);
|
|
10075
|
-
let done = 0;
|
|
10076
|
-
while (frontier.length > 0 && pages.length < maxPages) {
|
|
10077
|
-
const capacity = maxPages - pages.length;
|
|
10078
|
-
const wave = frontier.splice(0, Math.min(capacity, waveCap));
|
|
10079
|
-
const fetched = await crawlWithRotation(wave, {
|
|
10080
|
-
apiKey: opts.kernelApiKey,
|
|
10081
|
-
concurrency: browserConcurrency,
|
|
10082
|
-
urlsPerBrowser,
|
|
10083
|
-
onPage: (p) => {
|
|
10084
|
-
done++;
|
|
10085
|
-
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);
|
|
10086
10265
|
}
|
|
10087
|
-
}
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
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
|
+
}
|
|
10093
10287
|
});
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
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
|
+
}
|
|
10098
10299
|
}
|
|
10099
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;
|
|
10100
10314
|
}
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
await runWithConcurrency(urlsToExtract, concurrency, async (url) => {
|
|
10109
|
-
const data = await fetchAndParse(url, opts.kernelApiKey);
|
|
10110
|
-
if (data.via === "browser") browserRetries++;
|
|
10111
|
-
pages.push(data);
|
|
10112
|
-
});
|
|
10113
|
-
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;
|
|
10114
10322
|
}
|
|
10323
|
+
if (!spider) spider = makeSeedSpider(opts.startUrl, pages.map((p) => p.url));
|
|
10115
10324
|
const sitemapSet = sitemapUrls.length > 0 ? new Set(sitemapUrls.map((u) => normalizeUrl(u, opts.startUrl) ?? u)) : null;
|
|
10116
10325
|
for (const p of pages) p.inSitemap = sitemapSet ? sitemapSet.has(normalizeUrl(p.url, opts.startUrl) ?? p.url) : null;
|
|
10117
10326
|
let branding = null;
|
|
@@ -10121,10 +10330,11 @@ async function extractSite(opts) {
|
|
|
10121
10330
|
const seoAudit = opts.formats?.includes("issues") ? buildSeoAudit(pages, opts.startUrl) : null;
|
|
10122
10331
|
let imageAudit = null;
|
|
10123
10332
|
if (opts.formats?.includes("images")) {
|
|
10124
|
-
imageAudit = await auditImages(pages, { concurrency: 12, timeoutMs: 12e3 });
|
|
10333
|
+
imageAudit = await auditImages(pages, { concurrency: 12, timeoutMs: 12e3 }).catch(() => null);
|
|
10125
10334
|
}
|
|
10126
10335
|
return {
|
|
10127
10336
|
startUrl: opts.startUrl,
|
|
10337
|
+
stoppedEarly,
|
|
10128
10338
|
pages,
|
|
10129
10339
|
totalPages: pages.length,
|
|
10130
10340
|
spider,
|
|
@@ -10928,11 +11138,14 @@ var init_rates = __esm({
|
|
|
10928
11138
|
url_map: 500,
|
|
10929
11139
|
yt_channel: 200,
|
|
10930
11140
|
yt_transcription: 200,
|
|
10931
|
-
fb_ad:
|
|
11141
|
+
fb_ad: 700,
|
|
10932
11142
|
maps_search: 500,
|
|
10933
11143
|
maps_place: 6e3,
|
|
10934
11144
|
maps_review: 100,
|
|
10935
|
-
fb_search:
|
|
11145
|
+
fb_search: 800,
|
|
11146
|
+
fb_search_result: 100,
|
|
11147
|
+
fb_page_intel: 700,
|
|
11148
|
+
fb_page_intel_ad: 50,
|
|
10936
11149
|
fb_transcribe: 33300,
|
|
10937
11150
|
google_ads_search: 600,
|
|
10938
11151
|
google_ads_intel: 200,
|
|
@@ -11311,6 +11524,12 @@ var init_rates = __esm({
|
|
|
11311
11524
|
FB_SEARCH: "fb_search",
|
|
11312
11525
|
FB_TRANSCRIBE: "fb_transcribe",
|
|
11313
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",
|
|
11314
11533
|
FB_TRANSCRIBE_REFUND: "fb_transcribe_refund",
|
|
11315
11534
|
GOOGLE_ADS_SEARCH: "google_ads_search",
|
|
11316
11535
|
GOOGLE_ADS_SEARCH_REFUND: "google_ads_search_refund",
|
|
@@ -16262,8 +16481,12 @@ async function formatExtractSite(raw, input, ctx) {
|
|
|
16262
16481
|
schemaTypes: schemaTypesOf(p)
|
|
16263
16482
|
})));
|
|
16264
16483
|
const preview = pages.slice(0, BULK_PAGE_THRESHOLD).map(pageRow).join("\n");
|
|
16484
|
+
const stoppedEarly = d.stoppedEarly;
|
|
16265
16485
|
const summaryHeader = [
|
|
16266
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
|
+
` : "",
|
|
16267
16490
|
durationLine,
|
|
16268
16491
|
`
|
|
16269
16492
|
## Pages (first ${Math.min(BULK_PAGE_THRESHOLD, pages.length)} of ${pages.length})
|
|
@@ -18805,8 +19028,10 @@ var init_BrowserDriver = __esm({
|
|
|
18805
19028
|
return this.kernelClient ? "Google returned a CAPTCHA on this session \u2014 retrying with a fresh session." : RECAPTCHA_INSTRUCTIONS;
|
|
18806
19029
|
}
|
|
18807
19030
|
async navigateTo(url) {
|
|
19031
|
+
const arrivalBase = process.env.ADS_ARRIVAL_REDIRECT_BASE?.trim();
|
|
19032
|
+
const navUrl = arrivalBase ? serpArrivalUrl(url, arrivalBase) : url;
|
|
18808
19033
|
try {
|
|
18809
|
-
await this.page.goto(
|
|
19034
|
+
await this.page.goto(navUrl, { waitUntil: "domcontentloaded", timeout: 45e3 });
|
|
18810
19035
|
if (/google\.[^/]+\/search\?/i.test(url) && await this.isSerpChallengeVisible()) {
|
|
18811
19036
|
throw new CaptchaError(this.captchaMessage());
|
|
18812
19037
|
}
|
|
@@ -19022,6 +19247,30 @@ function closeOpenBusinessPanelDialogInBrowser(expectedHeading) {
|
|
|
19022
19247
|
close?.click();
|
|
19023
19248
|
return Boolean(close);
|
|
19024
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
|
+
}
|
|
19025
19274
|
async function clickPanelRowWithRetry(page, businessName, prefix) {
|
|
19026
19275
|
const input = { businessName, prefix };
|
|
19027
19276
|
if (await page.evaluate(clickBusinessPanelRowInBrowser, input)) return true;
|
|
@@ -19035,9 +19284,12 @@ async function extractExpandedList(page, businessName, rowPrefix, dialogHeading)
|
|
|
19035
19284
|
extractOpenBusinessPanelDialogItemsInBrowser,
|
|
19036
19285
|
dialogHeading
|
|
19037
19286
|
);
|
|
19038
|
-
|
|
19039
|
-
|
|
19040
|
-
|
|
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 });
|
|
19041
19293
|
}
|
|
19042
19294
|
async function extractExpandedGoogleBusinessPanelServices(page, businessName) {
|
|
19043
19295
|
const services = await extractExpandedList(page, businessName, "Services:", "Services");
|
|
@@ -19114,7 +19366,7 @@ var init_MapsSearchExtractor = __esm({
|
|
|
19114
19366
|
await page.waitForTimeout(LOCAL_RESULTS_WAIT_MS);
|
|
19115
19367
|
if (await this.detectBlock(page)) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
|
|
19116
19368
|
const reachedFinder = await this.clickIntoLocalFinder(page);
|
|
19117
|
-
if (!reachedFinder) throw new
|
|
19369
|
+
if (!reachedFinder) throw new MapsSearchIncompletePageError('Google "More businesses" control was not found on the organic results page');
|
|
19118
19370
|
await page.waitForTimeout(LOCAL_RESULTS_WAIT_MS);
|
|
19119
19371
|
if (await this.detectBlock(page)) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
|
|
19120
19372
|
const results = await this.collectResults(page, options);
|
|
@@ -20100,7 +20352,8 @@ function shouldRetryMapsSearchError(input) {
|
|
|
20100
20352
|
if (input.attemptIndex >= input.maxAttempts - 1) return false;
|
|
20101
20353
|
if (!retryableMapsSearchError(input.err, input.proxyMode)) return false;
|
|
20102
20354
|
if (input.err instanceof MapsSearchIncompletePageError) {
|
|
20103
|
-
|
|
20355
|
+
const unconfirmed = input.priorAttempts.filter((attempt) => attempt.outcome === "results_unconfirmed").length;
|
|
20356
|
+
return unconfirmed < UNCONFIRMED_PAGE_MAX_RETRIES;
|
|
20104
20357
|
}
|
|
20105
20358
|
return true;
|
|
20106
20359
|
}
|
|
@@ -20281,7 +20534,7 @@ async function runMapsSearchWithRotation(options) {
|
|
|
20281
20534
|
true
|
|
20282
20535
|
);
|
|
20283
20536
|
}
|
|
20284
|
-
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;
|
|
20285
20538
|
var init_maps_search_rotation = __esm({
|
|
20286
20539
|
"src/maps-search-rotation.ts"() {
|
|
20287
20540
|
"use strict";
|
|
@@ -20291,6 +20544,7 @@ var init_maps_search_rotation = __esm({
|
|
|
20291
20544
|
init_kernel_proxy_resolver();
|
|
20292
20545
|
init_backup_proxy();
|
|
20293
20546
|
init_cost_context();
|
|
20547
|
+
UNCONFIRMED_PAGE_MAX_RETRIES = 5;
|
|
20294
20548
|
MAPS_SEARCH_MAX_ATTEMPTS = 3;
|
|
20295
20549
|
MAPS_SEARCH_NONE_MAX_ATTEMPTS = 7;
|
|
20296
20550
|
MAPS_SEARCH_LOCATION_PROXY_MAX_ATTEMPTS = 5;
|
|
@@ -25823,6 +26077,7 @@ async function resolveYouTubeAudio(videoId) {
|
|
|
25823
26077
|
if (!base || !secret2) return null;
|
|
25824
26078
|
const controller = new AbortController();
|
|
25825
26079
|
const timer = setTimeout(() => controller.abort(), RESOLVE_TIMEOUT_MS);
|
|
26080
|
+
const startedMs = Date.now();
|
|
25826
26081
|
try {
|
|
25827
26082
|
const resp = await fetch(`${base.replace(/\/+$/, "")}/resolve-audio`, {
|
|
25828
26083
|
method: "POST",
|
|
@@ -25840,6 +26095,12 @@ async function resolveYouTubeAudio(videoId) {
|
|
|
25840
26095
|
console.warn(`[audio-resolver] ${videoId}: response missing mediaUrl`);
|
|
25841
26096
|
return null;
|
|
25842
26097
|
}
|
|
26098
|
+
void recordVendorUsage({
|
|
26099
|
+
vendor: "frames_audio_resolve",
|
|
26100
|
+
model: null,
|
|
26101
|
+
units: (Date.now() - startedMs) / 1e3,
|
|
26102
|
+
unitType: "compute_sec"
|
|
26103
|
+
});
|
|
25843
26104
|
return {
|
|
25844
26105
|
mediaUrl: data.mediaUrl,
|
|
25845
26106
|
durationS: Number(data.durationS) || 0,
|
|
@@ -25858,6 +26119,7 @@ var RESOLVE_TIMEOUT_MS;
|
|
|
25858
26119
|
var init_audio_resolver = __esm({
|
|
25859
26120
|
"src/youtube/audio-resolver.ts"() {
|
|
25860
26121
|
"use strict";
|
|
26122
|
+
init_cost_telemetry();
|
|
25861
26123
|
RESOLVE_TIMEOUT_MS = 24e4;
|
|
25862
26124
|
}
|
|
25863
26125
|
});
|
|
@@ -27430,10 +27692,20 @@ function isAllowedFacebookOrganicUrl(url) {
|
|
|
27430
27692
|
async function transcribeFacebookVideoUrl(videoUrl, markdownTitle = "# Facebook Video Transcript") {
|
|
27431
27693
|
return transcribeMediaUrl(videoUrl, markdownTitle);
|
|
27432
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
|
+
}
|
|
27433
27705
|
async function detectSoftBlock(driver) {
|
|
27434
27706
|
const page = driver.getPage();
|
|
27435
27707
|
const bodyText = await page.evaluate(() => document.body?.innerText ?? "").catch(() => "");
|
|
27436
|
-
return bodyText.
|
|
27708
|
+
return classifyAdLibraryPage(bodyText, page.url()) === "blocked";
|
|
27437
27709
|
}
|
|
27438
27710
|
function buildPageIntelUrl(body, country) {
|
|
27439
27711
|
if (body.libraryId?.trim()) return `https://www.facebook.com/ads/library/?id=${FacebookAdExtractor.resolveLibraryId(body.libraryId.trim()) ?? body.libraryId.trim()}`;
|
|
@@ -27509,7 +27781,7 @@ async function kernelLaunchOptsResidential() {
|
|
|
27509
27781
|
}
|
|
27510
27782
|
return { headless: true, kernelApiKey: browserServiceApiKey(), kernelProxyId: proxyId, viewport: { width: 1280, height: 900 }, locale: "en-US" };
|
|
27511
27783
|
}
|
|
27512
|
-
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;
|
|
27513
27785
|
var init_facebook_ad_routes = __esm({
|
|
27514
27786
|
"src/api/facebook-ad-routes.ts"() {
|
|
27515
27787
|
"use strict";
|
|
@@ -27559,6 +27831,9 @@ var init_facebook_ad_routes = __esm({
|
|
|
27559
27831
|
url: import_zod21.z.string().trim().min(1, "url is required"),
|
|
27560
27832
|
filename: import_zod21.z.string().trim().optional()
|
|
27561
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;
|
|
27562
27837
|
facebookAdApp = new import_hono7.Hono();
|
|
27563
27838
|
facebookAdApp.post("/ad", createApiKeyAuth(), async (c) => {
|
|
27564
27839
|
const raw = await c.req.json().catch(() => ({}));
|
|
@@ -27619,26 +27894,27 @@ var init_facebook_ad_routes = __esm({
|
|
|
27619
27894
|
const driver = new BrowserDriver();
|
|
27620
27895
|
let refunded = false, debited = false;
|
|
27621
27896
|
try {
|
|
27622
|
-
const { ok: fbOk, balance_mc: fbBal } = await debitMc(fbUser.id, MC_COSTS.
|
|
27623
|
-
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);
|
|
27624
27899
|
debited = true;
|
|
27625
27900
|
await driver.launch(await kernelLaunchOptsResidential());
|
|
27626
27901
|
await driver.navigateTo(listingUrl);
|
|
27627
27902
|
const extractor = new FacebookAdExtractor(driver);
|
|
27628
27903
|
const result = await extractor.extractPageIntel(listingUrl, maxAds);
|
|
27629
|
-
if (result.ads.length === 0
|
|
27630
|
-
await creditMc(fbUser.id, MC_COSTS.fb_ad, LedgerOperation.FB_AD_REFUND, "soft-block empty result");
|
|
27904
|
+
if (result.ads.length === 0) {
|
|
27631
27905
|
refunded = true;
|
|
27632
|
-
await
|
|
27633
|
-
return c.json({ error: "soft-block: no ads returned (refunded)" }, 503);
|
|
27906
|
+
return await refundEmptyPageIntel(c, fbUser.id, subject, await detectSoftBlock(driver));
|
|
27634
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);
|
|
27635
27911
|
const match = assessPageIntelMatch(body, result);
|
|
27636
27912
|
const output = { ...result, ...match };
|
|
27637
27913
|
await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "done", query: subject, resultCount: result.ads.length, result: output });
|
|
27638
27914
|
return c.json(output);
|
|
27639
27915
|
} catch (err) {
|
|
27640
27916
|
const msg = err instanceof Error ? err.message : String(err);
|
|
27641
|
-
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");
|
|
27642
27918
|
await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "failed", query: subject, error: msg });
|
|
27643
27919
|
if (msg.toLowerCase().includes("blocked") || msg.toLowerCase().includes("captcha")) {
|
|
27644
27920
|
return c.json({ error: msg }, 503);
|
|
@@ -27779,10 +28055,16 @@ var init_facebook_ad_routes = __esm({
|
|
|
27779
28055
|
debited = true;
|
|
27780
28056
|
await driver.launch(await kernelLaunchOptsResidential());
|
|
27781
28057
|
const page = driver.getPage();
|
|
27782
|
-
|
|
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
|
+
}
|
|
27783
28062
|
const gqlAdvertisers = advertisersFromResults(collated, maxResults);
|
|
27784
28063
|
if (gqlAdvertisers.length > 0) {
|
|
27785
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);
|
|
27786
28068
|
const searchResult2 = { query: body.query.trim(), searchUrl, results: results2, via: "graphql" };
|
|
27787
28069
|
await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "done", query: body.query.trim(), resultCount: results2.length, result: searchResult2 });
|
|
27788
28070
|
return c.json(searchResult2);
|
|
@@ -27795,7 +28077,7 @@ var init_facebook_ad_routes = __esm({
|
|
|
27795
28077
|
await page.waitForTimeout(1e3);
|
|
27796
28078
|
}
|
|
27797
28079
|
const rawBodyText = await page.evaluate(() => document.body?.innerText ?? "");
|
|
27798
|
-
if (rawBodyText.
|
|
28080
|
+
if (classifyAdLibraryPage(rawBodyText, page.url()) === "blocked") {
|
|
27799
28081
|
await creditMc(fbUser.id, MC_COSTS.fb_search, LedgerOperation.FB_SEARCH_REFUND, "soft-block empty result");
|
|
27800
28082
|
searchRefunded = true;
|
|
27801
28083
|
await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "failed", query: body.query.trim(), error: "soft-block: empty result refunded" });
|
|
@@ -27836,8 +28118,11 @@ var init_facebook_ad_routes = __esm({
|
|
|
27836
28118
|
await creditMc(fbUser.id, MC_COSTS.fb_search, LedgerOperation.FB_SEARCH_REFUND, "empty result");
|
|
27837
28119
|
searchRefunded = true;
|
|
27838
28120
|
await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "failed", query: body.query.trim(), error: "empty result refunded" });
|
|
27839
|
-
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);
|
|
27840
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);
|
|
27841
28126
|
const searchResult = { query: body.query.trim(), searchUrl, results };
|
|
27842
28127
|
await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "done", query: body.query.trim(), resultCount: results.length, result: searchResult });
|
|
27843
28128
|
return c.json(searchResult);
|
|
@@ -27903,9 +28188,6 @@ var init_facebook_ad_routes = __esm({
|
|
|
27903
28188
|
});
|
|
27904
28189
|
|
|
27905
28190
|
// src/extractor/GoogleAdsExtractor.ts
|
|
27906
|
-
function looksLikeDomain(q) {
|
|
27907
|
-
return /^[a-z0-9.-]+\.[a-z]{2,}$/i.test(q.trim()) && !q.includes(" ");
|
|
27908
|
-
}
|
|
27909
28191
|
var ORIGIN, GoogleAdsExtractor;
|
|
27910
28192
|
var init_GoogleAdsExtractor = __esm({
|
|
27911
28193
|
"src/extractor/GoogleAdsExtractor.ts"() {
|
|
@@ -27921,50 +28203,108 @@ var init_GoogleAdsExtractor = __esm({
|
|
|
27921
28203
|
}
|
|
27922
28204
|
async search(query, region, maxResults) {
|
|
27923
28205
|
const q = query.trim();
|
|
27924
|
-
|
|
27925
|
-
await this.driver.navigateTo(url);
|
|
28206
|
+
await this.driver.navigateTo(`${ORIGIN}/?region=${encodeURIComponent(region)}`);
|
|
27926
28207
|
const page = this.page();
|
|
27927
|
-
await page.waitForTimeout(
|
|
27928
|
-
await
|
|
27929
|
-
|
|
27930
|
-
|
|
27931
|
-
|
|
27932
|
-
|
|
27933
|
-
|
|
27934
|
-
|
|
27935
|
-
|
|
27936
|
-
const
|
|
27937
|
-
const
|
|
27938
|
-
|
|
27939
|
-
|
|
27940
|
-
|
|
27941
|
-
|
|
27942
|
-
|
|
27943
|
-
|
|
27944
|
-
|
|
27945
|
-
|
|
27946
|
-
|
|
27947
|
-
|
|
27948
|
-
|
|
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
|
+
}));
|
|
27949
28244
|
return {
|
|
27950
28245
|
query: q,
|
|
27951
28246
|
region,
|
|
27952
28247
|
advertiserCount: advertisers.length,
|
|
27953
|
-
advertisers
|
|
27954
|
-
|
|
27955
|
-
|
|
27956
|
-
domain: looksLikeDomain(q) ? q : null,
|
|
27957
|
-
approxAdCount: a.approxAdCount,
|
|
27958
|
-
detailUrl: `${ORIGIN}/advertiser/${a.advertiserId}?region=${region}`
|
|
27959
|
-
}))
|
|
28248
|
+
advertisers,
|
|
28249
|
+
websiteCount: websites.length,
|
|
28250
|
+
websites
|
|
27960
28251
|
};
|
|
27961
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
|
+
}
|
|
27962
28300
|
async pageIntel(opts) {
|
|
27963
28301
|
const { region, maxAds } = opts;
|
|
27964
28302
|
let advertiserId = opts.advertiserId?.trim() || null;
|
|
27965
|
-
if (!advertiserId && opts.domain) {
|
|
27966
|
-
|
|
27967
|
-
|
|
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);
|
|
27968
28308
|
}
|
|
27969
28309
|
if (!advertiserId) {
|
|
27970
28310
|
return { advertiserId: null, advertiserName: null, domain: opts.domain ?? null, region, totalCreatives: 0, videoCount: 0, imageCount: 0, textCount: 0, ads: [] };
|
|
@@ -28125,10 +28465,11 @@ var init_google_ads_routes = __esm({
|
|
|
28125
28465
|
});
|
|
28126
28466
|
GoogleAdsPageIntelBodySchema = import_zod22.z.object({
|
|
28127
28467
|
advertiserId: import_zod22.z.string().trim().optional(),
|
|
28468
|
+
advertiserName: import_zod22.z.string().trim().optional(),
|
|
28128
28469
|
domain: import_zod22.z.string().trim().optional(),
|
|
28129
28470
|
region: import_zod22.z.string().trim().toUpperCase().length(2).optional(),
|
|
28130
28471
|
maxAds: import_zod22.z.number().int().min(1).max(200).optional()
|
|
28131
|
-
}).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)" });
|
|
28132
28473
|
GoogleAdsTranscribeBodySchema = import_zod22.z.object({
|
|
28133
28474
|
videoUrl: import_zod22.z.string().trim().min(1, "videoUrl is required")
|
|
28134
28475
|
});
|
|
@@ -28154,13 +28495,13 @@ var init_google_ads_routes = __esm({
|
|
|
28154
28495
|
const extractor = new GoogleAdsExtractor(driver);
|
|
28155
28496
|
return extractor.search(body.query.trim(), region, maxResults);
|
|
28156
28497
|
});
|
|
28157
|
-
if (result.advertisers.length === 0) {
|
|
28498
|
+
if (result.advertisers.length === 0 && result.websites.length === 0) {
|
|
28158
28499
|
await creditMc(user.id, MC_COSTS.google_ads_search, LedgerOperation.GOOGLE_ADS_SEARCH_REFUND, "empty result");
|
|
28159
28500
|
refunded = true;
|
|
28160
28501
|
await logRequestEvent({ userId: user.id, source: "google_ads_search", status: "failed", query: body.query.trim(), error: "empty result refunded" });
|
|
28161
28502
|
return c.json({ error: "no advertisers found (refunded)" }, 503);
|
|
28162
28503
|
}
|
|
28163
|
-
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 });
|
|
28164
28505
|
return c.json(result);
|
|
28165
28506
|
} catch (err) {
|
|
28166
28507
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -28179,7 +28520,7 @@ var init_google_ads_routes = __esm({
|
|
|
28179
28520
|
const body = parsed.data;
|
|
28180
28521
|
const region = body.region ?? "US";
|
|
28181
28522
|
const maxAds = Math.min(200, Math.max(1, body.maxAds ?? 50));
|
|
28182
|
-
const queryLabel = body.advertiserId ?? body.domain ?? "";
|
|
28523
|
+
const queryLabel = body.advertiserId ?? body.domain ?? body.advertiserName ?? "";
|
|
28183
28524
|
const user = c.get("user");
|
|
28184
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 } });
|
|
28185
28526
|
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
@@ -28192,7 +28533,7 @@ var init_google_ads_routes = __esm({
|
|
|
28192
28533
|
const result = await runWithCostContext({ ...currentCostContext(), op: "google_ads_intel", userId: user.id }, async () => {
|
|
28193
28534
|
await driver.launch(await kernelLaunchOptsResidential2());
|
|
28194
28535
|
const extractor = new GoogleAdsExtractor(driver);
|
|
28195
|
-
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 });
|
|
28196
28537
|
});
|
|
28197
28538
|
if (result.ads.length === 0) {
|
|
28198
28539
|
await creditMc(user.id, MC_COSTS.google_ads_intel, LedgerOperation.GOOGLE_ADS_INTEL_REFUND, "empty result");
|
|
@@ -29140,6 +29481,7 @@ var init_instagram_routes = __esm({
|
|
|
29140
29481
|
init_db();
|
|
29141
29482
|
init_rates();
|
|
29142
29483
|
init_concurrency_gates();
|
|
29484
|
+
init_cost_context();
|
|
29143
29485
|
init_blob_store();
|
|
29144
29486
|
init_InstagramContentExtractor();
|
|
29145
29487
|
init_media_transcription();
|
|
@@ -29203,6 +29545,12 @@ var init_instagram_routes = __esm({
|
|
|
29203
29545
|
scrollDelayMs: body.scrollDelayMs,
|
|
29204
29546
|
stableScrollLimit: body.stableScrollLimit
|
|
29205
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
|
+
}
|
|
29206
29554
|
const response = { ...result, browser: launch.browser };
|
|
29207
29555
|
await logRequestEvent({ userId: user.id, source: "instagram_profile_content", status: "done", query: target.profileUrl, resultCount: result.items.length, result: response });
|
|
29208
29556
|
return c.json(response);
|
|
@@ -29326,7 +29674,10 @@ var init_instagram_routes = __esm({
|
|
|
29326
29674
|
const tx = await debitMc(user.id, holdMc, LedgerOperation.INSTAGRAM_TRANSCRIBE, sourceUrl.href);
|
|
29327
29675
|
if (!tx.ok) return c.json(insufficientBalanceResponse(tx.balance_mc, holdMc), 402);
|
|
29328
29676
|
transcriptDebited = true;
|
|
29329
|
-
transcript = await
|
|
29677
|
+
transcript = await runWithCostContext(
|
|
29678
|
+
{ ...currentCostContext(), op: "instagram_transcribe", userId: user.id },
|
|
29679
|
+
() => transcribeMediaUrl(audioUrl, "# Instagram Media Transcript")
|
|
29680
|
+
);
|
|
29330
29681
|
const settleMc = mediaTranscribeChargeMc(transcript.durationMs / 1e3);
|
|
29331
29682
|
const diff = holdMc - settleMc;
|
|
29332
29683
|
if (diff > 0) await creditMc(user.id, diff, LedgerOperation.INSTAGRAM_TRANSCRIBE_REFUND, "real-cost reconcile");
|
|
@@ -30028,6 +30379,8 @@ var init_PAAExtractor = __esm({
|
|
|
30028
30379
|
aioShareCaptured: null
|
|
30029
30380
|
};
|
|
30030
30381
|
aioShareUrlEarly = null;
|
|
30382
|
+
collectedRows = [];
|
|
30383
|
+
partialContext = null;
|
|
30031
30384
|
normalizeQuestion(q) {
|
|
30032
30385
|
return q.toLowerCase().replace(/[^\w\s]/g, "").replace(/\s+/g, " ").trim();
|
|
30033
30386
|
}
|
|
@@ -30132,11 +30485,50 @@ var init_PAAExtractor = __esm({
|
|
|
30132
30485
|
extracted_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
30133
30486
|
};
|
|
30134
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
|
+
}
|
|
30135
30526
|
async runBFS(page, options, signal) {
|
|
30136
30527
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
30137
30528
|
const seenQs = /* @__PURE__ */ new Set();
|
|
30138
30529
|
const orderedQs = [];
|
|
30139
|
-
|
|
30530
|
+
this.collectedRows = [];
|
|
30531
|
+
const results = this.collectedRows;
|
|
30140
30532
|
const abandonedNeverReclick = /* @__PURE__ */ new Set();
|
|
30141
30533
|
const clickedOnceEver = /* @__PURE__ */ new Set();
|
|
30142
30534
|
const itemArgs = {
|
|
@@ -30748,6 +31140,8 @@ var init_PAAExtractor = __esm({
|
|
|
30748
31140
|
const startMs = Date.now();
|
|
30749
31141
|
this.completeness = { paaWithoutAnswer: 0, paaWithoutSource: 0, paaAnswersRecovered: 0, aioShareCaptured: null };
|
|
30750
31142
|
this.aioShareUrlEarly = null;
|
|
31143
|
+
this.collectedRows = [];
|
|
31144
|
+
this.partialContext = { seed: options.query, location: options.location ?? null, startMs };
|
|
30751
31145
|
const isMobile = options.device === "mobile";
|
|
30752
31146
|
const config = {
|
|
30753
31147
|
headless: options.headless,
|
|
@@ -31295,7 +31689,23 @@ async function extractOnce2(options, signal) {
|
|
|
31295
31689
|
} : null);
|
|
31296
31690
|
cleanup = await driver.close();
|
|
31297
31691
|
}
|
|
31298
|
-
|
|
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 };
|
|
31299
31709
|
}
|
|
31300
31710
|
async function harvest(rawOptions) {
|
|
31301
31711
|
const raw = typeof rawOptions === "object" && rawOptions !== null ? rawOptions : {};
|
|
@@ -32913,6 +33323,7 @@ var init_trustpilot_routes = __esm({
|
|
|
32913
33323
|
const driver = new BrowserDriver();
|
|
32914
33324
|
const allReviews = [];
|
|
32915
33325
|
let pagesFetched = 0;
|
|
33326
|
+
let stoppedEarly = null;
|
|
32916
33327
|
try {
|
|
32917
33328
|
await driver.launch({
|
|
32918
33329
|
headless: false,
|
|
@@ -32930,6 +33341,12 @@ var init_trustpilot_routes = __esm({
|
|
|
32930
33341
|
if (cards.length === 0) break;
|
|
32931
33342
|
allReviews.push(...cards);
|
|
32932
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
|
+
};
|
|
32933
33350
|
} finally {
|
|
32934
33351
|
await driver.close();
|
|
32935
33352
|
}
|
|
@@ -32955,6 +33372,7 @@ var init_trustpilot_routes = __esm({
|
|
|
32955
33372
|
pagesFetched,
|
|
32956
33373
|
reviewCount: allReviews.length,
|
|
32957
33374
|
reviews: allReviews,
|
|
33375
|
+
stoppedEarly,
|
|
32958
33376
|
durationMs: Date.now() - startMs
|
|
32959
33377
|
};
|
|
32960
33378
|
await logRequestEvent({ userId: user.id, source: "trustpilot_reviews", status: "done", query: domain, resultCount: allReviews.length, result });
|
|
@@ -37500,7 +37918,7 @@ var PACKAGE_VERSION;
|
|
|
37500
37918
|
var init_version = __esm({
|
|
37501
37919
|
"src/version.ts"() {
|
|
37502
37920
|
"use strict";
|
|
37503
|
-
PACKAGE_VERSION = "0.38.
|
|
37921
|
+
PACKAGE_VERSION = "0.38.2";
|
|
37504
37922
|
}
|
|
37505
37923
|
});
|
|
37506
37924
|
|
|
@@ -37997,9 +38415,9 @@ var init_mcp_tool_schemas = __esm({
|
|
|
37997
38415
|
const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
37998
38416
|
const looksLikeIpv4 = /^\d{1,3}(?:\.\d{1,3}){3}$/.test(hostname);
|
|
37999
38417
|
const looksLikeIpv6 = hostname.includes(":");
|
|
38000
|
-
const
|
|
38418
|
+
const looksLikeDomain = hostname.includes(".");
|
|
38001
38419
|
const looksLikeLocalhost = hostname === "localhost";
|
|
38002
|
-
if (!looksLikeIpv4 && !looksLikeIpv6 && !
|
|
38420
|
+
if (!looksLikeIpv4 && !looksLikeIpv6 && !looksLikeDomain && !looksLikeLocalhost) {
|
|
38003
38421
|
ctx.addIssue({ code: import_zod38.z.ZodIssueCode.custom, message: WEBSITE_URL_OR_DOMAIN_ERROR });
|
|
38004
38422
|
return import_zod38.z.NEVER;
|
|
38005
38423
|
}
|
|
@@ -38135,8 +38553,9 @@ var init_mcp_tool_schemas = __esm({
|
|
|
38135
38553
|
maxResults: import_zod38.z.number().int().min(1).max(20).default(10).describe("Maximum advertisers to return. Default 10, maximum 20.")
|
|
38136
38554
|
};
|
|
38137
38555
|
GoogleAdsPageIntelInputSchema = {
|
|
38138
|
-
|
|
38139
|
-
|
|
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."),
|
|
38140
38559
|
region: import_zod38.z.string().length(2).default("US").describe("Two-letter region code for where the ads are shown. Default US."),
|
|
38141
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.")
|
|
38142
38561
|
};
|
|
@@ -38591,6 +39010,10 @@ var init_mcp_tool_schemas = __esm({
|
|
|
38591
39010
|
};
|
|
38592
39011
|
ExtractSiteOutputSchema = {
|
|
38593
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."),
|
|
38594
39017
|
pageCount: import_zod38.z.number().int().min(0).optional().describe("Absent when background is true \u2014 the crawl has not finished yet."),
|
|
38595
39018
|
pages: import_zod38.z.array(import_zod38.z.object({
|
|
38596
39019
|
url: import_zod38.z.string(),
|
|
@@ -38970,9 +39393,15 @@ var init_mcp_tool_schemas = __esm({
|
|
|
38970
39393
|
advertisers: import_zod38.z.array(import_zod38.z.object({
|
|
38971
39394
|
advertiserId: NullableString,
|
|
38972
39395
|
name: NullableString,
|
|
39396
|
+
basedIn: NullableString,
|
|
38973
39397
|
domain: NullableString,
|
|
38974
39398
|
approxAdCount: import_zod38.z.number().int().nullable(),
|
|
38975
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()
|
|
38976
39405
|
}))
|
|
38977
39406
|
};
|
|
38978
39407
|
GoogleAdsPageIntelOutputSchema = {
|
|
@@ -40289,14 +40718,14 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
|
|
|
40289
40718
|
}, async (input) => formatFacebookAdTranscribe(await executor.facebookAdTranscribe(input), input));
|
|
40290
40719
|
server.registerTool("google_ads_search", {
|
|
40291
40720
|
title: "Google Ads Transparency Search",
|
|
40292
|
-
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.",
|
|
40293
40722
|
inputSchema: GoogleAdsSearchInputSchema,
|
|
40294
40723
|
outputSchema: recordOutputSchema("google_ads_search", GoogleAdsSearchOutputSchema),
|
|
40295
40724
|
annotations: liveWebToolAnnotations("Google Ads Transparency Search")
|
|
40296
40725
|
}, async (input) => formatGoogleAdsSearch(await executor.googleAdsSearch(input), input));
|
|
40297
40726
|
server.registerTool("google_ads_page_intel", {
|
|
40298
40727
|
title: "Google Ads Advertiser Intel",
|
|
40299
|
-
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.",
|
|
40300
40729
|
inputSchema: GoogleAdsPageIntelInputSchema,
|
|
40301
40730
|
outputSchema: recordOutputSchema("google_ads_page_intel", GoogleAdsPageIntelOutputSchema),
|
|
40302
40731
|
annotations: liveWebToolAnnotations("Google Ads Advertiser Intel")
|
|
@@ -48394,6 +48823,106 @@ async function cancelStandaloneMemorySub(memSubId, baseSubId) {
|
|
|
48394
48823
|
console.error("[stripe] could not wind down standalone memory subscription", memSubId, err instanceof Error ? err.message : err);
|
|
48395
48824
|
}
|
|
48396
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
|
+
}
|
|
48397
48926
|
function linePriceId(line) {
|
|
48398
48927
|
const l = line;
|
|
48399
48928
|
return l?.price?.id ?? l?.pricing?.price_details?.price ?? l?.plan?.id;
|
|
@@ -48428,107 +48957,19 @@ var init_stripe_routes = __esm({
|
|
|
48428
48957
|
} catch {
|
|
48429
48958
|
return c.json({ error: "Invalid signature" }, 400);
|
|
48430
48959
|
}
|
|
48431
|
-
const
|
|
48432
|
-
if (
|
|
48433
|
-
|
|
48434
|
-
|
|
48435
|
-
const
|
|
48436
|
-
if (
|
|
48437
|
-
|
|
48438
|
-
|
|
48439
|
-
|
|
48440
|
-
|
|
48441
|
-
|
|
48442
|
-
|
|
48443
|
-
const liveBasePlanPriceId = subId ? findBasePlanItem(await stripe.subscriptions.retrieve(subId))?.price?.id : void 0;
|
|
48444
|
-
const tierPriceId = liveBasePlanPriceId ?? invoice.lines.data.map(linePriceId).find((id) => id && id in SUBSCRIPTION_TIERS);
|
|
48445
|
-
const tier = tierPriceId ? SUBSCRIPTION_TIERS[tierPriceId] : void 0;
|
|
48446
|
-
if (tier) {
|
|
48447
|
-
const user = await resolveUser3(invoice.customer, invoice.customer_email ?? void 0);
|
|
48448
|
-
if (user && invoice.id && !await ledgerExistsForStripePI(invoice.id)) {
|
|
48449
|
-
await creditMc(user.id, tier.credits_mc, LedgerOperation.SUBSCRIPTION, `${tier.label} subscription credits`, invoice.id, { neverExpires: tier.credits_never_expire });
|
|
48450
|
-
await setSubscriptionTier(user.id, tier.tier, tier.concurrency, subId ?? user.subscription_id);
|
|
48451
|
-
if (tier.includes_memory) {
|
|
48452
|
-
await cancelStandaloneMemorySub(user.memory_subscription_id, subId ?? user.subscription_id);
|
|
48453
|
-
await setMemoryPlan(user.id, "pro", null);
|
|
48454
|
-
await syncMemoryKeyPlan(user, resolveEffectiveMemoryPlan({ ...user, memory_plan: "pro", subscription_tier: tier.tier }));
|
|
48455
|
-
}
|
|
48456
|
-
const credentials = await syncScheduledActionCredentials(user);
|
|
48457
|
-
if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
|
|
48458
|
-
}
|
|
48459
|
-
}
|
|
48460
|
-
}
|
|
48461
|
-
}
|
|
48462
|
-
if (event.type === "customer.subscription.created" || event.type === "customer.subscription.updated") {
|
|
48463
|
-
const sub = event.data.object;
|
|
48464
|
-
const priceId = findBasePlanItem(sub)?.price?.id ?? sub.items.data[0]?.price?.id;
|
|
48465
|
-
if (priceId && priceId in MEMORY_PLANS) {
|
|
48466
|
-
const mem = MEMORY_PLANS[priceId];
|
|
48467
|
-
const user2 = await resolveUser3(sub.customer, sub.customer_email);
|
|
48468
|
-
if (user2) {
|
|
48469
|
-
const live = sub.status === "active" || sub.status === "trialing";
|
|
48470
|
-
const newMemoryPlan = live ? mem.plan : "free";
|
|
48471
|
-
await setMemoryPlan(user2.id, newMemoryPlan, live ? sub.id : null);
|
|
48472
|
-
await syncMemoryKeyPlan(user2, resolveEffectiveMemoryPlan({ ...user2, memory_plan: newMemoryPlan }));
|
|
48473
|
-
}
|
|
48474
|
-
return c.json({ received: true });
|
|
48475
|
-
}
|
|
48476
|
-
const user = await getUserByStripeCustomerId(sub.customer);
|
|
48477
|
-
if (!user) return c.json({ received: true });
|
|
48478
|
-
if (priceId && priceId in SUBSCRIPTION_TIERS) {
|
|
48479
|
-
const tier = SUBSCRIPTION_TIERS[priceId];
|
|
48480
|
-
const live = sub.status === "active" || sub.status === "trialing";
|
|
48481
|
-
const newTier = live ? tier.tier : null;
|
|
48482
|
-
await setSubscriptionTier(user.id, newTier, live ? tier.concurrency : 0, live ? sub.id : null);
|
|
48483
|
-
if (tier.includes_memory) {
|
|
48484
|
-
const newMemoryPlan = live ? "pro" : "free";
|
|
48485
|
-
if (live) await cancelStandaloneMemorySub(user.memory_subscription_id, sub.id);
|
|
48486
|
-
await setMemoryPlan(user.id, newMemoryPlan, live ? sub.id : null);
|
|
48487
|
-
await syncMemoryKeyPlan(user, resolveEffectiveMemoryPlan({ ...user, memory_plan: newMemoryPlan }));
|
|
48488
|
-
} else if (user.memory_subscription_id === sub.id) {
|
|
48489
|
-
await setMemoryPlan(user.id, "free", null);
|
|
48490
|
-
await syncMemoryKeyPlan(user, resolveEffectiveMemoryPlan({ ...user, memory_plan: "free" }));
|
|
48491
|
-
}
|
|
48492
|
-
if (live) {
|
|
48493
|
-
const credentials = await syncScheduledActionCredentials(user);
|
|
48494
|
-
if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
|
|
48495
|
-
}
|
|
48496
|
-
} else if (priceId === CONCURRENCY_PRICE_ID && event.type === "customer.subscription.created") {
|
|
48497
|
-
await setExtraConcurrencySlots(user.id, user.extra_concurrency_slots + 1);
|
|
48498
|
-
await setConcurrencySubId(user.id, sub.id);
|
|
48499
|
-
}
|
|
48500
|
-
}
|
|
48501
|
-
if (event.type === "customer.subscription.deleted") {
|
|
48502
|
-
const sub = event.data.object;
|
|
48503
|
-
const priceId = findBasePlanItem(sub)?.price?.id ?? sub.items.data[0]?.price?.id;
|
|
48504
|
-
if (priceId && priceId in SUBSCRIPTION_TIERS) {
|
|
48505
|
-
const tier = SUBSCRIPTION_TIERS[priceId];
|
|
48506
|
-
const user2 = await getUserByStripeCustomerId(sub.customer);
|
|
48507
|
-
if (user2) {
|
|
48508
|
-
await setSubscriptionTier(user2.id, null, 0, null);
|
|
48509
|
-
if (tier.includes_memory || user2.memory_subscription_id === sub.id) {
|
|
48510
|
-
await setMemoryPlan(user2.id, "free", null);
|
|
48511
|
-
await syncMemoryKeyPlan(user2, resolveEffectiveMemoryPlan({ ...user2, memory_plan: "free" }));
|
|
48512
|
-
}
|
|
48513
|
-
}
|
|
48514
|
-
return c.json({ received: true });
|
|
48515
|
-
}
|
|
48516
|
-
if (priceId && priceId in MEMORY_PLANS || await resolveUserByMemorySubscription(sub.id)) {
|
|
48517
|
-
const user2 = await resolveUserByMemorySubscription(sub.id) ?? await getUserByStripeCustomerId(sub.customer);
|
|
48518
|
-
if (user2) {
|
|
48519
|
-
await setMemoryPlan(user2.id, "free", null);
|
|
48520
|
-
await syncMemoryKeyPlan(user2, resolveEffectiveMemoryPlan({ ...user2, memory_plan: "free" }));
|
|
48521
|
-
}
|
|
48522
|
-
return c.json({ received: true });
|
|
48523
|
-
}
|
|
48524
|
-
const user = await getUserByStripeCustomerId(sub.customer);
|
|
48525
|
-
if (!user) return c.json({ received: true });
|
|
48526
|
-
if (priceId === CONCURRENCY_PRICE_ID) {
|
|
48527
|
-
await setExtraConcurrencySlots(user.id, Math.max(0, user.extra_concurrency_slots - 1));
|
|
48528
|
-
await setConcurrencySubId(user.id, null);
|
|
48529
|
-
}
|
|
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);
|
|
48530
48972
|
}
|
|
48531
|
-
return c.json({ received: true });
|
|
48532
48973
|
});
|
|
48533
48974
|
}
|
|
48534
48975
|
});
|
|
@@ -55201,6 +55642,7 @@ function opForCostPath(p) {
|
|
|
55201
55642
|
if (p.startsWith("/facebook/")) {
|
|
55202
55643
|
if (p.endsWith("transcribe")) return "fb_transcribe";
|
|
55203
55644
|
if (p.endsWith("/search")) return "fb_search";
|
|
55645
|
+
if (p.endsWith("/page-intel")) return "fb_page_intel";
|
|
55204
55646
|
return "fb_ad";
|
|
55205
55647
|
}
|
|
55206
55648
|
if (p.startsWith("/instagram/")) return "instagram";
|