mcp-scraper 0.21.2 → 0.21.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bin/api-server.cjs +1061 -308
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +3 -3
- package/dist/bin/mcp-scraper-cli.cjs +1 -1
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-install.cjs +1 -1
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +1 -1
- package/dist/bin/mcp-stdio-server.cjs +101 -34
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +5 -5
- package/dist/bin/paa-harvest.cjs +22 -0
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +3 -3
- package/dist/{chunk-ZJDANZTQ.js → chunk-E5J4HJBO.js} +3 -2
- package/dist/chunk-E5J4HJBO.js.map +1 -0
- package/dist/{chunk-DZBGULWW.js → chunk-ICT7DDHL.js} +399 -3
- package/dist/chunk-ICT7DDHL.js.map +1 -0
- package/dist/{chunk-2FF4GBQ4.js → chunk-JHI373VB.js} +49 -7
- package/dist/chunk-JHI373VB.js.map +1 -0
- package/dist/{chunk-GMZFQRH2.js → chunk-MTSBI7ZH.js} +23 -6
- package/dist/chunk-MTSBI7ZH.js.map +1 -0
- package/dist/{chunk-ZNNYPE2U.js → chunk-N4JHXPW6.js} +89 -27
- package/dist/chunk-N4JHXPW6.js.map +1 -0
- package/dist/{chunk-X3NIXHHG.js → chunk-PNBYYAEK.js} +3 -3
- package/dist/{chunk-JBME5WJR.js → chunk-SDHPQTFM.js} +2 -2
- package/dist/chunk-WQI7JIIN.js +7 -0
- package/dist/chunk-WQI7JIIN.js.map +1 -0
- package/dist/{db-EONGTNJM.js → db-WRILVOFY.js} +6 -2
- package/dist/{extract-bundle-HMCYUVBJ.js → extract-bundle-NFFVMUMT.js} +2 -2
- package/dist/index.cjs +22 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -3
- package/dist/{server-CHTX6YQA.js → server-OLOOFN5W.js} +499 -271
- package/dist/server-OLOOFN5W.js.map +1 -0
- package/dist/{site-extract-repository-5V5EEQ72.js → site-extract-repository-2QQSL2QC.js} +4 -4
- package/dist/{worker-GE5JMHE6.js → worker-RV2TYXE3.js} +15 -8
- package/dist/worker-RV2TYXE3.js.map +1 -0
- package/docs/mcp-tool-manifest.generated.json +90 -11
- package/docs/specs/oauth-mcp-spec.md +2 -2
- package/package.json +1 -1
- package/dist/chunk-2FF4GBQ4.js.map +0 -1
- package/dist/chunk-DZBGULWW.js.map +0 -1
- package/dist/chunk-GMZFQRH2.js.map +0 -1
- package/dist/chunk-NBF4F4UT.js +0 -7
- package/dist/chunk-NBF4F4UT.js.map +0 -1
- package/dist/chunk-ZJDANZTQ.js.map +0 -1
- package/dist/chunk-ZNNYPE2U.js.map +0 -1
- package/dist/server-CHTX6YQA.js.map +0 -1
- package/dist/worker-GE5JMHE6.js.map +0 -1
- /package/dist/{chunk-X3NIXHHG.js.map → chunk-PNBYYAEK.js.map} +0 -0
- /package/dist/{chunk-JBME5WJR.js.map → chunk-SDHPQTFM.js.map} +0 -0
- /package/dist/{db-EONGTNJM.js.map → db-WRILVOFY.js.map} +0 -0
- /package/dist/{extract-bundle-HMCYUVBJ.js.map → extract-bundle-NFFVMUMT.js.map} +0 -0
- /package/dist/{site-extract-repository-5V5EEQ72.js.map → site-extract-repository-2QQSL2QC.js.map} +0 -0
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
createHarvestAttemptRecorder,
|
|
7
7
|
harvestProblemResponse,
|
|
8
8
|
serializeHarvestProblem
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-SDHPQTFM.js";
|
|
10
10
|
import {
|
|
11
11
|
BrowserDriver,
|
|
12
12
|
MapsSelectors,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
deleteKernelProxyId,
|
|
15
15
|
harvest,
|
|
16
16
|
resolveKernelProxyId
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-E5J4HJBO.js";
|
|
18
18
|
import {
|
|
19
19
|
HttpMcpToolExecutor,
|
|
20
20
|
MemoryMcpToolExecutor,
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
sanitizeAttempts,
|
|
36
36
|
sanitizeHarvestResult,
|
|
37
37
|
transcribeMediaUrl
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-N4JHXPW6.js";
|
|
39
39
|
import {
|
|
40
40
|
auditImages,
|
|
41
41
|
buildLinkReport,
|
|
@@ -49,11 +49,10 @@ import {
|
|
|
49
49
|
browserServiceProxyId,
|
|
50
50
|
currentCostContext,
|
|
51
51
|
kernelCostUsd,
|
|
52
|
-
migrateCostTelemetry,
|
|
53
52
|
recordKernelSession,
|
|
54
53
|
recordVendorUsage,
|
|
55
54
|
runWithCostContext
|
|
56
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-MTSBI7ZH.js";
|
|
57
56
|
import {
|
|
58
57
|
csvRecords,
|
|
59
58
|
listWorkflowDefinitions,
|
|
@@ -77,7 +76,7 @@ import {
|
|
|
77
76
|
RawMapsOverviewSchema,
|
|
78
77
|
RawMapsReviewStatsSchema
|
|
79
78
|
} from "./chunk-XGIPATLV.js";
|
|
80
|
-
import "./chunk-
|
|
79
|
+
import "./chunk-WQI7JIIN.js";
|
|
81
80
|
import {
|
|
82
81
|
completeExtractJob,
|
|
83
82
|
countSuccessfulPages,
|
|
@@ -88,12 +87,11 @@ import {
|
|
|
88
87
|
saveExtractPages,
|
|
89
88
|
setExtractJobTotal,
|
|
90
89
|
settleExtractJob
|
|
91
|
-
} from "./chunk-
|
|
90
|
+
} from "./chunk-PNBYYAEK.js";
|
|
92
91
|
import {
|
|
93
92
|
BROWSER_OPEN_MIN_BALANCE_MC,
|
|
94
93
|
CONCURRENCY_PRICE_ID,
|
|
95
94
|
CREDIT_COST_CATALOG,
|
|
96
|
-
FREE_SIGNUP_MC,
|
|
97
95
|
LedgerOperation,
|
|
98
96
|
MC_COSTS,
|
|
99
97
|
MC_PER_CREDIT,
|
|
@@ -115,9 +113,11 @@ import {
|
|
|
115
113
|
VIDEO_ANALYSIS_LLM_MARGIN_MULTIPLE,
|
|
116
114
|
browserActiveCostMc,
|
|
117
115
|
concurrencySlotBillingInfo,
|
|
116
|
+
getPublicRatesPayload,
|
|
118
117
|
insufficientBalanceResponse,
|
|
118
|
+
serpActualCostMc,
|
|
119
119
|
vendorCostUsdToMc
|
|
120
|
-
} from "./chunk-
|
|
120
|
+
} from "./chunk-JHI373VB.js";
|
|
121
121
|
import {
|
|
122
122
|
CaptchaError,
|
|
123
123
|
RECAPTCHA_INSTRUCTIONS,
|
|
@@ -143,6 +143,7 @@ import {
|
|
|
143
143
|
creditMc,
|
|
144
144
|
deactivateUser,
|
|
145
145
|
debitMc,
|
|
146
|
+
debitMcIdempotent,
|
|
146
147
|
deleteWorkflowSchedule,
|
|
147
148
|
expireOldLots,
|
|
148
149
|
failJob,
|
|
@@ -198,9 +199,10 @@ import {
|
|
|
198
199
|
setStripeCustomerId,
|
|
199
200
|
setSubscriptionTier,
|
|
200
201
|
setUserMemoryCreds,
|
|
202
|
+
settleDebitMcIdempotent,
|
|
201
203
|
upsertPageSnapshot,
|
|
202
204
|
verifyPassword
|
|
203
|
-
} from "./chunk-
|
|
205
|
+
} from "./chunk-ICT7DDHL.js";
|
|
204
206
|
|
|
205
207
|
// src/blog/registry.ts
|
|
206
208
|
var posts = [
|
|
@@ -7358,7 +7360,7 @@ var siteExtractFn = inngest.createFunction(
|
|
|
7358
7360
|
return buildSeoAudit(pages, job.startUrl);
|
|
7359
7361
|
}) : null;
|
|
7360
7362
|
const artifacts = await step.run("finalize", async () => {
|
|
7361
|
-
const { assembleExtractArtifacts } = await import("./extract-bundle-
|
|
7363
|
+
const { assembleExtractArtifacts } = await import("./extract-bundle-NFFVMUMT.js");
|
|
7362
7364
|
const stored = await assembleExtractArtifacts(job, { branding, imageAudit, seoAudit });
|
|
7363
7365
|
await completeExtractJob(jobId, stored);
|
|
7364
7366
|
return stored;
|
|
@@ -7988,8 +7990,17 @@ import { createHash as createHash2, createHmac, randomUUID, timingSafeEqual } fr
|
|
|
7988
7990
|
// src/api/scheduling-access.ts
|
|
7989
7991
|
var PAID_PLAN_REQUIRED_CODE = "paid_plan_required";
|
|
7990
7992
|
var PAID_PLAN_REQUIRED_MESSAGE = "Scheduled Actions requires an active MCP Scraper Starter plan or higher.";
|
|
7993
|
+
function schedulingAccessOverrides() {
|
|
7994
|
+
return new Set(
|
|
7995
|
+
(process.env.SCHEDULING_ACCESS_OVERRIDE_IDENTITIES ?? "").split(",").map((identity) => identity.trim().toLowerCase()).filter(Boolean)
|
|
7996
|
+
);
|
|
7997
|
+
}
|
|
7991
7998
|
function hasPaidSchedulingAccess(user) {
|
|
7992
|
-
|
|
7999
|
+
const tier = typeof user.subscription_tier === "string" ? user.subscription_tier.trim() : "";
|
|
8000
|
+
const subscriptionId = typeof user.subscription_id === "string" ? user.subscription_id.trim() : "";
|
|
8001
|
+
if (tier && subscriptionId) return true;
|
|
8002
|
+
const identity = typeof user.email === "string" ? user.email.trim().toLowerCase() : "";
|
|
8003
|
+
return identity.length > 0 && schedulingAccessOverrides().has(identity);
|
|
7993
8004
|
}
|
|
7994
8005
|
function paidPlanRequiredPayload() {
|
|
7995
8006
|
return {
|
|
@@ -8026,7 +8037,7 @@ function memoryScheduleToolNeedsCredentials(upstreamName) {
|
|
|
8026
8037
|
// src/api/unified-billing.ts
|
|
8027
8038
|
var SCHEDULED_RUN_BILLING_CLASS = "scheduled_agent_run";
|
|
8028
8039
|
var SCHEDULED_RUN_SOURCE_SURFACE = "mastra_scheduler";
|
|
8029
|
-
var AUTHORIZATION_TTL_MS =
|
|
8040
|
+
var AUTHORIZATION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
8030
8041
|
var UnifiedBillingError = class extends Error {
|
|
8031
8042
|
constructor(code, message, httpStatus, details = {}) {
|
|
8032
8043
|
super(message);
|
|
@@ -8088,6 +8099,15 @@ function safeMetadata(value) {
|
|
|
8088
8099
|
if (!value) return "{}";
|
|
8089
8100
|
return JSON.stringify(value).slice(0, 4e3);
|
|
8090
8101
|
}
|
|
8102
|
+
function metadataReason(row) {
|
|
8103
|
+
if (!row.metadata) return void 0;
|
|
8104
|
+
try {
|
|
8105
|
+
const metadata = JSON.parse(row.metadata);
|
|
8106
|
+
return typeof metadata.reason === "string" ? metadata.reason : void 0;
|
|
8107
|
+
} catch {
|
|
8108
|
+
return void 0;
|
|
8109
|
+
}
|
|
8110
|
+
}
|
|
8091
8111
|
function mapAuthorization(row, balanceMc) {
|
|
8092
8112
|
return {
|
|
8093
8113
|
id: row.id,
|
|
@@ -8116,23 +8136,28 @@ async function eventByKey(idempotencyKey) {
|
|
|
8116
8136
|
});
|
|
8117
8137
|
return result.rows[0] ? result.rows[0] : null;
|
|
8118
8138
|
}
|
|
8119
|
-
function
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8139
|
+
function mapStartEvent(row, balanceMc, duplicate) {
|
|
8140
|
+
return {
|
|
8141
|
+
id: row.id,
|
|
8142
|
+
authorizationId: row.authorization_id,
|
|
8143
|
+
status: "started",
|
|
8144
|
+
amountMc: SCHEDULED_RUN_BASE_MC,
|
|
8145
|
+
credits: SCHEDULED_RUN_BASE_MC / MC_PER_CREDIT,
|
|
8146
|
+
balanceMc,
|
|
8147
|
+
duplicate
|
|
8148
|
+
};
|
|
8149
|
+
}
|
|
8150
|
+
function mapSettlementEvent(row, balanceMc, duplicate) {
|
|
8128
8151
|
const rawNanos = row.raw_cost_usd_nanos == null ? null : Number(row.raw_cost_usd_nanos);
|
|
8129
|
-
const modelMc = Math.max(0, Number(row.amount_mc)
|
|
8152
|
+
const modelMc = Math.max(0, Number(row.amount_mc));
|
|
8153
|
+
const amountMc = SCHEDULED_RUN_BASE_MC + modelMc;
|
|
8154
|
+
const reason = metadataReason(row);
|
|
8130
8155
|
return {
|
|
8131
8156
|
id: row.id,
|
|
8132
8157
|
authorizationId: row.authorization_id,
|
|
8133
8158
|
status: row.status === "settled" ? "settled" : "cost_pending",
|
|
8134
|
-
amountMc
|
|
8135
|
-
credits:
|
|
8159
|
+
amountMc,
|
|
8160
|
+
credits: amountMc / MC_PER_CREDIT,
|
|
8136
8161
|
baseMc: SCHEDULED_RUN_BASE_MC,
|
|
8137
8162
|
modelMc,
|
|
8138
8163
|
rawCostUsd: rawNanos == null ? null : rawNanos / USD_NANOS,
|
|
@@ -8154,7 +8179,7 @@ async function authorizeScheduledRun(args) {
|
|
|
8154
8179
|
args: [user.id, SCHEDULED_RUN_BILLING_CLASS]
|
|
8155
8180
|
});
|
|
8156
8181
|
if (pending.rows.length > 0) {
|
|
8157
|
-
throw new UnifiedBillingError("pending_cost", "a previous scheduled run has an unresolved cost", 409);
|
|
8182
|
+
throw new UnifiedBillingError("pending_cost", "a previous scheduled run has an unresolved model cost", 409);
|
|
8158
8183
|
}
|
|
8159
8184
|
const existing = await getDb().execute({
|
|
8160
8185
|
sql: "SELECT id, user_id, idempotency_key, status, minimum_amount_mc, maximum_amount_mc, token_hash, expires_at FROM billing_authorizations WHERE idempotency_key = ? LIMIT 1",
|
|
@@ -8163,14 +8188,9 @@ async function authorizeScheduledRun(args) {
|
|
|
8163
8188
|
if (existing.rows[0]) {
|
|
8164
8189
|
const row = existing.rows[0];
|
|
8165
8190
|
if (Number(row.user_id) !== user.id) {
|
|
8166
|
-
throw new UnifiedBillingError(
|
|
8167
|
-
"invalid_request",
|
|
8168
|
-
"the idempotency key is already in use by another billing identity",
|
|
8169
|
-
409
|
|
8170
|
-
);
|
|
8191
|
+
throw new UnifiedBillingError("invalid_request", "the idempotency key is already in use by another billing identity", 409);
|
|
8171
8192
|
}
|
|
8172
|
-
|
|
8173
|
-
return mapAuthorization(row, balanceMc2);
|
|
8193
|
+
return mapAuthorization(row, await reconcileBalanceMc(user.id, user.balance_mc));
|
|
8174
8194
|
}
|
|
8175
8195
|
const maximumCredits = args.maximumCredits ?? SCHEDULED_RUN_DEFAULT_MAX_CREDITS;
|
|
8176
8196
|
if (!Number.isFinite(maximumCredits) || maximumCredits < SCHEDULED_RUN_BASE_MC / MC_PER_CREDIT) {
|
|
@@ -8191,17 +8211,7 @@ async function authorizeScheduledRun(args) {
|
|
|
8191
8211
|
const expiresAt = new Date(Date.now() + AUTHORIZATION_TTL_MS).toISOString();
|
|
8192
8212
|
const inserted = await getDb().execute({
|
|
8193
8213
|
sql: "INSERT OR IGNORE INTO billing_authorizations (id, user_id, idempotency_key, billing_class, status, minimum_amount_mc, maximum_amount_mc, token_hash, metadata, expires_at) VALUES (?, ?, ?, ?, 'authorized', ?, ?, ?, ?, ?)",
|
|
8194
|
-
args: [
|
|
8195
|
-
id,
|
|
8196
|
-
user.id,
|
|
8197
|
-
idempotencyKey,
|
|
8198
|
-
SCHEDULED_RUN_BILLING_CLASS,
|
|
8199
|
-
SCHEDULED_RUN_BASE_MC,
|
|
8200
|
-
maximumMc,
|
|
8201
|
-
tokenHash(token),
|
|
8202
|
-
safeMetadata(args.metadata),
|
|
8203
|
-
expiresAt
|
|
8204
|
-
]
|
|
8214
|
+
args: [id, user.id, idempotencyKey, SCHEDULED_RUN_BILLING_CLASS, SCHEDULED_RUN_BASE_MC, maximumMc, tokenHash(token), safeMetadata(args.metadata), expiresAt]
|
|
8205
8215
|
});
|
|
8206
8216
|
if (inserted.rowsAffected === 0) {
|
|
8207
8217
|
const raced = await getDb().execute({
|
|
@@ -8211,11 +8221,7 @@ async function authorizeScheduledRun(args) {
|
|
|
8211
8221
|
if (!raced.rows[0]) throw new Error("billing authorization insert raced without a readable winner");
|
|
8212
8222
|
const row = raced.rows[0];
|
|
8213
8223
|
if (Number(row.user_id) !== user.id) {
|
|
8214
|
-
throw new UnifiedBillingError(
|
|
8215
|
-
"invalid_request",
|
|
8216
|
-
"the idempotency key is already in use by another billing identity",
|
|
8217
|
-
409
|
|
8218
|
-
);
|
|
8224
|
+
throw new UnifiedBillingError("invalid_request", "the idempotency key is already in use by another billing identity", 409);
|
|
8219
8225
|
}
|
|
8220
8226
|
return mapAuthorization(row, balanceMc);
|
|
8221
8227
|
}
|
|
@@ -8232,180 +8238,235 @@ async function authorizeScheduledRun(args) {
|
|
|
8232
8238
|
reservationMode: "balance_check"
|
|
8233
8239
|
};
|
|
8234
8240
|
}
|
|
8235
|
-
async function
|
|
8236
|
-
const
|
|
8237
|
-
|
|
8238
|
-
if (!user) throw new UnifiedBillingError("user_not_found", "billing identity was not found", 404);
|
|
8239
|
-
const authorization = await authorizationById(args.authorizationId);
|
|
8241
|
+
async function startScheduledRun(args) {
|
|
8242
|
+
const user = await resolveSchedulingUser(args.identity);
|
|
8243
|
+
let authorization = await authorizationById(args.authorizationId);
|
|
8240
8244
|
if (!authorization || Number(authorization.user_id) !== user.id) {
|
|
8241
8245
|
throw new UnifiedBillingError("authorization_not_found", "billing authorization was not found", 404);
|
|
8242
8246
|
}
|
|
8243
8247
|
verifyToken(authorization, args.authorizationToken);
|
|
8244
|
-
const
|
|
8245
|
-
const existing = await eventByKey(
|
|
8246
|
-
if (existing) {
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
}
|
|
8251
|
-
return
|
|
8248
|
+
const startKey = authorization.id + ":start";
|
|
8249
|
+
const existing = await eventByKey(startKey);
|
|
8250
|
+
if (existing?.status === "settled") {
|
|
8251
|
+
await getDb().execute({
|
|
8252
|
+
sql: "UPDATE billing_authorizations SET status = 'started' WHERE id = ? AND status IN ('authorized', 'starting')",
|
|
8253
|
+
args: [authorization.id]
|
|
8254
|
+
});
|
|
8255
|
+
return mapStartEvent(existing, await reconcileBalanceMc(user.id, user.balance_mc), true);
|
|
8252
8256
|
}
|
|
8253
|
-
if (
|
|
8254
|
-
throw new UnifiedBillingError(
|
|
8257
|
+
if (!hasPaidSchedulingAccess(user)) {
|
|
8258
|
+
throw new UnifiedBillingError(PAID_PLAN_REQUIRED_CODE, PAID_PLAN_REQUIRED_MESSAGE, 403);
|
|
8255
8259
|
}
|
|
8256
|
-
if (
|
|
8260
|
+
if (authorization.status === "authorized") {
|
|
8261
|
+
if (Date.parse(authorization.expires_at) <= Date.now()) {
|
|
8262
|
+
await getDb().execute({
|
|
8263
|
+
sql: "UPDATE billing_authorizations SET status = 'expired' WHERE id = ? AND status = 'authorized'",
|
|
8264
|
+
args: [authorization.id]
|
|
8265
|
+
});
|
|
8266
|
+
throw new UnifiedBillingError("authorization_expired", "billing authorization expired before execution started", 409);
|
|
8267
|
+
}
|
|
8257
8268
|
await getDb().execute({
|
|
8258
|
-
sql: "UPDATE billing_authorizations SET status = '
|
|
8269
|
+
sql: "UPDATE billing_authorizations SET status = 'starting' WHERE id = ? AND status = 'authorized'",
|
|
8259
8270
|
args: [authorization.id]
|
|
8260
8271
|
});
|
|
8261
|
-
|
|
8272
|
+
authorization = await authorizationById(authorization.id);
|
|
8262
8273
|
}
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
throw new UnifiedBillingError("invalid_request", "reported model cost must be a finite non-negative number", 400);
|
|
8274
|
+
if (!authorization || !["starting", "started"].includes(authorization.status)) {
|
|
8275
|
+
throw new UnifiedBillingError("authorization_closed", "billing authorization is no longer startable", 409);
|
|
8266
8276
|
}
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
}
|
|
8270
|
-
const eventId = randomUUID();
|
|
8271
|
-
const rawCostNanos = rawCostUsd == null ? null : Math.round(rawCostUsd * USD_NANOS);
|
|
8272
|
-
const modelMc = rawCostUsd == null ? 0 : vendorCostUsdToMc(rawCostUsd, SCHEDULED_AGENT_LLM_MARKUP_BPS);
|
|
8273
|
-
const totalMc = SCHEDULED_RUN_BASE_MC + modelMc;
|
|
8274
|
-
const pendingReason = args.modelCostStatus === "missing" ? "openrouter_cost_missing" : totalMc > Number(authorization.maximum_amount_mc) ? "max_credits_per_run_exceeded" : null;
|
|
8275
|
-
if (pendingReason) {
|
|
8277
|
+
const eventId = existing?.id ?? randomUUID();
|
|
8278
|
+
if (!existing) {
|
|
8276
8279
|
await getDb().execute({
|
|
8277
|
-
sql: "INSERT INTO billing_events (id, user_id, authorization_id, idempotency_key, billing_class, source_surface, status, amount_mc,
|
|
8280
|
+
sql: "INSERT OR IGNORE INTO billing_events (id, user_id, authorization_id, idempotency_key, billing_class, source_surface, status, amount_mc, multiplier_bps, metadata) VALUES (?, ?, ?, ?, ?, ?, 'capturing', ?, ?, ?)",
|
|
8278
8281
|
args: [
|
|
8279
8282
|
eventId,
|
|
8280
8283
|
user.id,
|
|
8281
8284
|
authorization.id,
|
|
8282
|
-
|
|
8285
|
+
startKey,
|
|
8283
8286
|
SCHEDULED_RUN_BILLING_CLASS,
|
|
8284
8287
|
SCHEDULED_RUN_SOURCE_SURFACE,
|
|
8285
|
-
|
|
8286
|
-
rawCostNanos,
|
|
8288
|
+
SCHEDULED_RUN_BASE_MC,
|
|
8287
8289
|
SCHEDULED_AGENT_LLM_MARKUP_BPS,
|
|
8288
|
-
|
|
8289
|
-
Math.max(0, Math.round(args.outputTokens ?? 0)),
|
|
8290
|
-
Math.max(0, Math.round(args.cachedTokens ?? 0)),
|
|
8291
|
-
Math.max(0, Math.round(args.modelSteps ?? 0)),
|
|
8292
|
-
safeMetadata({ ...args.metadata, reason: pendingReason })
|
|
8290
|
+
safeMetadata({ ...args.metadata, billingPhase: "start", chargeMode: "base_capture" })
|
|
8293
8291
|
]
|
|
8294
8292
|
});
|
|
8295
|
-
await getDb().execute({
|
|
8296
|
-
sql: "UPDATE billing_authorizations SET status = 'cost_pending', settled_at = datetime('now') WHERE id = ?",
|
|
8297
|
-
args: [authorization.id]
|
|
8298
|
-
});
|
|
8299
|
-
return {
|
|
8300
|
-
id: eventId,
|
|
8301
|
-
authorizationId: authorization.id,
|
|
8302
|
-
status: "cost_pending",
|
|
8303
|
-
amountMc: totalMc,
|
|
8304
|
-
credits: totalMc / MC_PER_CREDIT,
|
|
8305
|
-
baseMc: SCHEDULED_RUN_BASE_MC,
|
|
8306
|
-
modelMc,
|
|
8307
|
-
rawCostUsd,
|
|
8308
|
-
multiplierBps: SCHEDULED_AGENT_LLM_MARKUP_BPS,
|
|
8309
|
-
balanceMc: await reconcileBalanceMc(user.id, user.balance_mc),
|
|
8310
|
-
reason: pendingReason,
|
|
8311
|
-
duplicate: false
|
|
8312
|
-
};
|
|
8313
8293
|
}
|
|
8314
|
-
const
|
|
8315
|
-
sql: "INSERT OR IGNORE INTO billing_events (id, user_id, authorization_id, idempotency_key, billing_class, source_surface, status, amount_mc, raw_cost_usd_nanos, multiplier_bps, input_tokens, output_tokens, cached_tokens, model_steps, metadata) VALUES (?, ?, ?, ?, ?, ?, 'settling', ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
8316
|
-
args: [
|
|
8317
|
-
eventId,
|
|
8318
|
-
user.id,
|
|
8319
|
-
authorization.id,
|
|
8320
|
-
settlementKey,
|
|
8321
|
-
SCHEDULED_RUN_BILLING_CLASS,
|
|
8322
|
-
SCHEDULED_RUN_SOURCE_SURFACE,
|
|
8323
|
-
totalMc,
|
|
8324
|
-
rawCostNanos,
|
|
8325
|
-
SCHEDULED_AGENT_LLM_MARKUP_BPS,
|
|
8326
|
-
Math.max(0, Math.round(args.inputTokens ?? 0)),
|
|
8327
|
-
Math.max(0, Math.round(args.outputTokens ?? 0)),
|
|
8328
|
-
Math.max(0, Math.round(args.cachedTokens ?? 0)),
|
|
8329
|
-
Math.max(0, Math.round(args.modelSteps ?? 0)),
|
|
8330
|
-
safeMetadata(args.metadata)
|
|
8331
|
-
]
|
|
8332
|
-
});
|
|
8333
|
-
if (claimed.rowsAffected === 0) {
|
|
8334
|
-
const raced = await eventByKey(settlementKey);
|
|
8335
|
-
if (!raced) throw new Error("billing settlement raced without a readable winner");
|
|
8336
|
-
if (raced.status === "settling") {
|
|
8337
|
-
throw new UnifiedBillingError("settlement_in_progress", "scheduled-run settlement is already in progress", 409);
|
|
8338
|
-
}
|
|
8339
|
-
return mapEvent(raced, await reconcileBalanceMc(user.id, user.balance_mc), true);
|
|
8340
|
-
}
|
|
8341
|
-
const debit = await debitMc(
|
|
8294
|
+
const debit = await debitMcIdempotent(
|
|
8342
8295
|
user.id,
|
|
8343
|
-
|
|
8296
|
+
SCHEDULED_RUN_BASE_MC,
|
|
8344
8297
|
LedgerOperation.SCHEDULED_RUN,
|
|
8345
|
-
"scheduled-run:" + authorization.id
|
|
8298
|
+
"scheduled-run:" + authorization.id + ":base",
|
|
8299
|
+
startKey
|
|
8346
8300
|
);
|
|
8347
8301
|
if (!debit.ok) {
|
|
8348
|
-
const metadata = safeMetadata({ ...args.metadata, reason: "insufficient_credits_at_settlement" });
|
|
8349
8302
|
await getDb().execute({
|
|
8350
|
-
sql: "UPDATE
|
|
8351
|
-
args: [metadata, eventId]
|
|
8352
|
-
});
|
|
8353
|
-
await getDb().execute({
|
|
8354
|
-
sql: "UPDATE billing_authorizations SET status = 'cost_pending', settled_at = datetime('now') WHERE id = ?",
|
|
8303
|
+
sql: "UPDATE billing_authorizations SET status = 'authorized' WHERE id = ? AND status = 'starting'",
|
|
8355
8304
|
args: [authorization.id]
|
|
8356
8305
|
});
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
|
|
8306
|
+
throw new UnifiedBillingError(
|
|
8307
|
+
"insufficient_credits",
|
|
8308
|
+
"the wallet could not capture the scheduled-run base charge",
|
|
8309
|
+
402,
|
|
8310
|
+
{ balanceMc: debit.balance_mc, requiredMc: SCHEDULED_RUN_BASE_MC }
|
|
8311
|
+
);
|
|
8312
|
+
}
|
|
8313
|
+
await getDb().execute({
|
|
8314
|
+
sql: "UPDATE billing_events SET status = 'settled', settled_at = datetime('now') WHERE idempotency_key = ? AND status = 'capturing'",
|
|
8315
|
+
args: [startKey]
|
|
8316
|
+
});
|
|
8317
|
+
await getDb().execute({
|
|
8318
|
+
sql: "UPDATE billing_authorizations SET status = 'started' WHERE id = ? AND status IN ('authorized', 'starting', 'started')",
|
|
8319
|
+
args: [authorization.id]
|
|
8320
|
+
});
|
|
8321
|
+
const started = await eventByKey(startKey);
|
|
8322
|
+
if (!started) throw new Error("scheduled-run base capture completed without an event receipt");
|
|
8323
|
+
return mapStartEvent(started, debit.balance_mc, debit.duplicate);
|
|
8324
|
+
}
|
|
8325
|
+
function validateModelCost(args) {
|
|
8326
|
+
const rawCostUsd = args.rawCostUsd ?? null;
|
|
8327
|
+
if (args.modelCostStatus === "reported" && (rawCostUsd == null || !Number.isFinite(rawCostUsd) || rawCostUsd < 0)) {
|
|
8328
|
+
throw new UnifiedBillingError("invalid_request", "reported model cost must be a finite non-negative number", 400);
|
|
8329
|
+
}
|
|
8330
|
+
if (args.modelCostStatus === "not_applicable" && rawCostUsd != null && rawCostUsd !== 0) {
|
|
8331
|
+
throw new UnifiedBillingError("invalid_request", "not_applicable model cost cannot include a non-zero cost", 400);
|
|
8332
|
+
}
|
|
8333
|
+
return rawCostUsd;
|
|
8334
|
+
}
|
|
8335
|
+
async function recoverSettlement(row, user, authorization, duplicate) {
|
|
8336
|
+
const modelMc = Math.max(0, Number(row.amount_mc));
|
|
8337
|
+
let balanceMc = await reconcileBalanceMc(user.id, user.balance_mc);
|
|
8338
|
+
if (modelMc > 0) {
|
|
8339
|
+
const debit = await debitMcIdempotent(
|
|
8340
|
+
user.id,
|
|
8364
8341
|
modelMc,
|
|
8365
|
-
|
|
8366
|
-
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8342
|
+
LedgerOperation.SCHEDULED_RUN,
|
|
8343
|
+
"scheduled-run:" + authorization.id + ":model",
|
|
8344
|
+
authorization.id + ":model"
|
|
8345
|
+
);
|
|
8346
|
+
balanceMc = debit.balance_mc;
|
|
8347
|
+
if (!debit.ok) {
|
|
8348
|
+
await getDb().execute({
|
|
8349
|
+
sql: "UPDATE billing_events SET status = 'cost_pending', metadata = ? WHERE id = ? AND status = 'settling'",
|
|
8350
|
+
args: [safeMetadata({ reason: "insufficient_credits_for_model_delta", billingPhase: "settlement", chargeMode: "model_delta" }), row.id]
|
|
8351
|
+
});
|
|
8352
|
+
await getDb().execute({
|
|
8353
|
+
sql: "UPDATE billing_authorizations SET status = 'cost_pending' WHERE id = ? AND status IN ('started', 'cost_pending')",
|
|
8354
|
+
args: [authorization.id]
|
|
8355
|
+
});
|
|
8356
|
+
const pending = await eventByKey(authorization.id + ":settlement");
|
|
8357
|
+
if (!pending) throw new Error("scheduled-run pending settlement lost its event");
|
|
8358
|
+
return mapSettlementEvent(pending, balanceMc, duplicate);
|
|
8359
|
+
}
|
|
8371
8360
|
}
|
|
8372
8361
|
await getDb().execute({
|
|
8373
|
-
sql: "UPDATE billing_events SET status = 'settled', settled_at = datetime('now') WHERE id = ?",
|
|
8374
|
-
args: [
|
|
8362
|
+
sql: "UPDATE billing_events SET status = 'settled', settled_at = datetime('now') WHERE id = ? AND status = 'settling'",
|
|
8363
|
+
args: [row.id]
|
|
8375
8364
|
});
|
|
8376
8365
|
await getDb().execute({
|
|
8377
|
-
sql: "UPDATE billing_authorizations SET status = 'settled', settled_at = datetime('now') WHERE id = ?",
|
|
8366
|
+
sql: "UPDATE billing_authorizations SET status = 'settled', settled_at = datetime('now') WHERE id = ? AND status IN ('started', 'cost_pending', 'settled')",
|
|
8378
8367
|
args: [authorization.id]
|
|
8379
8368
|
});
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8369
|
+
const settled = await eventByKey(authorization.id + ":settlement");
|
|
8370
|
+
if (!settled) throw new Error("scheduled-run settlement completed without an event receipt");
|
|
8371
|
+
return mapSettlementEvent(settled, balanceMc, duplicate);
|
|
8372
|
+
}
|
|
8373
|
+
async function settleScheduledRun(args) {
|
|
8374
|
+
const user = await resolveSchedulingUser(args.identity);
|
|
8375
|
+
let authorization = await authorizationById(args.authorizationId);
|
|
8376
|
+
if (!authorization || Number(authorization.user_id) !== user.id) {
|
|
8377
|
+
throw new UnifiedBillingError("authorization_not_found", "billing authorization was not found", 404);
|
|
8378
|
+
}
|
|
8379
|
+
verifyToken(authorization, args.authorizationToken);
|
|
8380
|
+
const rawCostUsd = validateModelCost(args);
|
|
8381
|
+
const startEvent = await eventByKey(authorization.id + ":start");
|
|
8382
|
+
if (!startEvent || startEvent.status !== "settled") {
|
|
8383
|
+
throw new UnifiedBillingError("authorization_closed", "scheduled-run settlement requires a captured start", 409);
|
|
8384
|
+
}
|
|
8385
|
+
if (authorization.status === "starting") {
|
|
8386
|
+
await getDb().execute({
|
|
8387
|
+
sql: "UPDATE billing_authorizations SET status = 'started' WHERE id = ? AND status = 'starting'",
|
|
8388
|
+
args: [authorization.id]
|
|
8389
|
+
});
|
|
8390
|
+
authorization = await authorizationById(authorization.id);
|
|
8391
|
+
}
|
|
8392
|
+
if (!authorization || !["started", "cost_pending", "settled"].includes(authorization.status)) {
|
|
8393
|
+
throw new UnifiedBillingError("authorization_closed", "billing authorization is not open for settlement", 409);
|
|
8394
|
+
}
|
|
8395
|
+
const settlementKey = authorization.id + ":settlement";
|
|
8396
|
+
let existing = await eventByKey(settlementKey);
|
|
8397
|
+
if (existing?.status === "settled") {
|
|
8398
|
+
return mapSettlementEvent(existing, await reconcileBalanceMc(user.id, user.balance_mc), true);
|
|
8399
|
+
}
|
|
8400
|
+
if (existing?.status === "settling") {
|
|
8401
|
+
return recoverSettlement(existing, user, authorization, true);
|
|
8402
|
+
}
|
|
8403
|
+
const hadExistingClaim = Boolean(existing);
|
|
8404
|
+
const rawCostNanos = rawCostUsd == null ? null : Math.round(rawCostUsd * USD_NANOS);
|
|
8405
|
+
const modelMc = rawCostUsd == null ? 0 : vendorCostUsdToMc(rawCostUsd, SCHEDULED_AGENT_LLM_MARKUP_BPS);
|
|
8406
|
+
const maximumModelMc = Math.max(0, Number(authorization.maximum_amount_mc) - SCHEDULED_RUN_BASE_MC);
|
|
8407
|
+
const pendingReason = args.modelCostStatus === "missing" ? "openrouter_cost_missing" : modelMc > maximumModelMc ? "max_credits_per_run_exceeded" : null;
|
|
8408
|
+
const metadata = safeMetadata({
|
|
8409
|
+
...args.metadata,
|
|
8410
|
+
billingPhase: "settlement",
|
|
8411
|
+
chargeMode: "model_delta",
|
|
8412
|
+
...pendingReason ? { reason: pendingReason } : {}
|
|
8413
|
+
});
|
|
8414
|
+
const eventId = existing?.id ?? randomUUID();
|
|
8415
|
+
const status = pendingReason ? "cost_pending" : "settling";
|
|
8416
|
+
const eventValues = [
|
|
8387
8417
|
modelMc,
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8418
|
+
rawCostNanos,
|
|
8419
|
+
SCHEDULED_AGENT_LLM_MARKUP_BPS,
|
|
8420
|
+
Math.max(0, Math.round(args.inputTokens ?? 0)),
|
|
8421
|
+
Math.max(0, Math.round(args.outputTokens ?? 0)),
|
|
8422
|
+
Math.max(0, Math.round(args.cachedTokens ?? 0)),
|
|
8423
|
+
Math.max(0, Math.round(args.modelSteps ?? 0)),
|
|
8424
|
+
metadata
|
|
8425
|
+
];
|
|
8426
|
+
if (existing) {
|
|
8427
|
+
await getDb().execute({
|
|
8428
|
+
sql: "UPDATE billing_events SET status = ?, amount_mc = ?, raw_cost_usd_nanos = ?, multiplier_bps = ?, input_tokens = ?, output_tokens = ?, cached_tokens = ?, model_steps = ?, metadata = ?, settled_at = NULL WHERE id = ? AND status = 'cost_pending'",
|
|
8429
|
+
args: [status, ...eventValues, existing.id]
|
|
8430
|
+
});
|
|
8431
|
+
} else {
|
|
8432
|
+
await getDb().execute({
|
|
8433
|
+
sql: "INSERT OR IGNORE INTO billing_events (id, user_id, authorization_id, idempotency_key, billing_class, source_surface, status, amount_mc, raw_cost_usd_nanos, multiplier_bps, input_tokens, output_tokens, cached_tokens, model_steps, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
8434
|
+
args: [eventId, user.id, authorization.id, settlementKey, SCHEDULED_RUN_BILLING_CLASS, SCHEDULED_RUN_SOURCE_SURFACE, status, ...eventValues]
|
|
8435
|
+
});
|
|
8436
|
+
}
|
|
8437
|
+
existing = await eventByKey(settlementKey);
|
|
8438
|
+
if (!existing) throw new Error("scheduled-run settlement claim was not readable");
|
|
8439
|
+
const duplicateClaim = hadExistingClaim || eventId !== existing.id;
|
|
8440
|
+
if (existing.status === "settled") {
|
|
8441
|
+
return mapSettlementEvent(existing, await reconcileBalanceMc(user.id, user.balance_mc), true);
|
|
8442
|
+
}
|
|
8443
|
+
if (existing.status === "cost_pending") {
|
|
8444
|
+
await getDb().execute({
|
|
8445
|
+
sql: "UPDATE billing_authorizations SET status = 'cost_pending' WHERE id = ? AND status IN ('started', 'cost_pending')",
|
|
8446
|
+
args: [authorization.id]
|
|
8447
|
+
});
|
|
8448
|
+
return mapSettlementEvent(existing, await reconcileBalanceMc(user.id, user.balance_mc), duplicateClaim);
|
|
8449
|
+
}
|
|
8450
|
+
if (existing.status !== "settling") {
|
|
8451
|
+
throw new UnifiedBillingError("settlement_in_progress", "scheduled-run settlement could not be claimed", 409);
|
|
8452
|
+
}
|
|
8453
|
+
return recoverSettlement(existing, user, authorization, duplicateClaim);
|
|
8393
8454
|
}
|
|
8394
8455
|
async function voidScheduledRunAuthorization(args) {
|
|
8395
|
-
const
|
|
8396
|
-
const user = await getUserByEmail(identity);
|
|
8397
|
-
if (!user) throw new UnifiedBillingError("user_not_found", "billing identity was not found", 404);
|
|
8456
|
+
const user = await resolveSchedulingUser(args.identity);
|
|
8398
8457
|
const authorization = await authorizationById(args.authorizationId);
|
|
8399
8458
|
if (!authorization || Number(authorization.user_id) !== user.id) {
|
|
8400
8459
|
throw new UnifiedBillingError("authorization_not_found", "billing authorization was not found", 404);
|
|
8401
8460
|
}
|
|
8402
8461
|
verifyToken(authorization, args.authorizationToken);
|
|
8403
8462
|
if (authorization.status === "authorized") {
|
|
8404
|
-
await getDb().execute({
|
|
8463
|
+
const result = await getDb().execute({
|
|
8405
8464
|
sql: "UPDATE billing_authorizations SET status = 'void', metadata = ?, settled_at = datetime('now') WHERE id = ? AND status = 'authorized'",
|
|
8406
8465
|
args: [safeMetadata({ reason: args.reason ?? "voided_before_execution" }), authorization.id]
|
|
8407
8466
|
});
|
|
8408
|
-
return { ok: true, status: "void" };
|
|
8467
|
+
if (result.rowsAffected > 0) return { ok: true, status: "void" };
|
|
8468
|
+
const raced = await authorizationById(authorization.id);
|
|
8469
|
+
return { ok: true, status: raced?.status ?? "closed" };
|
|
8409
8470
|
}
|
|
8410
8471
|
return { ok: true, status: authorization.status };
|
|
8411
8472
|
}
|
|
@@ -8609,6 +8670,12 @@ var settleSchema = z10.object({
|
|
|
8609
8670
|
modelSteps: z10.number().finite().nonnegative().optional(),
|
|
8610
8671
|
metadata: metadataSchema
|
|
8611
8672
|
});
|
|
8673
|
+
var startSchema = z10.object({
|
|
8674
|
+
identity: z10.string().min(1).max(320),
|
|
8675
|
+
authorizationId: z10.string().uuid(),
|
|
8676
|
+
authorizationToken: z10.string().min(20).max(500),
|
|
8677
|
+
metadata: metadataSchema
|
|
8678
|
+
});
|
|
8612
8679
|
var voidSchema = z10.object({
|
|
8613
8680
|
identity: z10.string().min(1).max(320),
|
|
8614
8681
|
authorizationId: z10.string().uuid(),
|
|
@@ -8704,6 +8771,19 @@ internalBillingApp.post("/scheduled-run/settle", async (c) => {
|
|
|
8704
8771
|
return c.json(response.body, response.status);
|
|
8705
8772
|
}
|
|
8706
8773
|
});
|
|
8774
|
+
internalBillingApp.post("/scheduled-run/start", async (c) => {
|
|
8775
|
+
const parsed = startSchema.safeParse(await c.req.json().catch(() => null));
|
|
8776
|
+
if (!parsed.success) {
|
|
8777
|
+
return c.json({ ok: false, error_code: "invalid_request", error: "invalid scheduled-run start request" }, 400);
|
|
8778
|
+
}
|
|
8779
|
+
try {
|
|
8780
|
+
const start = await startScheduledRun(parsed.data);
|
|
8781
|
+
return c.json({ ok: true, start });
|
|
8782
|
+
} catch (error) {
|
|
8783
|
+
const response = billingError(error);
|
|
8784
|
+
return c.json(response.body, response.status);
|
|
8785
|
+
}
|
|
8786
|
+
});
|
|
8707
8787
|
internalBillingApp.post("/scheduled-run/void", async (c) => {
|
|
8708
8788
|
const parsed = voidSchema.safeParse(await c.req.json().catch(() => null));
|
|
8709
8789
|
if (!parsed.success) {
|
|
@@ -10924,6 +11004,15 @@ function buildPageIntelUrl(body, country) {
|
|
|
10924
11004
|
if (body.pageId?.trim()) return `https://www.facebook.com/ads/library/?active_status=all&ad_type=all&country=${country}&is_targeted_country=false&media_type=all&search_type=page&view_all_page_id=${body.pageId.trim()}`;
|
|
10925
11005
|
return `https://www.facebook.com/ads/library/?active_status=all&ad_type=all&country=${country}&q=${encodeURIComponent(body.query.trim())}&search_type=keyword_unordered`;
|
|
10926
11006
|
}
|
|
11007
|
+
function pageIntelRequestContext(body) {
|
|
11008
|
+
const country = body.country?.trim().toUpperCase() ?? "US";
|
|
11009
|
+
return {
|
|
11010
|
+
country,
|
|
11011
|
+
maxAds: Math.min(200, Math.max(1, body.maxAds ?? 50)),
|
|
11012
|
+
listingUrl: buildPageIntelUrl(body, country),
|
|
11013
|
+
subject: body.pageId ?? body.query ?? body.libraryId ?? ""
|
|
11014
|
+
};
|
|
11015
|
+
}
|
|
10927
11016
|
function normalizeAdvertiserName(value) {
|
|
10928
11017
|
return value.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, " ").trim().replace(/\s+/g, " ");
|
|
10929
11018
|
}
|
|
@@ -11037,16 +11126,14 @@ facebookAdApp.post("/page-intel", createApiKeyAuth(), async (c) => {
|
|
|
11037
11126
|
const parsed = FacebookPageIntelBodySchema.safeParse(raw);
|
|
11038
11127
|
if (!parsed.success) return c.json(invalidRequest(parsed.error.issues[0]?.message ?? "Invalid request"), 400);
|
|
11039
11128
|
const body = parsed.data;
|
|
11040
|
-
const
|
|
11041
|
-
const country = body.country?.trim().toUpperCase() ?? "US";
|
|
11042
|
-
const listingUrl = buildPageIntelUrl(body, country);
|
|
11129
|
+
const { country, maxAds, listingUrl, subject } = pageIntelRequestContext(body);
|
|
11043
11130
|
const fbUser = c.get("user");
|
|
11044
11131
|
const gate = await acquireConcurrencyGate(fbUser, "facebook_page_intel", { reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"), metadata: { pageId: body.pageId, query: body.query, libraryId: body.libraryId, country } });
|
|
11045
11132
|
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
11046
11133
|
const driver = new BrowserDriver();
|
|
11047
11134
|
let refunded = false, debited = false;
|
|
11048
11135
|
try {
|
|
11049
|
-
const { ok: fbOk, balance_mc: fbBal } = await debitMc(fbUser.id, MC_COSTS.fb_ad, LedgerOperation.FB_AD,
|
|
11136
|
+
const { ok: fbOk, balance_mc: fbBal } = await debitMc(fbUser.id, MC_COSTS.fb_ad, LedgerOperation.FB_AD, subject);
|
|
11050
11137
|
if (!fbOk) return c.json(insufficientBalanceResponse(fbBal, MC_COSTS.fb_ad), 402);
|
|
11051
11138
|
debited = true;
|
|
11052
11139
|
await driver.launch(await kernelLaunchOptsResidential());
|
|
@@ -11056,17 +11143,17 @@ facebookAdApp.post("/page-intel", createApiKeyAuth(), async (c) => {
|
|
|
11056
11143
|
if (result.ads.length === 0 && await detectSoftBlock(driver)) {
|
|
11057
11144
|
await creditMc(fbUser.id, MC_COSTS.fb_ad, LedgerOperation.FB_AD_REFUND, "soft-block empty result");
|
|
11058
11145
|
refunded = true;
|
|
11059
|
-
await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "failed", query:
|
|
11146
|
+
await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "failed", query: subject, error: "soft-block: empty result refunded" });
|
|
11060
11147
|
return c.json({ error: "soft-block: no ads returned (refunded)" }, 503);
|
|
11061
11148
|
}
|
|
11062
11149
|
const match = assessPageIntelMatch(body, result);
|
|
11063
11150
|
const output = { ...result, ...match };
|
|
11064
|
-
await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "done", query:
|
|
11151
|
+
await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "done", query: subject, resultCount: result.ads.length, result: output });
|
|
11065
11152
|
return c.json(output);
|
|
11066
11153
|
} catch (err) {
|
|
11067
11154
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11068
11155
|
if (debited && !refunded) await creditMc(fbUser.id, MC_COSTS.fb_ad, LedgerOperation.FB_AD_REFUND, "failed call");
|
|
11069
|
-
await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "failed", query:
|
|
11156
|
+
await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "failed", query: subject, error: msg });
|
|
11070
11157
|
if (msg.toLowerCase().includes("blocked") || msg.toLowerCase().includes("captcha")) {
|
|
11071
11158
|
return c.json({ error: msg }, 503);
|
|
11072
11159
|
}
|
|
@@ -15004,6 +15091,7 @@ workflowApp.post("/cron/dispatch", async (c) => {
|
|
|
15004
15091
|
|
|
15005
15092
|
// src/api/serp-intelligence-routes.ts
|
|
15006
15093
|
import { Hono as Hono16 } from "hono";
|
|
15094
|
+
import { createHash as createHash4, randomUUID as randomUUID3 } from "crypto";
|
|
15007
15095
|
|
|
15008
15096
|
// src/serp-intelligence/page-snapshot-extractor.ts
|
|
15009
15097
|
import { createHash as createHash3 } from "crypto";
|
|
@@ -15397,12 +15485,22 @@ var SerpIntelligencePageSnapshotRequestSchema = z24.object({
|
|
|
15397
15485
|
sourcePosition: z24.number().int().min(1).optional()
|
|
15398
15486
|
}).strict();
|
|
15399
15487
|
var SerpIntelligencePageSnapshotsBodySchema = z24.object({
|
|
15400
|
-
urls: z24.array(SerpIntelligencePublicHttpUrlSchema).min(1).max(25),
|
|
15488
|
+
urls: z24.array(SerpIntelligencePublicHttpUrlSchema).min(1).max(25).optional(),
|
|
15401
15489
|
targets: z24.array(SerpIntelligencePageSnapshotRequestSchema).min(1).max(25).optional(),
|
|
15402
15490
|
maxConcurrency: z24.number().int().min(1).max(5).default(2),
|
|
15403
15491
|
timeoutMs: z24.number().int().min(1e3).max(6e4).default(15e3),
|
|
15404
15492
|
debug: z24.boolean().default(false)
|
|
15405
|
-
}).strict()
|
|
15493
|
+
}).strict().superRefine((body, ctx) => {
|
|
15494
|
+
const hasUrls = Boolean(body.urls?.length);
|
|
15495
|
+
const hasTargets = Boolean(body.targets?.length);
|
|
15496
|
+
if (hasUrls === hasTargets) {
|
|
15497
|
+
ctx.addIssue({
|
|
15498
|
+
code: z24.ZodIssueCode.custom,
|
|
15499
|
+
message: "provide exactly one of urls or targets",
|
|
15500
|
+
path: hasUrls ? ["targets"] : ["urls"]
|
|
15501
|
+
});
|
|
15502
|
+
}
|
|
15503
|
+
});
|
|
15406
15504
|
var SerpIntelligenceAICitationSchema = z24.object({
|
|
15407
15505
|
text: z24.string(),
|
|
15408
15506
|
href: z24.string()
|
|
@@ -15689,14 +15787,24 @@ async function captureSerpIntelligenceSnapshot(rawInput, runtimeOptions = {}) {
|
|
|
15689
15787
|
});
|
|
15690
15788
|
const pageSnapshotLimit = normalizePageSnapshotLimit(parsedInput);
|
|
15691
15789
|
const pageSnapshotTargets = collectPageSnapshotTargets(harvestResult, pageSnapshotLimit);
|
|
15692
|
-
const pageSnapshotArtifacts = pageSnapshotTargets.length > 0 ? (await
|
|
15693
|
-
|
|
15694
|
-
|
|
15695
|
-
|
|
15696
|
-
|
|
15697
|
-
|
|
15698
|
-
|
|
15699
|
-
|
|
15790
|
+
const pageSnapshotArtifacts = pageSnapshotTargets.length > 0 ? (await runWithCostContext(
|
|
15791
|
+
{
|
|
15792
|
+
...currentCostContext() ?? { op: "serp" },
|
|
15793
|
+
subOp: "page_snapshot",
|
|
15794
|
+
// Page snapshot launches have their own fixed product rate. They
|
|
15795
|
+
// must not overwrite the headless/headful mode used to settle the
|
|
15796
|
+
// preceding SERP harvest.
|
|
15797
|
+
headlessSentOut: void 0
|
|
15798
|
+
},
|
|
15799
|
+
() => capturePageSnapshotsFn(pageSnapshotTargets, {
|
|
15800
|
+
kernelApiKey: runtimeOptions.kernelApiKey ?? browserServiceApiKey(),
|
|
15801
|
+
timeoutMs: runtimeOptions.pageSnapshotTimeoutMs,
|
|
15802
|
+
maxConcurrency: runtimeOptions.pageSnapshotMaxConcurrency,
|
|
15803
|
+
debug,
|
|
15804
|
+
includeContent: runtimeOptions.includePageSnapshotContent,
|
|
15805
|
+
maxContentChars: runtimeOptions.maxPageSnapshotContentChars
|
|
15806
|
+
})
|
|
15807
|
+
)).pageSnapshotArtifacts.map(stripPageSnapshotArtifact) : [];
|
|
15700
15808
|
return normalizeHarvestResultForSerpIntelligence({
|
|
15701
15809
|
harvestResult,
|
|
15702
15810
|
attempts,
|
|
@@ -15713,6 +15821,7 @@ var SERP_INTELLIGENCE_RATE_LIMIT = 60;
|
|
|
15713
15821
|
var SERP_INTELLIGENCE_RATE_WINDOW_SECONDS = 60;
|
|
15714
15822
|
var POST_CAPTURE_ROUTE_LABEL = "POST /capture";
|
|
15715
15823
|
var POST_PAGE_SNAPSHOTS_ROUTE_LABEL = "POST /page-snapshots";
|
|
15824
|
+
var SERP_CAPTURE_BILLING_SOURCE = "serp_intelligence_capture";
|
|
15716
15825
|
var serpIntelligenceApp = new Hono16();
|
|
15717
15826
|
serpIntelligenceApp.use("*", createApiKeyAuth());
|
|
15718
15827
|
function structuredError(input) {
|
|
@@ -15752,18 +15861,46 @@ async function enforceSerpIntelligenceRateLimit(c, userId) {
|
|
|
15752
15861
|
);
|
|
15753
15862
|
}
|
|
15754
15863
|
function pageSnapshotTargetsFromBody(body) {
|
|
15755
|
-
return body.targets?.length ? body.targets : body.urls.map((url) => ({
|
|
15864
|
+
return body.targets?.length ? body.targets : (body.urls ?? []).map((url) => ({
|
|
15756
15865
|
url,
|
|
15757
15866
|
sourceKind: "configured_target",
|
|
15758
15867
|
sourcePosition: null
|
|
15759
15868
|
}));
|
|
15760
15869
|
}
|
|
15870
|
+
async function logRequestEventBestEffort(input) {
|
|
15871
|
+
try {
|
|
15872
|
+
await logRequestEvent(input);
|
|
15873
|
+
} catch (error) {
|
|
15874
|
+
console.warn("[serp-intelligence] request audit log failed:", error instanceof Error ? error.message : String(error));
|
|
15875
|
+
}
|
|
15876
|
+
}
|
|
15877
|
+
function captureBillingKeys(userId, suppliedKey, body) {
|
|
15878
|
+
const responseKey = suppliedKey?.trim() || randomUUID3();
|
|
15879
|
+
const requestFingerprint = createHash4("sha256").update(JSON.stringify(body)).digest("hex");
|
|
15880
|
+
const keyDigest = createHash4("sha256").update(String(userId)).update("\0").update(responseKey).digest("hex");
|
|
15881
|
+
return {
|
|
15882
|
+
responseKey,
|
|
15883
|
+
debitKey: `serp-capture:${userId}:${keyDigest}`,
|
|
15884
|
+
debitDescription: `${body.query} [request:${requestFingerprint}]`
|
|
15885
|
+
};
|
|
15886
|
+
}
|
|
15761
15887
|
serpIntelligenceApp.post("/capture", async (c) => {
|
|
15762
15888
|
void POST_CAPTURE_ROUTE_LABEL;
|
|
15763
15889
|
const user = c.get("user");
|
|
15764
15890
|
const raw = await c.req.json().catch(() => ({}));
|
|
15765
15891
|
const parsed = SerpIntelligenceCaptureBodySchema.safeParse(raw);
|
|
15766
15892
|
if (!parsed.success) return c.json(formatZodError(parsed.error), 400);
|
|
15893
|
+
const suppliedIdempotencyKey = c.req.header("idempotency-key") ?? c.req.header("x-idempotency-key");
|
|
15894
|
+
if (suppliedIdempotencyKey && suppliedIdempotencyKey.trim().length > 500) {
|
|
15895
|
+
return c.json(structuredError({
|
|
15896
|
+
error_code: "invalid_request",
|
|
15897
|
+
error_type: "validation_error",
|
|
15898
|
+
message: "Idempotency-Key must be 500 characters or fewer.",
|
|
15899
|
+
retryable: false
|
|
15900
|
+
}), 400);
|
|
15901
|
+
}
|
|
15902
|
+
const billingKeys = captureBillingKeys(user.id, suppliedIdempotencyKey, parsed.data);
|
|
15903
|
+
c.header("Idempotency-Key", billingKeys.responseKey);
|
|
15767
15904
|
const limited = await enforceSerpIntelligenceRateLimit(c, user.id);
|
|
15768
15905
|
if (limited) return limited;
|
|
15769
15906
|
const gate = await acquireConcurrencyGate(user, "serp_intelligence_capture", {
|
|
@@ -15771,51 +15908,130 @@ serpIntelligenceApp.post("/capture", async (c) => {
|
|
|
15771
15908
|
metadata: { query: parsed.data.query, location: parsed.data.location }
|
|
15772
15909
|
});
|
|
15773
15910
|
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
15774
|
-
const
|
|
15775
|
-
|
|
15911
|
+
const pageSnapshotHold = parsed.data.includePageSnapshots ? parsed.data.pageSnapshotLimit * MC_COSTS.page_scrape : 0;
|
|
15912
|
+
const cost = MC_COSTS.serp_headful + pageSnapshotHold;
|
|
15913
|
+
const headlessSentOut = { value: null };
|
|
15776
15914
|
try {
|
|
15777
|
-
const { ok, balance_mc } = await
|
|
15915
|
+
const { ok, balance_mc } = await debitMcIdempotent(
|
|
15778
15916
|
user.id,
|
|
15779
15917
|
cost,
|
|
15780
15918
|
LedgerOperation.SERP,
|
|
15781
|
-
|
|
15919
|
+
billingKeys.debitDescription,
|
|
15920
|
+
billingKeys.debitKey
|
|
15782
15921
|
);
|
|
15783
|
-
if (!ok)
|
|
15784
|
-
|
|
15785
|
-
|
|
15786
|
-
|
|
15787
|
-
|
|
15788
|
-
|
|
15789
|
-
|
|
15922
|
+
if (!ok) {
|
|
15923
|
+
await releaseConcurrencyGate(gate.lockId);
|
|
15924
|
+
return c.json(insufficientBalanceResponse(balance_mc, cost), 402);
|
|
15925
|
+
}
|
|
15926
|
+
} catch (error) {
|
|
15927
|
+
const idempotencyConflict = error instanceof Error && error.message === "idempotent debit key conflicts with another debit";
|
|
15928
|
+
const body = structuredError({
|
|
15929
|
+
error_code: idempotencyConflict ? "idempotency_conflict" : "capture_failed",
|
|
15930
|
+
error_type: idempotencyConflict ? "validation_error" : "capture_error",
|
|
15931
|
+
message: error instanceof Error ? error.message : String(error),
|
|
15932
|
+
retryable: !idempotencyConflict
|
|
15790
15933
|
});
|
|
15791
|
-
await
|
|
15934
|
+
await logRequestEventBestEffort({
|
|
15792
15935
|
userId: user.id,
|
|
15793
|
-
source:
|
|
15794
|
-
status: "
|
|
15936
|
+
source: SERP_CAPTURE_BILLING_SOURCE,
|
|
15937
|
+
status: "failed",
|
|
15795
15938
|
query: parsed.data.query,
|
|
15796
15939
|
location: parsed.data.location,
|
|
15797
|
-
|
|
15798
|
-
result
|
|
15940
|
+
error: body.message
|
|
15799
15941
|
});
|
|
15800
|
-
|
|
15942
|
+
await releaseConcurrencyGate(gate.lockId);
|
|
15943
|
+
return c.json(body, idempotencyConflict ? 409 : 500);
|
|
15944
|
+
}
|
|
15945
|
+
let result;
|
|
15946
|
+
try {
|
|
15947
|
+
const parentCostContext = currentCostContext();
|
|
15948
|
+
result = await runWithCostContext(
|
|
15949
|
+
{
|
|
15950
|
+
...parentCostContext ?? {},
|
|
15951
|
+
op: "serp",
|
|
15952
|
+
userId: Number(user.id),
|
|
15953
|
+
headlessSentOut
|
|
15954
|
+
},
|
|
15955
|
+
() => captureSerpIntelligenceSnapshot(parsed.data, {
|
|
15956
|
+
kernelApiKey: browserServiceApiKey(),
|
|
15957
|
+
kernelProxyId: browserServiceProxyId(),
|
|
15958
|
+
signal: c.req.raw.signal
|
|
15959
|
+
})
|
|
15960
|
+
);
|
|
15801
15961
|
} catch (error) {
|
|
15802
|
-
|
|
15962
|
+
try {
|
|
15963
|
+
await settleDebitMcIdempotent(
|
|
15964
|
+
user.id,
|
|
15965
|
+
billingKeys.debitKey,
|
|
15966
|
+
0,
|
|
15967
|
+
LedgerOperation.SERP_REFUND,
|
|
15968
|
+
"failed serp intelligence capture",
|
|
15969
|
+
SERP_CAPTURE_BILLING_SOURCE
|
|
15970
|
+
);
|
|
15971
|
+
} catch (settlementError) {
|
|
15972
|
+
console.warn(
|
|
15973
|
+
"[serp-intelligence] failed capture refund remains recoverable:",
|
|
15974
|
+
settlementError instanceof Error ? settlementError.message : String(settlementError)
|
|
15975
|
+
);
|
|
15976
|
+
}
|
|
15803
15977
|
const body = error instanceof SerpIntelligenceCaptureError ? error.toJSON() : structuredError({
|
|
15804
15978
|
error_code: "capture_failed",
|
|
15805
15979
|
error_type: "capture_error",
|
|
15806
15980
|
message: error instanceof Error ? error.message : String(error),
|
|
15807
15981
|
retryable: true
|
|
15808
15982
|
});
|
|
15809
|
-
await
|
|
15983
|
+
await logRequestEventBestEffort({
|
|
15810
15984
|
userId: user.id,
|
|
15811
|
-
source:
|
|
15985
|
+
source: SERP_CAPTURE_BILLING_SOURCE,
|
|
15812
15986
|
status: "failed",
|
|
15813
15987
|
query: parsed.data.query,
|
|
15814
15988
|
location: parsed.data.location,
|
|
15815
15989
|
error: "message" in body && typeof body.message === "string" ? body.message : "SERP Intelligence capture failed"
|
|
15816
15990
|
});
|
|
15817
15991
|
const status = error instanceof SerpIntelligenceCaptureError ? error.httpStatus : 500;
|
|
15992
|
+
await releaseConcurrencyGate(gate.lockId);
|
|
15818
15993
|
return c.json(body, status);
|
|
15994
|
+
}
|
|
15995
|
+
try {
|
|
15996
|
+
const requestedActualCost = serpActualCostMc(headlessSentOut.value) + result.pageSnapshotArtifacts.length * MC_COSTS.page_scrape;
|
|
15997
|
+
const settlement = await settleDebitMcIdempotent(
|
|
15998
|
+
user.id,
|
|
15999
|
+
billingKeys.debitKey,
|
|
16000
|
+
requestedActualCost,
|
|
16001
|
+
LedgerOperation.SERP_REFUND,
|
|
16002
|
+
"SERP capture pricing settle",
|
|
16003
|
+
SERP_CAPTURE_BILLING_SOURCE
|
|
16004
|
+
);
|
|
16005
|
+
const settledResult = {
|
|
16006
|
+
...result,
|
|
16007
|
+
billing: { creditsUsed: settlement.final_amount_mc / MC_PER_CREDIT }
|
|
16008
|
+
};
|
|
16009
|
+
await logRequestEventBestEffort({
|
|
16010
|
+
userId: user.id,
|
|
16011
|
+
source: SERP_CAPTURE_BILLING_SOURCE,
|
|
16012
|
+
status: "done",
|
|
16013
|
+
query: parsed.data.query,
|
|
16014
|
+
location: parsed.data.location,
|
|
16015
|
+
resultCount: settledResult.harvestResult.organicResults.length,
|
|
16016
|
+
result: settledResult
|
|
16017
|
+
});
|
|
16018
|
+
return c.json(settledResult);
|
|
16019
|
+
} catch (error) {
|
|
16020
|
+
const body = structuredError({
|
|
16021
|
+
error_code: "capture_failed",
|
|
16022
|
+
error_type: "capture_error",
|
|
16023
|
+
message: error instanceof Error ? error.message : String(error),
|
|
16024
|
+
retryable: true
|
|
16025
|
+
});
|
|
16026
|
+
await logRequestEventBestEffort({
|
|
16027
|
+
userId: user.id,
|
|
16028
|
+
source: SERP_CAPTURE_BILLING_SOURCE,
|
|
16029
|
+
status: "failed",
|
|
16030
|
+
query: parsed.data.query,
|
|
16031
|
+
location: parsed.data.location,
|
|
16032
|
+
error: body.message
|
|
16033
|
+
});
|
|
16034
|
+
return c.json(body, 500);
|
|
15819
16035
|
} finally {
|
|
15820
16036
|
await releaseConcurrencyGate(gate.lockId);
|
|
15821
16037
|
}
|
|
@@ -15851,7 +16067,7 @@ serpIntelligenceApp.post("/page-snapshots", async (c) => {
|
|
|
15851
16067
|
maxConcurrency: parsed.data.maxConcurrency,
|
|
15852
16068
|
debug: parsed.data.debug
|
|
15853
16069
|
});
|
|
15854
|
-
await
|
|
16070
|
+
await logRequestEventBestEffort({
|
|
15855
16071
|
userId: user.id,
|
|
15856
16072
|
source: "serp_intelligence_page_snapshots",
|
|
15857
16073
|
status: "done",
|
|
@@ -15868,7 +16084,7 @@ serpIntelligenceApp.post("/page-snapshots", async (c) => {
|
|
|
15868
16084
|
message: error instanceof Error ? error.message : String(error),
|
|
15869
16085
|
retryable: true
|
|
15870
16086
|
});
|
|
15871
|
-
await
|
|
16087
|
+
await logRequestEventBestEffort({
|
|
15872
16088
|
userId: user.id,
|
|
15873
16089
|
source: "serp_intelligence_page_snapshots",
|
|
15874
16090
|
status: "failed",
|
|
@@ -16513,15 +16729,42 @@ mcpApp.all("/", async (c) => {
|
|
|
16513
16729
|
});
|
|
16514
16730
|
|
|
16515
16731
|
// src/api/browser-agent-routes.ts
|
|
16516
|
-
import { randomUUID as
|
|
16732
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
16517
16733
|
import { Hono as Hono18 } from "hono";
|
|
16518
16734
|
|
|
16519
16735
|
// src/api/browser-agent-db.ts
|
|
16520
|
-
import { randomUUID as
|
|
16736
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
16521
16737
|
var _ready = false;
|
|
16738
|
+
var _migrationPromise = null;
|
|
16522
16739
|
async function migrateBrowserAgent() {
|
|
16523
16740
|
if (_ready) return;
|
|
16741
|
+
if (_migrationPromise) return _migrationPromise;
|
|
16742
|
+
_migrationPromise = runBrowserAgentMigration().finally(() => {
|
|
16743
|
+
_migrationPromise = null;
|
|
16744
|
+
});
|
|
16745
|
+
return _migrationPromise;
|
|
16746
|
+
}
|
|
16747
|
+
async function runBrowserAgentMigration() {
|
|
16524
16748
|
const db = getDb();
|
|
16749
|
+
const existing = await db.execute(`
|
|
16750
|
+
SELECT
|
|
16751
|
+
(SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name IN (
|
|
16752
|
+
'browser_agent_sessions',
|
|
16753
|
+
'browser_agent_actions',
|
|
16754
|
+
'browser_agent_replays',
|
|
16755
|
+
'browser_auth_connections',
|
|
16756
|
+
'browser_agent_extensions'
|
|
16757
|
+
)) = 5
|
|
16758
|
+
AND (SELECT COUNT(*) FROM pragma_table_info('browser_agent_sessions') WHERE name = 'concurrency_lock_id') = 1
|
|
16759
|
+
AND (SELECT COUNT(*) FROM pragma_table_info('browser_auth_connections') WHERE name IN (
|
|
16760
|
+
'account_email', 'note', 'updated_at', 'status', 'browser_agent_session_id'
|
|
16761
|
+
)) = 5
|
|
16762
|
+
AS ready
|
|
16763
|
+
`);
|
|
16764
|
+
if (Number(existing.rows[0]?.ready ?? 0) === 1) {
|
|
16765
|
+
_ready = true;
|
|
16766
|
+
return;
|
|
16767
|
+
}
|
|
16525
16768
|
await db.execute(`
|
|
16526
16769
|
CREATE TABLE IF NOT EXISTS browser_agent_sessions (
|
|
16527
16770
|
id TEXT PRIMARY KEY,
|
|
@@ -16621,7 +16864,7 @@ async function migrateBrowserAgent() {
|
|
|
16621
16864
|
}
|
|
16622
16865
|
async function createExtensionRow(input) {
|
|
16623
16866
|
const db = getDb();
|
|
16624
|
-
const id = `bext_${
|
|
16867
|
+
const id = `bext_${randomUUID4().replace(/-/g, "").slice(0, 20)}`;
|
|
16625
16868
|
await db.execute({
|
|
16626
16869
|
sql: `INSERT INTO browser_agent_extensions (id, user_id, name, backend_id, backend_name, source, source_url, size_bytes)
|
|
16627
16870
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
@@ -16656,7 +16899,7 @@ async function deleteExtensionRow(userId, name) {
|
|
|
16656
16899
|
}
|
|
16657
16900
|
async function createAuthConnectionRow(input) {
|
|
16658
16901
|
const db = getDb();
|
|
16659
|
-
const connectionId = `authc_${
|
|
16902
|
+
const connectionId = `authc_${randomUUID4().replace(/-/g, "").slice(0, 20)}`;
|
|
16660
16903
|
await db.execute({
|
|
16661
16904
|
sql: `INSERT INTO browser_auth_connections (connection_id, domain, profile, account_email, note, status, browser_agent_session_id)
|
|
16662
16905
|
VALUES (?, ?, ?, ?, ?, 'NEEDS_AUTH', ?)`,
|
|
@@ -16690,7 +16933,7 @@ async function listAuthConnectionsByProfile(profile) {
|
|
|
16690
16933
|
}
|
|
16691
16934
|
async function createSessionRow(input) {
|
|
16692
16935
|
const db = getDb();
|
|
16693
|
-
const id = `bas_${
|
|
16936
|
+
const id = `bas_${randomUUID4().replace(/-/g, "").slice(0, 20)}`;
|
|
16694
16937
|
await db.execute({
|
|
16695
16938
|
sql: `INSERT INTO browser_agent_sessions (id, runtime_session_id, live_view_url, cdp_ws_url, status, label, user_id, concurrency_lock_id, last_action_at)
|
|
16696
16939
|
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, datetime('now'))`,
|
|
@@ -16759,7 +17002,7 @@ async function recordAction(input) {
|
|
|
16759
17002
|
sql: `INSERT INTO browser_agent_actions (id, session_id, type, params_json, ok, error)
|
|
16760
17003
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
16761
17004
|
args: [
|
|
16762
|
-
`baa_${
|
|
17005
|
+
`baa_${randomUUID4().replace(/-/g, "").slice(0, 20)}`,
|
|
16763
17006
|
input.sessionId,
|
|
16764
17007
|
input.type,
|
|
16765
17008
|
input.params == null ? null : JSON.stringify(input.params),
|
|
@@ -18447,7 +18690,7 @@ function buildBrowserAgentRoutes() {
|
|
|
18447
18690
|
}
|
|
18448
18691
|
const existing = await getExtensionRow(user.id, name);
|
|
18449
18692
|
if (existing) return c.json({ error: `an extension named "${name}" already exists \u2014 delete it first or pick another name` }, 409);
|
|
18450
|
-
const backendName = `u${user.id}_${
|
|
18693
|
+
const backendName = `u${user.id}_${randomUUID5().replace(/-/g, "")}`;
|
|
18451
18694
|
try {
|
|
18452
18695
|
const imported = await importExtensionFromStore(storeUrl, backendName);
|
|
18453
18696
|
const row = await createExtensionRow({
|
|
@@ -18877,7 +19120,7 @@ async function resolveUser(customerId, emailFallback) {
|
|
|
18877
19120
|
// src/api/oauth-routes.ts
|
|
18878
19121
|
import { Hono as Hono20 } from "hono";
|
|
18879
19122
|
import { getCookie, setCookie } from "hono/cookie";
|
|
18880
|
-
import { createHash as
|
|
19123
|
+
import { createHash as createHash5, randomBytes as randomBytes2, randomUUID as randomUUID6 } from "crypto";
|
|
18881
19124
|
import { importPKCS8, exportJWK, SignJWT } from "jose";
|
|
18882
19125
|
var ISSUER = "https://mcpscraper.dev";
|
|
18883
19126
|
var RESOURCE = () => process.env.MCP_MEMORY_RESOURCE_URL ?? "https://memory.mcpscraper.dev/mcp";
|
|
@@ -18910,7 +19153,7 @@ async function getKeys() {
|
|
|
18910
19153
|
const privateKey = await importPKCS8(pem, "RS256", { extractable: true });
|
|
18911
19154
|
const full = await exportJWK(privateKey);
|
|
18912
19155
|
const publicJwk = { kty: full.kty, n: full.n, e: full.e };
|
|
18913
|
-
const kid =
|
|
19156
|
+
const kid = createHash5("sha256").update(JSON.stringify({ e: publicJwk.e, kty: publicJwk.kty, n: publicJwk.n })).digest("base64url").slice(0, 16);
|
|
18914
19157
|
publicJwk.kid = kid;
|
|
18915
19158
|
publicJwk.alg = "RS256";
|
|
18916
19159
|
publicJwk.use = "sig";
|
|
@@ -19220,12 +19463,12 @@ oauthApp.post("/oauth/authorize", async (c) => {
|
|
|
19220
19463
|
});
|
|
19221
19464
|
function pkceMatches(verifier, challenge) {
|
|
19222
19465
|
if (!verifier) return false;
|
|
19223
|
-
const computed =
|
|
19466
|
+
const computed = createHash5("sha256").update(verifier).digest("base64url");
|
|
19224
19467
|
return computed === challenge;
|
|
19225
19468
|
}
|
|
19226
19469
|
async function mintAccessToken(identity, scope, plan, audience) {
|
|
19227
19470
|
const { privateKey, kid } = await getKeys();
|
|
19228
|
-
return new SignJWT({ scope, plan }).setProtectedHeader({ alg: "RS256", kid }).setIssuer(ISSUER).setSubject(identity).setAudience(audience).setIssuedAt().setJti(
|
|
19471
|
+
return new SignJWT({ scope, plan }).setProtectedHeader({ alg: "RS256", kid }).setIssuer(ISSUER).setSubject(identity).setAudience(audience).setIssuedAt().setJti(randomUUID6()).setExpirationTime(`${ACCESS_TTL_SECONDS}s`).sign(privateKey);
|
|
19229
19472
|
}
|
|
19230
19473
|
function tokenErrorResponse(c, error, description, status) {
|
|
19231
19474
|
return c.json({ error, error_description: description }, status);
|
|
@@ -20571,13 +20814,13 @@ import { getCookie as getCookie2, setCookie as setCookie2, deleteCookie } from "
|
|
|
20571
20814
|
import Stripe2 from "stripe";
|
|
20572
20815
|
|
|
20573
20816
|
// src/api/page-diff.ts
|
|
20574
|
-
import { createHash as
|
|
20817
|
+
import { createHash as createHash6 } from "crypto";
|
|
20575
20818
|
import { diffLines } from "diff";
|
|
20576
20819
|
var MAX_SNAPSHOT_CONTENT_CHARS = 25e4;
|
|
20577
20820
|
var MAX_DIFF_HUNKS = 200;
|
|
20578
20821
|
var MAX_DIFF_LINES_PER_RESPONSE = 2e3;
|
|
20579
20822
|
function sha256Hex(value) {
|
|
20580
|
-
return
|
|
20823
|
+
return createHash6("sha256").update(value).digest("hex");
|
|
20581
20824
|
}
|
|
20582
20825
|
function truncateForStorage(value, maxChars = MAX_SNAPSHOT_CONTENT_CHARS) {
|
|
20583
20826
|
if (value.length <= maxChars) return { value, truncated: false };
|
|
@@ -20684,7 +20927,7 @@ async function persistScrapeBody(user, opts) {
|
|
|
20684
20927
|
}
|
|
20685
20928
|
|
|
20686
20929
|
// src/api/connection-memory-import.ts
|
|
20687
|
-
import { createHash as
|
|
20930
|
+
import { createHash as createHash7 } from "crypto";
|
|
20688
20931
|
var CONNECTION_MEMORY_IMPORT_MAX_ARGS_BYTES = 64 * 1024;
|
|
20689
20932
|
var CONNECTION_MEMORY_IMPORT_MAX_RESULT_BYTES = 1e6;
|
|
20690
20933
|
var CONNECTION_MEMORY_IMPORT_MAX_STRING_CHARS = 256e3;
|
|
@@ -20714,7 +20957,7 @@ function canonicalJson(value) {
|
|
|
20714
20957
|
return JSON.stringify(value);
|
|
20715
20958
|
}
|
|
20716
20959
|
function sha2562(value) {
|
|
20717
|
-
return
|
|
20960
|
+
return createHash7("sha256").update(value).digest("hex");
|
|
20718
20961
|
}
|
|
20719
20962
|
function sensitiveKey(key) {
|
|
20720
20963
|
const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
@@ -20983,7 +21226,7 @@ async function cleanupExpiredScrapeBlobs(maxAgeMs = SCRAPE_BLOB_TTL_MS) {
|
|
|
20983
21226
|
}
|
|
20984
21227
|
|
|
20985
21228
|
// src/api/connected-data-export.ts
|
|
20986
|
-
import { randomUUID as
|
|
21229
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
20987
21230
|
var CONNECTED_DATA_INLINE_BUDGET_BYTES = Number(
|
|
20988
21231
|
process.env.MCP_SCRAPER_CONNECTED_DATA_INLINE_BUDGET_BYTES ?? 5e4
|
|
20989
21232
|
);
|
|
@@ -21108,7 +21351,7 @@ function finitePositive(value, fallback) {
|
|
|
21108
21351
|
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
21109
21352
|
}
|
|
21110
21353
|
async function collectConnectedDataExport(args) {
|
|
21111
|
-
const exportId =
|
|
21354
|
+
const exportId = randomUUID7();
|
|
21112
21355
|
const now = args.now ?? Date.now;
|
|
21113
21356
|
const startedAt = now();
|
|
21114
21357
|
const budgetMs = finitePositive(CONNECTED_DATA_EXPORT_BUDGET_MS, 24e4);
|
|
@@ -21220,12 +21463,6 @@ ${lines.length ? `${lines.join("\n")}
|
|
|
21220
21463
|
}
|
|
21221
21464
|
|
|
21222
21465
|
// src/api/credit-operations.ts
|
|
21223
|
-
async function grantSignupCredit(userId) {
|
|
21224
|
-
if (!FREE_SIGNUP_MC || FREE_SIGNUP_MC <= 0) return;
|
|
21225
|
-
const already = await ledgerExistsForOperation(userId, LedgerOperation.SIGNUP_GRANT);
|
|
21226
|
-
if (already) return;
|
|
21227
|
-
await creditMc(userId, FREE_SIGNUP_MC, LedgerOperation.SIGNUP_GRANT, "Welcome credits");
|
|
21228
|
-
}
|
|
21229
21466
|
async function applyMonthlyFreeRefresh(user) {
|
|
21230
21467
|
return user;
|
|
21231
21468
|
}
|
|
@@ -21437,7 +21674,7 @@ async function dbUsage(identity, plan) {
|
|
|
21437
21674
|
}
|
|
21438
21675
|
|
|
21439
21676
|
// src/api/nango-control.ts
|
|
21440
|
-
import { createHash as
|
|
21677
|
+
import { createHash as createHash8 } from "crypto";
|
|
21441
21678
|
var DEFAULT_NANGO_CONTROL_URL = "https://mcp-scraper-scheduler.vercel.app";
|
|
21442
21679
|
var DISABLED_NANGO_TOOLS = {
|
|
21443
21680
|
"google-mail": /* @__PURE__ */ new Set(["list-filters"]),
|
|
@@ -22036,7 +22273,7 @@ function canonicalJson2(value) {
|
|
|
22036
22273
|
return JSON.stringify(value);
|
|
22037
22274
|
}
|
|
22038
22275
|
function projectedToolSchemaHash(tool) {
|
|
22039
|
-
return
|
|
22276
|
+
return createHash8("sha256").update(canonicalJson2(tool)).digest("hex");
|
|
22040
22277
|
}
|
|
22041
22278
|
async function describeNangoTool(identity, connectionId, tool, fresh) {
|
|
22042
22279
|
const body = await controlRequest("/api/internal/nango/connections/actions/describe", {
|
|
@@ -22637,7 +22874,6 @@ app.use("*", (c, next) => {
|
|
|
22637
22874
|
const forceHeadless = c.req.header("x-kernel-headless") === "true";
|
|
22638
22875
|
return runWithCostContext({ op, probeRunId, forceHeadless }, () => next());
|
|
22639
22876
|
});
|
|
22640
|
-
void migrateCostTelemetry();
|
|
22641
22877
|
app.post("/auth/register", requireAllowedOrigin, async (c) => {
|
|
22642
22878
|
const { email, password } = await c.req.json();
|
|
22643
22879
|
const normalizedEmail = email?.trim().toLowerCase();
|
|
@@ -22668,7 +22904,6 @@ app.post("/auth/register", requireAllowedOrigin, async (c) => {
|
|
|
22668
22904
|
}
|
|
22669
22905
|
}
|
|
22670
22906
|
const newKey = await rotateApiKey(user.id);
|
|
22671
|
-
await grantSignupCredit(user.id);
|
|
22672
22907
|
void (async () => {
|
|
22673
22908
|
try {
|
|
22674
22909
|
const full = await getUserById(user.id);
|
|
@@ -22776,11 +23011,7 @@ app.get("/catalog", (c) => {
|
|
|
22776
23011
|
return c.json(CATALOG);
|
|
22777
23012
|
});
|
|
22778
23013
|
app.get("/rates", (c) => {
|
|
22779
|
-
|
|
22780
|
-
...cost,
|
|
22781
|
-
...cost.notes ? { notes: cost.notes.replace(/ via fal\.ai/g, "") } : {}
|
|
22782
|
-
}));
|
|
22783
|
-
return c.json({ creditsPerDollarStarter: Math.round(1 / 15e-5), costs });
|
|
23014
|
+
return c.json(getPublicRatesPayload());
|
|
22784
23015
|
});
|
|
22785
23016
|
app.get("/me", async (c) => {
|
|
22786
23017
|
const token = getCookie2(c, "session");
|
|
@@ -23851,7 +24082,7 @@ app.post("/harvest", auth2, async (c) => {
|
|
|
23851
24082
|
serpOnly: body.serpOnly ?? false,
|
|
23852
24083
|
pages: Math.min(2, Math.max(1, body.pages ?? 1))
|
|
23853
24084
|
};
|
|
23854
|
-
const harvestCost = options.serpOnly ? MC_COSTS.
|
|
24085
|
+
const harvestCost = options.serpOnly ? MC_COSTS.serp_headful : paaCostForQuestionCount(options.maxQuestions);
|
|
23855
24086
|
const { ok: harvestOk, balance_mc: harvestBal } = await debitMc(user.id, harvestCost, options.serpOnly ? LedgerOperation.SERP : LedgerOperation.PAA, options.query);
|
|
23856
24087
|
if (!harvestOk) return c.json(insufficientBalanceResponse(harvestBal, harvestCost), 402);
|
|
23857
24088
|
const jobId = await createJob(user.id, options.query, { ...options, billingHoldMc: harvestCost }, body.callback_url);
|
|
@@ -23887,7 +24118,7 @@ app.post("/harvest/sync", auth2, async (c) => {
|
|
|
23887
24118
|
serpOnly: body.serpOnly ?? false,
|
|
23888
24119
|
pages: Math.min(2, Math.max(1, body.pages ?? 1))
|
|
23889
24120
|
};
|
|
23890
|
-
const syncCost = options.serpOnly ? MC_COSTS.
|
|
24121
|
+
const syncCost = options.serpOnly ? MC_COSTS.serp_headful : paaCostForQuestionCount(options.maxQuestions);
|
|
23891
24122
|
const { ok: syncOk, balance_mc: syncBal } = await debitMc(user.id, syncCost, options.serpOnly ? LedgerOperation.SERP : LedgerOperation.PAA, options.query);
|
|
23892
24123
|
if (!syncOk) return c.json(insufficientBalanceResponse(syncBal, syncCost), 402);
|
|
23893
24124
|
const jobId = await createRunningJob(user.id, options.query, options);
|
|
@@ -23897,10 +24128,11 @@ app.post("/harvest/sync", auth2, async (c) => {
|
|
|
23897
24128
|
c.req.raw.signal,
|
|
23898
24129
|
AbortSignal.timeout(syncTimeoutMs)
|
|
23899
24130
|
]);
|
|
24131
|
+
const headlessSentOut = { value: null };
|
|
23900
24132
|
try {
|
|
23901
24133
|
const harvestCtx = currentCostContext();
|
|
23902
24134
|
const result = await runWithCostContext(
|
|
23903
|
-
{ ...harvestCtx, op: options.serpOnly ? "serp" : "paa", userId: user.id },
|
|
24135
|
+
{ ...harvestCtx, op: options.serpOnly ? "serp" : "paa", userId: user.id, headlessSentOut },
|
|
23904
24136
|
() => harvest({
|
|
23905
24137
|
...options,
|
|
23906
24138
|
kernelApiKey: browserServiceApiKey(),
|
|
@@ -23919,6 +24151,11 @@ app.post("/harvest/sync", auth2, async (c) => {
|
|
|
23919
24151
|
const diff = syncCost - actualCost;
|
|
23920
24152
|
if (diff > 0) await creditMc(user.id, diff, LedgerOperation.PAA_REFUND, "overestimate refund");
|
|
23921
24153
|
else if (diff < 0) await debitMc(user.id, -diff, LedgerOperation.PAA, options.query);
|
|
24154
|
+
} else {
|
|
24155
|
+
const actualCost = serpActualCostMc(headlessSentOut.value);
|
|
24156
|
+
const diff = syncCost - actualCost;
|
|
24157
|
+
if (diff > 0) await creditMc(user.id, diff, LedgerOperation.SERP_REFUND, "headless-mode pricing settle");
|
|
24158
|
+
else if (diff < 0) await debitMc(user.id, -diff, LedgerOperation.SERP, options.query);
|
|
23922
24159
|
}
|
|
23923
24160
|
return c.json({ job_id: jobId, status: "done", result: sanitizeHarvestResult(result), attempts: sanitizeAttempts(attempts) });
|
|
23924
24161
|
} catch (err) {
|
|
@@ -23994,22 +24231,13 @@ app.delete("/admin/users/:id", adminAuth, async (c) => {
|
|
|
23994
24231
|
});
|
|
23995
24232
|
app.post("/admin/backfill-signup-credits", adminAuth, async (c) => {
|
|
23996
24233
|
const users = await listUsers();
|
|
23997
|
-
|
|
23998
|
-
|
|
23999
|
-
|
|
24000
|
-
|
|
24001
|
-
|
|
24002
|
-
|
|
24003
|
-
|
|
24004
|
-
if (alreadyHasGrant) {
|
|
24005
|
-
skipped++;
|
|
24006
|
-
} else {
|
|
24007
|
-
await grantSignupCredit(user.id);
|
|
24008
|
-
credited++;
|
|
24009
|
-
users_credited.push({ id: user.id, email: user.email });
|
|
24010
|
-
}
|
|
24011
|
-
}
|
|
24012
|
-
return c.json({ processed, credited, skipped, users_credited });
|
|
24234
|
+
return c.json({
|
|
24235
|
+
processed: users.length,
|
|
24236
|
+
credited: 0,
|
|
24237
|
+
skipped: users.length,
|
|
24238
|
+
users_credited: [],
|
|
24239
|
+
retired: true
|
|
24240
|
+
});
|
|
24013
24241
|
});
|
|
24014
24242
|
app.post("/extract-url", auth2, async (c) => {
|
|
24015
24243
|
const raw = await c.req.json().catch(() => ({}));
|
|
@@ -24590,7 +24818,7 @@ app.get("/cron/tick", async (c) => {
|
|
|
24590
24818
|
if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
24591
24819
|
return c.json({ error: "Unauthorized" }, 401);
|
|
24592
24820
|
}
|
|
24593
|
-
const { drainQueue } = await import("./worker-
|
|
24821
|
+
const { drainQueue } = await import("./worker-RV2TYXE3.js");
|
|
24594
24822
|
const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
|
|
24595
24823
|
const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
|
|
24596
24824
|
const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup] = await Promise.all([
|
|
@@ -24618,15 +24846,15 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
|
|
|
24618
24846
|
return c.json({ error: "Unauthorized" }, 401);
|
|
24619
24847
|
}
|
|
24620
24848
|
const jobId = c.req.param("id");
|
|
24621
|
-
const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-
|
|
24622
|
-
const { assembleExtractArtifacts } = await import("./extract-bundle-
|
|
24849
|
+
const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-2QQSL2QC.js");
|
|
24850
|
+
const { assembleExtractArtifacts } = await import("./extract-bundle-NFFVMUMT.js");
|
|
24623
24851
|
const job = await getExtractJob2(jobId);
|
|
24624
24852
|
if (!job) return c.json({ error: "job not found" }, 404);
|
|
24625
24853
|
const stored = await assembleExtractArtifacts(job, {});
|
|
24626
24854
|
await completeExtractJob2(jobId, stored);
|
|
24627
24855
|
let settlement = "already_settled_or_refunded";
|
|
24628
24856
|
if (job.billedMc == null && job.userId != null) {
|
|
24629
|
-
const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-
|
|
24857
|
+
const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-2QQSL2QC.js");
|
|
24630
24858
|
const heldMc = Number(job.options.heldMc ?? 0);
|
|
24631
24859
|
const successful = await countSuccessfulPages2(jobId);
|
|
24632
24860
|
const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
|
|
@@ -24792,4 +25020,4 @@ app.get("/blog/:slug/", (c) => {
|
|
|
24792
25020
|
export {
|
|
24793
25021
|
app
|
|
24794
25022
|
};
|
|
24795
|
-
//# sourceMappingURL=server-
|
|
25023
|
+
//# sourceMappingURL=server-OLOOFN5W.js.map
|