mcp-scraper 0.21.1 → 0.21.3
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 +4 -2
- package/dist/bin/api-server.cjs +749 -399
- 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 +2 -2
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.cjs +17 -11
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +6 -6
- package/dist/bin/paa-harvest.cjs +1 -0
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +3 -3
- package/dist/{chunk-JBME5WJR.js → chunk-2P5VFIJM.js} +2 -2
- package/dist/{chunk-GMZFQRH2.js → chunk-5IU5ZFVO.js} +2 -2
- package/dist/chunk-5IU5ZFVO.js.map +1 -0
- package/dist/{chunk-7BHU4SJQ.js → chunk-7LO6KLKD.js} +14 -10
- package/dist/{chunk-7BHU4SJQ.js.map → chunk-7LO6KLKD.js.map} +1 -1
- package/dist/{chunk-DZBGULWW.js → chunk-H7APH5UG.js} +81 -1
- package/dist/chunk-H7APH5UG.js.map +1 -0
- package/dist/{chunk-ZJDANZTQ.js → chunk-JJ5RMEGX.js} +3 -2
- package/dist/chunk-JJ5RMEGX.js.map +1 -0
- package/dist/{chunk-HXGZMMW5.js → chunk-OBNKOYOE.js} +44 -8
- package/dist/chunk-OBNKOYOE.js.map +1 -0
- package/dist/chunk-VYUWWIVR.js +7 -0
- package/dist/chunk-VYUWWIVR.js.map +1 -0
- package/dist/{chunk-TUSLATZY.js → chunk-XDRHBGJR.js} +3 -3
- package/dist/{chunk-Q44DN6T2.js → chunk-YNHPZU6B.js} +2 -2
- package/dist/{chunk-Q44DN6T2.js.map → chunk-YNHPZU6B.js.map} +1 -1
- package/dist/{db-EONGTNJM.js → db-Z35S224E.js} +4 -2
- package/dist/{extract-bundle-HMCYUVBJ.js → extract-bundle-4RO3YU4Q.js} +2 -2
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -3
- package/dist/{server-FRR4B4Z5.js → server-PBBRSLBP.js} +595 -384
- package/dist/server-PBBRSLBP.js.map +1 -0
- package/dist/{site-extract-repository-YNPXEZCB.js → site-extract-repository-FDH6ZXKT.js} +4 -4
- package/dist/{worker-45WEWCMQ.js → worker-BXJQ7J4N.js} +13 -7
- package/dist/worker-BXJQ7J4N.js.map +1 -0
- package/docs/mcp-tool-manifest.generated.json +28 -10
- package/docs/specs/google-drive-bulk-access-and-mcp-schema-passthrough-spec.md +16 -7
- package/package.json +2 -2
- package/dist/chunk-DZBGULWW.js.map +0 -1
- package/dist/chunk-GMZFQRH2.js.map +0 -1
- package/dist/chunk-HXGZMMW5.js.map +0 -1
- package/dist/chunk-QDUFW45J.js +0 -7
- package/dist/chunk-QDUFW45J.js.map +0 -1
- package/dist/chunk-ZJDANZTQ.js.map +0 -1
- package/dist/server-FRR4B4Z5.js.map +0 -1
- package/dist/worker-45WEWCMQ.js.map +0 -1
- /package/dist/{chunk-JBME5WJR.js.map → chunk-2P5VFIJM.js.map} +0 -0
- /package/dist/{chunk-TUSLATZY.js.map → chunk-XDRHBGJR.js.map} +0 -0
- /package/dist/{db-EONGTNJM.js.map → db-Z35S224E.js.map} +0 -0
- /package/dist/{extract-bundle-HMCYUVBJ.js.map → extract-bundle-4RO3YU4Q.js.map} +0 -0
- /package/dist/{site-extract-repository-YNPXEZCB.js.map → site-extract-repository-FDH6ZXKT.js.map} +0 -0
package/dist/bin/api-server.cjs
CHANGED
|
@@ -3602,6 +3602,7 @@ __export(db_exports, {
|
|
|
3602
3602
|
creditMc: () => creditMc,
|
|
3603
3603
|
deactivateUser: () => deactivateUser,
|
|
3604
3604
|
debitMc: () => debitMc,
|
|
3605
|
+
debitMcIdempotent: () => debitMcIdempotent,
|
|
3605
3606
|
deleteWorkflowSchedule: () => deleteWorkflowSchedule,
|
|
3606
3607
|
expireOldLots: () => expireOldLots,
|
|
3607
3608
|
failJob: () => failJob,
|
|
@@ -3918,6 +3919,19 @@ async function migrate() {
|
|
|
3918
3919
|
`);
|
|
3919
3920
|
await db.execute(`CREATE INDEX IF NOT EXISTS billing_events_user_created_at ON billing_events(user_id, created_at DESC)`);
|
|
3920
3921
|
await db.execute(`CREATE INDEX IF NOT EXISTS billing_events_authorization ON billing_events(authorization_id)`);
|
|
3922
|
+
await db.execute(`
|
|
3923
|
+
CREATE TABLE IF NOT EXISTS billing_debits (
|
|
3924
|
+
idempotency_key TEXT PRIMARY KEY,
|
|
3925
|
+
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
3926
|
+
amount_mc INTEGER NOT NULL,
|
|
3927
|
+
operation TEXT NOT NULL,
|
|
3928
|
+
description TEXT,
|
|
3929
|
+
status TEXT NOT NULL DEFAULT 'claimed',
|
|
3930
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
3931
|
+
applied_at TEXT
|
|
3932
|
+
)
|
|
3933
|
+
`);
|
|
3934
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS billing_debits_user_status ON billing_debits(user_id, status)`);
|
|
3921
3935
|
await db.execute(`
|
|
3922
3936
|
CREATE TABLE IF NOT EXISTS site_audit_jobs (
|
|
3923
3937
|
id TEXT PRIMARY KEY,
|
|
@@ -5289,6 +5303,72 @@ async function debitMc(userId, mc, operation, description) {
|
|
|
5289
5303
|
const res = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
|
|
5290
5304
|
return { ok: true, balance_mc: Number(res.rows[0]?.balance_mc ?? 0) };
|
|
5291
5305
|
}
|
|
5306
|
+
async function debitMcIdempotent(userId, mc, operation, description, idempotencyKey) {
|
|
5307
|
+
if (!Number.isSafeInteger(mc) || mc <= 0) throw new Error("idempotent debit amount must be a positive integer");
|
|
5308
|
+
if (!idempotencyKey || idempotencyKey.length > 500) throw new Error("idempotent debit key is required and must be bounded");
|
|
5309
|
+
const db = getDb();
|
|
5310
|
+
await ensureMigrated(userId);
|
|
5311
|
+
const results = await db.batch([
|
|
5312
|
+
{
|
|
5313
|
+
sql: "UPDATE billing_debits SET amount_mc = ?, description = ? WHERE idempotency_key = ? AND user_id = ? AND operation = ? AND status = 'claimed'",
|
|
5314
|
+
args: [mc, description, idempotencyKey, userId, operation]
|
|
5315
|
+
},
|
|
5316
|
+
{
|
|
5317
|
+
sql: "INSERT OR IGNORE INTO billing_debits (idempotency_key, user_id, amount_mc, operation, description, status) VALUES (?, ?, ?, ?, ?, 'claimed')",
|
|
5318
|
+
args: [idempotencyKey, userId, mc, operation, description]
|
|
5319
|
+
},
|
|
5320
|
+
{
|
|
5321
|
+
sql: "UPDATE users SET balance_mc = balance_mc - ? WHERE id = ? AND balance_mc >= ? AND EXISTS (SELECT 1 FROM billing_debits WHERE idempotency_key = ? AND user_id = ? AND amount_mc = ? AND operation = ? AND status = 'claimed')",
|
|
5322
|
+
args: [mc, userId, mc, idempotencyKey, userId, mc, operation]
|
|
5323
|
+
},
|
|
5324
|
+
{
|
|
5325
|
+
sql: "UPDATE billing_debits SET status = 'applying' WHERE idempotency_key = ? AND user_id = ? AND amount_mc = ? AND operation = ? AND status = 'claimed' AND changes() = 1",
|
|
5326
|
+
args: [idempotencyKey, userId, mc, operation]
|
|
5327
|
+
},
|
|
5328
|
+
{
|
|
5329
|
+
sql: `WITH ordered AS MATERIALIZED (
|
|
5330
|
+
SELECT id,
|
|
5331
|
+
remaining_mc,
|
|
5332
|
+
COALESCE(SUM(remaining_mc) OVER (
|
|
5333
|
+
ORDER BY expires_at ASC, granted_at ASC, id ASC
|
|
5334
|
+
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
|
|
5335
|
+
), 0) AS prior_mc
|
|
5336
|
+
FROM credit_lots
|
|
5337
|
+
WHERE user_id = ? AND remaining_mc > 0 AND expires_at > datetime('now')
|
|
5338
|
+
), allocation AS (
|
|
5339
|
+
SELECT id, MIN(remaining_mc, MAX(0, ? - prior_mc)) AS take_mc
|
|
5340
|
+
FROM ordered
|
|
5341
|
+
)
|
|
5342
|
+
UPDATE credit_lots
|
|
5343
|
+
SET remaining_mc = remaining_mc - COALESCE((SELECT take_mc FROM allocation WHERE allocation.id = credit_lots.id), 0)
|
|
5344
|
+
WHERE id IN (SELECT id FROM allocation WHERE take_mc > 0)
|
|
5345
|
+
AND EXISTS (SELECT 1 FROM billing_debits WHERE idempotency_key = ? AND status = 'applying')`,
|
|
5346
|
+
args: [userId, mc, idempotencyKey]
|
|
5347
|
+
},
|
|
5348
|
+
{
|
|
5349
|
+
sql: "INSERT INTO ledger (user_id, amount_mc, operation, description) SELECT user_id, -amount_mc, operation, description FROM billing_debits WHERE idempotency_key = ? AND status = 'applying'",
|
|
5350
|
+
args: [idempotencyKey]
|
|
5351
|
+
},
|
|
5352
|
+
{
|
|
5353
|
+
sql: "UPDATE billing_debits SET status = 'applied', applied_at = datetime('now') WHERE idempotency_key = ? AND status = 'applying'",
|
|
5354
|
+
args: [idempotencyKey]
|
|
5355
|
+
}
|
|
5356
|
+
], "write");
|
|
5357
|
+
const claim = await db.execute({
|
|
5358
|
+
sql: "SELECT user_id, amount_mc, operation, status FROM billing_debits WHERE idempotency_key = ? LIMIT 1",
|
|
5359
|
+
args: [idempotencyKey]
|
|
5360
|
+
});
|
|
5361
|
+
const row = claim.rows[0];
|
|
5362
|
+
if (!row || Number(row.user_id) !== userId || Number(row.amount_mc) !== mc || String(row.operation) !== operation) {
|
|
5363
|
+
throw new Error("idempotent debit key conflicts with another debit");
|
|
5364
|
+
}
|
|
5365
|
+
const balance = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
|
|
5366
|
+
return {
|
|
5367
|
+
ok: String(row.status) === "applied",
|
|
5368
|
+
balance_mc: Number(balance.rows[0]?.balance_mc ?? 0),
|
|
5369
|
+
duplicate: results[2]?.rowsAffected === 0 && String(row.status) === "applied"
|
|
5370
|
+
};
|
|
5371
|
+
}
|
|
5292
5372
|
async function getLedger(userId, limit = 50) {
|
|
5293
5373
|
const res = await getDb().execute({
|
|
5294
5374
|
sql: "SELECT * FROM ledger WHERE user_id = ? ORDER BY created_at DESC LIMIT ?",
|
|
@@ -8496,6 +8576,18 @@ function vendorCostUsdToMc(costUsd, markupBps) {
|
|
|
8496
8576
|
if (mc > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("calculated mc exceeds the safe integer range");
|
|
8497
8577
|
return Number(mc);
|
|
8498
8578
|
}
|
|
8579
|
+
function getPublicRatesPayload() {
|
|
8580
|
+
const starter = SUBSCRIPTION_TIER_BY_KEY.starter;
|
|
8581
|
+
const costs = CREDIT_COST_CATALOG.map(({ aliases, ...cost }) => ({
|
|
8582
|
+
...cost,
|
|
8583
|
+
...cost.notes ? { notes: cost.notes.replace(/ via fal\.ai/g, "") } : {}
|
|
8584
|
+
}));
|
|
8585
|
+
return {
|
|
8586
|
+
creditsPerDollarStarter: Math.round(mcToCredits(starter.credits_mc) / starter.monthly_usd),
|
|
8587
|
+
costs,
|
|
8588
|
+
scheduling: SCHEDULED_ACTIONS_RATE_POLICY
|
|
8589
|
+
};
|
|
8590
|
+
}
|
|
8499
8591
|
function concurrencySlotBillingInfo() {
|
|
8500
8592
|
const billingUrl = process.env.CONCURRENCY_BILLING_URL ?? "https://mcpscraper.dev/billing";
|
|
8501
8593
|
return {
|
|
@@ -8525,12 +8617,14 @@ function insufficientBalanceResponse(balanceMc, requiredMc) {
|
|
|
8525
8617
|
topup_url: topupUrl
|
|
8526
8618
|
};
|
|
8527
8619
|
}
|
|
8528
|
-
var MC_COSTS, MC_PER_BROWSER_MS, BROWSER_OPEN_MIN_BALANCE_MC, MC_PER_CREDIT, BILLING_MC_PER_USD_NUMERATOR, BILLING_MC_PER_USD_DENOMINATOR, BILLING_BASIS_POINTS, USD_NANOS, SCHEDULED_RUN_BASE_CREDITS, SCHEDULED_RUN_BASE_MC, SCHEDULED_AGENT_LLM_MARKUP_BPS, SCHEDULED_AGENT_LLM_MARKUP, SCHEDULED_RUN_DEFAULT_MAX_CREDITS, CREDIT_COST_CATALOG, CONCURRENCY_PRICE_ID, SUBSCRIPTION_TIERS, SUBSCRIPTION_TIER_BY_KEY, CONCURRENCY_SLOT_PRICE_USD, CONCURRENCY_SLOT_PRICE_CURRENCY, CONCURRENCY_SLOT_PRICE_INTERVAL, CONCURRENCY_SLOT_PRICE_LABEL, CONCURRENCY_SLOT_TERMINAL_COMMAND, FREE_SIGNUP_MC, MEMORY_FREE_QUOTA_BYTES, MEMORY_PLANS, MEMORY_PLAN_QUOTA, MEMORY_MARGIN_MULTIPLE, MEMORY_PRO_COST_BUDGET_USD,
|
|
8620
|
+
var MC_COSTS, MC_PER_BROWSER_MS, BROWSER_OPEN_MIN_BALANCE_MC, MC_PER_CREDIT, BILLING_MC_PER_USD_NUMERATOR, BILLING_MC_PER_USD_DENOMINATOR, BILLING_BASIS_POINTS, USD_NANOS, SCHEDULED_RUN_BASE_CREDITS, SCHEDULED_RUN_BASE_MC, SCHEDULED_AGENT_LLM_MARKUP_BPS, SCHEDULED_AGENT_LLM_MARKUP, SCHEDULED_RUN_DEFAULT_MAX_CREDITS, SCHEDULED_ACTIONS_RATE_POLICY, CREDIT_COST_CATALOG, CONCURRENCY_PRICE_ID, SUBSCRIPTION_TIERS, SUBSCRIPTION_TIER_BY_KEY, CONCURRENCY_SLOT_PRICE_USD, CONCURRENCY_SLOT_PRICE_CURRENCY, CONCURRENCY_SLOT_PRICE_INTERVAL, CONCURRENCY_SLOT_PRICE_LABEL, CONCURRENCY_SLOT_TERMINAL_COMMAND, FREE_SIGNUP_MC, MEMORY_FREE_QUOTA_BYTES, MEMORY_PLANS, MEMORY_PLAN_QUOTA, MEMORY_MARGIN_MULTIPLE, MEMORY_PRO_COST_BUDGET_USD, LedgerOperation, MEMORY_AI_MARGIN_MULTIPLE, SITE_AUDIT_LLM_MARGIN_MULTIPLE, VIDEO_ANALYSIS_LLM_MARGIN_MULTIPLE, MEDIA_TRANSCRIBE_MARGIN_MULTIPLE, MEDIA_TRANSCRIBE_HOLD_MC, MC_PER_USD;
|
|
8529
8621
|
var init_rates = __esm({
|
|
8530
8622
|
"src/api/rates.ts"() {
|
|
8531
8623
|
"use strict";
|
|
8532
8624
|
MC_COSTS = {
|
|
8533
8625
|
serp: 100,
|
|
8626
|
+
serp_headless: 400,
|
|
8627
|
+
serp_headful: 1400,
|
|
8534
8628
|
paa: 300,
|
|
8535
8629
|
paa_base: 5500,
|
|
8536
8630
|
page_scrape: 100,
|
|
@@ -8571,14 +8665,38 @@ var init_rates = __esm({
|
|
|
8571
8665
|
SCHEDULED_AGENT_LLM_MARKUP_BPS = 15e3;
|
|
8572
8666
|
SCHEDULED_AGENT_LLM_MARKUP = SCHEDULED_AGENT_LLM_MARKUP_BPS / BILLING_BASIS_POINTS;
|
|
8573
8667
|
SCHEDULED_RUN_DEFAULT_MAX_CREDITS = 5e3;
|
|
8668
|
+
SCHEDULED_ACTIONS_RATE_POLICY = {
|
|
8669
|
+
billingMode: "credits",
|
|
8670
|
+
entitlement: "paid_plan_included",
|
|
8671
|
+
separateSubscriptionRequired: false,
|
|
8672
|
+
chargeEvent: "started_occurrence",
|
|
8673
|
+
runBaseCredits: SCHEDULED_RUN_BASE_CREDITS,
|
|
8674
|
+
runBaseMc: SCHEDULED_RUN_BASE_MC,
|
|
8675
|
+
llmCostMultiplier: SCHEDULED_AGENT_LLM_MARKUP,
|
|
8676
|
+
llmCostMultiplierBps: SCHEDULED_AGENT_LLM_MARKUP_BPS,
|
|
8677
|
+
modes: {
|
|
8678
|
+
connection_sync: {
|
|
8679
|
+
runBaseCredits: SCHEDULED_RUN_BASE_CREDITS,
|
|
8680
|
+
modelCostBilling: "none",
|
|
8681
|
+
modelCostMultiplier: 0
|
|
8682
|
+
},
|
|
8683
|
+
agent: {
|
|
8684
|
+
runBaseCredits: SCHEDULED_RUN_BASE_CREDITS,
|
|
8685
|
+
modelCostBilling: "actual_reported_cost",
|
|
8686
|
+
modelCostProvider: "openrouter",
|
|
8687
|
+
modelCostMultiplier: SCHEDULED_AGENT_LLM_MARKUP,
|
|
8688
|
+
modelCostMultiplierBps: SCHEDULED_AGENT_LLM_MARKUP_BPS
|
|
8689
|
+
}
|
|
8690
|
+
}
|
|
8691
|
+
};
|
|
8574
8692
|
CREDIT_COST_CATALOG = [
|
|
8575
8693
|
{
|
|
8576
8694
|
key: "serp",
|
|
8577
8695
|
label: "SERP search",
|
|
8578
8696
|
aliases: ["search_serp", "serp", "google search", "organic results"],
|
|
8579
|
-
credits: mcToCredits(MC_COSTS.
|
|
8580
|
-
unit: "per search",
|
|
8581
|
-
notes:
|
|
8697
|
+
credits: mcToCredits(MC_COSTS.serp_headless),
|
|
8698
|
+
unit: "per search (headless); escalates to headful only when needed",
|
|
8699
|
+
notes: `Returns AI Overview, PAA snippet, videos, forums, and local pack. Billed ${mcToCredits(MC_COSTS.serp_headless)} credits when served headless (the common case), ${mcToCredits(MC_COSTS.serp_headful)} credits if an anti-bot escalation forces headful mode (overestimate refunded).`
|
|
8582
8700
|
},
|
|
8583
8701
|
{
|
|
8584
8702
|
key: "paa",
|
|
@@ -8810,9 +8928,6 @@ var init_rates = __esm({
|
|
|
8810
8928
|
MEMORY_PLAN_QUOTA = { free: MEMORY_FREE_QUOTA_BYTES, pro: 5e9, team: 5e10 };
|
|
8811
8929
|
MEMORY_MARGIN_MULTIPLE = 3;
|
|
8812
8930
|
MEMORY_PRO_COST_BUDGET_USD = 19 / MEMORY_MARGIN_MULTIPLE;
|
|
8813
|
-
SCHEDULING_PLANS = {
|
|
8814
|
-
"price_1ToXcHS8aAcsk3TGf3cW7zHx": { price_id: "price_1ToXcHS8aAcsk3TGf3cW7zHx", label: "Scheduled Actions", interval: "month", monthly_usd: 10, quota_per_period: 1e3 }
|
|
8815
|
-
};
|
|
8816
8931
|
LedgerOperation = {
|
|
8817
8932
|
TOPUP: "topup",
|
|
8818
8933
|
SUBSCRIPTION: "subscription",
|
|
@@ -8821,6 +8936,7 @@ var init_rates = __esm({
|
|
|
8821
8936
|
PAA: "paa",
|
|
8822
8937
|
PAA_REFUND: "paa_refund",
|
|
8823
8938
|
SERP: "serp",
|
|
8939
|
+
SERP_REFUND: "serp_refund",
|
|
8824
8940
|
REFUND: "refund",
|
|
8825
8941
|
TRANSCRIPTION: "transcription",
|
|
8826
8942
|
TRANSCRIPTION_HOLD: "transcription_hold",
|
|
@@ -11903,6 +12019,60 @@ var init_internal_memory_routes = __esm({
|
|
|
11903
12019
|
}
|
|
11904
12020
|
});
|
|
11905
12021
|
|
|
12022
|
+
// src/api/scheduling-access.ts
|
|
12023
|
+
function schedulingAccessOverrides() {
|
|
12024
|
+
return new Set(
|
|
12025
|
+
(process.env.SCHEDULING_ACCESS_OVERRIDE_IDENTITIES ?? "").split(",").map((identity) => identity.trim().toLowerCase()).filter(Boolean)
|
|
12026
|
+
);
|
|
12027
|
+
}
|
|
12028
|
+
function hasPaidSchedulingAccess(user) {
|
|
12029
|
+
const tier = typeof user.subscription_tier === "string" ? user.subscription_tier.trim() : "";
|
|
12030
|
+
const subscriptionId = typeof user.subscription_id === "string" ? user.subscription_id.trim() : "";
|
|
12031
|
+
if (tier && subscriptionId) return true;
|
|
12032
|
+
const identity = typeof user.email === "string" ? user.email.trim().toLowerCase() : "";
|
|
12033
|
+
return identity.length > 0 && schedulingAccessOverrides().has(identity);
|
|
12034
|
+
}
|
|
12035
|
+
function paidPlanRequiredPayload() {
|
|
12036
|
+
return {
|
|
12037
|
+
ok: false,
|
|
12038
|
+
enabled: false,
|
|
12039
|
+
code: PAID_PLAN_REQUIRED_CODE,
|
|
12040
|
+
error: PAID_PLAN_REQUIRED_MESSAGE,
|
|
12041
|
+
billingMode: "credits",
|
|
12042
|
+
runBaseCredits: SCHEDULED_RUN_BASE_CREDITS,
|
|
12043
|
+
llmCostMultiplier: SCHEDULED_AGENT_LLM_MARKUP
|
|
12044
|
+
};
|
|
12045
|
+
}
|
|
12046
|
+
function memoryScheduleAccessPayload(upstreamName, user) {
|
|
12047
|
+
if (hasPaidSchedulingAccess(user)) return null;
|
|
12048
|
+
if (upstreamName === "getScheduleStatusTool") return paidPlanRequiredPayload();
|
|
12049
|
+
if (PLAN_GATED_MEMORY_SCHEDULE_TOOLS.has(upstreamName)) {
|
|
12050
|
+
return {
|
|
12051
|
+
ok: false,
|
|
12052
|
+
code: PAID_PLAN_REQUIRED_CODE,
|
|
12053
|
+
error: PAID_PLAN_REQUIRED_MESSAGE
|
|
12054
|
+
};
|
|
12055
|
+
}
|
|
12056
|
+
return null;
|
|
12057
|
+
}
|
|
12058
|
+
function memoryScheduleToolNeedsCredentials(upstreamName) {
|
|
12059
|
+
return upstreamName === "createScheduledActionTool" || upstreamName === "getScheduleLinkTool";
|
|
12060
|
+
}
|
|
12061
|
+
var PAID_PLAN_REQUIRED_CODE, PAID_PLAN_REQUIRED_MESSAGE, PLAN_GATED_MEMORY_SCHEDULE_TOOLS;
|
|
12062
|
+
var init_scheduling_access = __esm({
|
|
12063
|
+
"src/api/scheduling-access.ts"() {
|
|
12064
|
+
"use strict";
|
|
12065
|
+
init_rates();
|
|
12066
|
+
PAID_PLAN_REQUIRED_CODE = "paid_plan_required";
|
|
12067
|
+
PAID_PLAN_REQUIRED_MESSAGE = "Scheduled Actions requires an active MCP Scraper Starter plan or higher.";
|
|
12068
|
+
PLAN_GATED_MEMORY_SCHEDULE_TOOLS = /* @__PURE__ */ new Set([
|
|
12069
|
+
"createScheduledActionTool",
|
|
12070
|
+
"getScheduleLinkTool",
|
|
12071
|
+
"resumeScheduledActionTool"
|
|
12072
|
+
]);
|
|
12073
|
+
}
|
|
12074
|
+
});
|
|
12075
|
+
|
|
11906
12076
|
// src/api/unified-billing.ts
|
|
11907
12077
|
function billingSecret() {
|
|
11908
12078
|
const secret2 = process.env.INTERNAL_BILLING_SECRET ?? process.env.INTERNAL_MEMORY_DEBIT_SECRET;
|
|
@@ -11916,6 +12086,19 @@ function normalizeIdentity(identity) {
|
|
|
11916
12086
|
}
|
|
11917
12087
|
return normalized;
|
|
11918
12088
|
}
|
|
12089
|
+
async function resolveSchedulingUser(identity) {
|
|
12090
|
+
const user = await getUserByEmail(normalizeIdentity(identity));
|
|
12091
|
+
if (!user) throw new UnifiedBillingError("user_not_found", "billing identity was not found", 404);
|
|
12092
|
+
return user;
|
|
12093
|
+
}
|
|
12094
|
+
async function getSchedulingAccess(identity) {
|
|
12095
|
+
const user = await getUserByEmail(normalizeIdentity(identity));
|
|
12096
|
+
if (!user) return { allowed: false, subscriptionTier: null };
|
|
12097
|
+
return {
|
|
12098
|
+
allowed: hasPaidSchedulingAccess(user),
|
|
12099
|
+
subscriptionTier: user.subscription_tier
|
|
12100
|
+
};
|
|
12101
|
+
}
|
|
11919
12102
|
function normalizeIdempotencyKey(value) {
|
|
11920
12103
|
const key = value.trim();
|
|
11921
12104
|
if (!key || key.length > 500) {
|
|
@@ -11940,6 +12123,15 @@ function safeMetadata(value) {
|
|
|
11940
12123
|
if (!value) return "{}";
|
|
11941
12124
|
return JSON.stringify(value).slice(0, 4e3);
|
|
11942
12125
|
}
|
|
12126
|
+
function metadataReason(row) {
|
|
12127
|
+
if (!row.metadata) return void 0;
|
|
12128
|
+
try {
|
|
12129
|
+
const metadata = JSON.parse(row.metadata);
|
|
12130
|
+
return typeof metadata.reason === "string" ? metadata.reason : void 0;
|
|
12131
|
+
} catch {
|
|
12132
|
+
return void 0;
|
|
12133
|
+
}
|
|
12134
|
+
}
|
|
11943
12135
|
function mapAuthorization(row, balanceMc) {
|
|
11944
12136
|
return {
|
|
11945
12137
|
id: row.id,
|
|
@@ -11968,23 +12160,28 @@ async function eventByKey(idempotencyKey) {
|
|
|
11968
12160
|
});
|
|
11969
12161
|
return result.rows[0] ? result.rows[0] : null;
|
|
11970
12162
|
}
|
|
11971
|
-
function
|
|
11972
|
-
|
|
11973
|
-
|
|
11974
|
-
|
|
11975
|
-
|
|
11976
|
-
|
|
11977
|
-
|
|
11978
|
-
|
|
11979
|
-
|
|
12163
|
+
function mapStartEvent(row, balanceMc, duplicate) {
|
|
12164
|
+
return {
|
|
12165
|
+
id: row.id,
|
|
12166
|
+
authorizationId: row.authorization_id,
|
|
12167
|
+
status: "started",
|
|
12168
|
+
amountMc: SCHEDULED_RUN_BASE_MC,
|
|
12169
|
+
credits: SCHEDULED_RUN_BASE_MC / MC_PER_CREDIT,
|
|
12170
|
+
balanceMc,
|
|
12171
|
+
duplicate
|
|
12172
|
+
};
|
|
12173
|
+
}
|
|
12174
|
+
function mapSettlementEvent(row, balanceMc, duplicate) {
|
|
11980
12175
|
const rawNanos = row.raw_cost_usd_nanos == null ? null : Number(row.raw_cost_usd_nanos);
|
|
11981
|
-
const modelMc = Math.max(0, Number(row.amount_mc)
|
|
12176
|
+
const modelMc = Math.max(0, Number(row.amount_mc));
|
|
12177
|
+
const amountMc = SCHEDULED_RUN_BASE_MC + modelMc;
|
|
12178
|
+
const reason = metadataReason(row);
|
|
11982
12179
|
return {
|
|
11983
12180
|
id: row.id,
|
|
11984
12181
|
authorizationId: row.authorization_id,
|
|
11985
12182
|
status: row.status === "settled" ? "settled" : "cost_pending",
|
|
11986
|
-
amountMc
|
|
11987
|
-
credits:
|
|
12183
|
+
amountMc,
|
|
12184
|
+
credits: amountMc / MC_PER_CREDIT,
|
|
11988
12185
|
baseMc: SCHEDULED_RUN_BASE_MC,
|
|
11989
12186
|
modelMc,
|
|
11990
12187
|
rawCostUsd: rawNanos == null ? null : rawNanos / USD_NANOS,
|
|
@@ -11997,14 +12194,16 @@ function mapEvent(row, balanceMc, duplicate) {
|
|
|
11997
12194
|
async function authorizeScheduledRun(args) {
|
|
11998
12195
|
const identity = normalizeIdentity(args.identity);
|
|
11999
12196
|
const idempotencyKey = normalizeIdempotencyKey(args.idempotencyKey);
|
|
12000
|
-
const user = await
|
|
12001
|
-
if (!user)
|
|
12197
|
+
const user = await resolveSchedulingUser(identity);
|
|
12198
|
+
if (!hasPaidSchedulingAccess(user)) {
|
|
12199
|
+
throw new UnifiedBillingError(PAID_PLAN_REQUIRED_CODE, PAID_PLAN_REQUIRED_MESSAGE, 403);
|
|
12200
|
+
}
|
|
12002
12201
|
const pending = await getDb().execute({
|
|
12003
12202
|
sql: "SELECT id FROM billing_events WHERE user_id = ? AND billing_class = ? AND status = 'cost_pending' LIMIT 1",
|
|
12004
12203
|
args: [user.id, SCHEDULED_RUN_BILLING_CLASS]
|
|
12005
12204
|
});
|
|
12006
12205
|
if (pending.rows.length > 0) {
|
|
12007
|
-
throw new UnifiedBillingError("pending_cost", "a previous scheduled run has an unresolved cost", 409);
|
|
12206
|
+
throw new UnifiedBillingError("pending_cost", "a previous scheduled run has an unresolved model cost", 409);
|
|
12008
12207
|
}
|
|
12009
12208
|
const existing = await getDb().execute({
|
|
12010
12209
|
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",
|
|
@@ -12013,14 +12212,9 @@ async function authorizeScheduledRun(args) {
|
|
|
12013
12212
|
if (existing.rows[0]) {
|
|
12014
12213
|
const row = existing.rows[0];
|
|
12015
12214
|
if (Number(row.user_id) !== user.id) {
|
|
12016
|
-
throw new UnifiedBillingError(
|
|
12017
|
-
"invalid_request",
|
|
12018
|
-
"the idempotency key is already in use by another billing identity",
|
|
12019
|
-
409
|
|
12020
|
-
);
|
|
12215
|
+
throw new UnifiedBillingError("invalid_request", "the idempotency key is already in use by another billing identity", 409);
|
|
12021
12216
|
}
|
|
12022
|
-
|
|
12023
|
-
return mapAuthorization(row, balanceMc2);
|
|
12217
|
+
return mapAuthorization(row, await reconcileBalanceMc(user.id, user.balance_mc));
|
|
12024
12218
|
}
|
|
12025
12219
|
const maximumCredits = args.maximumCredits ?? SCHEDULED_RUN_DEFAULT_MAX_CREDITS;
|
|
12026
12220
|
if (!Number.isFinite(maximumCredits) || maximumCredits < SCHEDULED_RUN_BASE_MC / MC_PER_CREDIT) {
|
|
@@ -12041,17 +12235,7 @@ async function authorizeScheduledRun(args) {
|
|
|
12041
12235
|
const expiresAt = new Date(Date.now() + AUTHORIZATION_TTL_MS).toISOString();
|
|
12042
12236
|
const inserted = await getDb().execute({
|
|
12043
12237
|
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', ?, ?, ?, ?, ?)",
|
|
12044
|
-
args: [
|
|
12045
|
-
id,
|
|
12046
|
-
user.id,
|
|
12047
|
-
idempotencyKey,
|
|
12048
|
-
SCHEDULED_RUN_BILLING_CLASS,
|
|
12049
|
-
SCHEDULED_RUN_BASE_MC,
|
|
12050
|
-
maximumMc,
|
|
12051
|
-
tokenHash(token),
|
|
12052
|
-
safeMetadata(args.metadata),
|
|
12053
|
-
expiresAt
|
|
12054
|
-
]
|
|
12238
|
+
args: [id, user.id, idempotencyKey, SCHEDULED_RUN_BILLING_CLASS, SCHEDULED_RUN_BASE_MC, maximumMc, tokenHash(token), safeMetadata(args.metadata), expiresAt]
|
|
12055
12239
|
});
|
|
12056
12240
|
if (inserted.rowsAffected === 0) {
|
|
12057
12241
|
const raced = await getDb().execute({
|
|
@@ -12061,11 +12245,7 @@ async function authorizeScheduledRun(args) {
|
|
|
12061
12245
|
if (!raced.rows[0]) throw new Error("billing authorization insert raced without a readable winner");
|
|
12062
12246
|
const row = raced.rows[0];
|
|
12063
12247
|
if (Number(row.user_id) !== user.id) {
|
|
12064
|
-
throw new UnifiedBillingError(
|
|
12065
|
-
"invalid_request",
|
|
12066
|
-
"the idempotency key is already in use by another billing identity",
|
|
12067
|
-
409
|
|
12068
|
-
);
|
|
12248
|
+
throw new UnifiedBillingError("invalid_request", "the idempotency key is already in use by another billing identity", 409);
|
|
12069
12249
|
}
|
|
12070
12250
|
return mapAuthorization(row, balanceMc);
|
|
12071
12251
|
}
|
|
@@ -12082,180 +12262,235 @@ async function authorizeScheduledRun(args) {
|
|
|
12082
12262
|
reservationMode: "balance_check"
|
|
12083
12263
|
};
|
|
12084
12264
|
}
|
|
12085
|
-
async function
|
|
12086
|
-
const
|
|
12087
|
-
|
|
12088
|
-
if (!user) throw new UnifiedBillingError("user_not_found", "billing identity was not found", 404);
|
|
12089
|
-
const authorization = await authorizationById(args.authorizationId);
|
|
12265
|
+
async function startScheduledRun(args) {
|
|
12266
|
+
const user = await resolveSchedulingUser(args.identity);
|
|
12267
|
+
let authorization = await authorizationById(args.authorizationId);
|
|
12090
12268
|
if (!authorization || Number(authorization.user_id) !== user.id) {
|
|
12091
12269
|
throw new UnifiedBillingError("authorization_not_found", "billing authorization was not found", 404);
|
|
12092
12270
|
}
|
|
12093
12271
|
verifyToken(authorization, args.authorizationToken);
|
|
12094
|
-
const
|
|
12095
|
-
const existing = await eventByKey(
|
|
12096
|
-
if (existing) {
|
|
12097
|
-
|
|
12098
|
-
|
|
12099
|
-
|
|
12100
|
-
}
|
|
12101
|
-
return
|
|
12272
|
+
const startKey = authorization.id + ":start";
|
|
12273
|
+
const existing = await eventByKey(startKey);
|
|
12274
|
+
if (existing?.status === "settled") {
|
|
12275
|
+
await getDb().execute({
|
|
12276
|
+
sql: "UPDATE billing_authorizations SET status = 'started' WHERE id = ? AND status IN ('authorized', 'starting')",
|
|
12277
|
+
args: [authorization.id]
|
|
12278
|
+
});
|
|
12279
|
+
return mapStartEvent(existing, await reconcileBalanceMc(user.id, user.balance_mc), true);
|
|
12102
12280
|
}
|
|
12103
|
-
if (
|
|
12104
|
-
throw new UnifiedBillingError(
|
|
12281
|
+
if (!hasPaidSchedulingAccess(user)) {
|
|
12282
|
+
throw new UnifiedBillingError(PAID_PLAN_REQUIRED_CODE, PAID_PLAN_REQUIRED_MESSAGE, 403);
|
|
12105
12283
|
}
|
|
12106
|
-
if (
|
|
12284
|
+
if (authorization.status === "authorized") {
|
|
12285
|
+
if (Date.parse(authorization.expires_at) <= Date.now()) {
|
|
12286
|
+
await getDb().execute({
|
|
12287
|
+
sql: "UPDATE billing_authorizations SET status = 'expired' WHERE id = ? AND status = 'authorized'",
|
|
12288
|
+
args: [authorization.id]
|
|
12289
|
+
});
|
|
12290
|
+
throw new UnifiedBillingError("authorization_expired", "billing authorization expired before execution started", 409);
|
|
12291
|
+
}
|
|
12107
12292
|
await getDb().execute({
|
|
12108
|
-
sql: "UPDATE billing_authorizations SET status = '
|
|
12293
|
+
sql: "UPDATE billing_authorizations SET status = 'starting' WHERE id = ? AND status = 'authorized'",
|
|
12109
12294
|
args: [authorization.id]
|
|
12110
12295
|
});
|
|
12111
|
-
|
|
12296
|
+
authorization = await authorizationById(authorization.id);
|
|
12112
12297
|
}
|
|
12113
|
-
|
|
12114
|
-
|
|
12115
|
-
throw new UnifiedBillingError("invalid_request", "reported model cost must be a finite non-negative number", 400);
|
|
12116
|
-
}
|
|
12117
|
-
if (args.modelCostStatus === "not_applicable" && rawCostUsd != null && rawCostUsd !== 0) {
|
|
12118
|
-
throw new UnifiedBillingError("invalid_request", "not_applicable model cost cannot include a non-zero cost", 400);
|
|
12298
|
+
if (!authorization || !["starting", "started"].includes(authorization.status)) {
|
|
12299
|
+
throw new UnifiedBillingError("authorization_closed", "billing authorization is no longer startable", 409);
|
|
12119
12300
|
}
|
|
12120
|
-
const eventId = (0, import_node_crypto4.randomUUID)();
|
|
12121
|
-
|
|
12122
|
-
const modelMc = rawCostUsd == null ? 0 : vendorCostUsdToMc(rawCostUsd, SCHEDULED_AGENT_LLM_MARKUP_BPS);
|
|
12123
|
-
const totalMc = SCHEDULED_RUN_BASE_MC + modelMc;
|
|
12124
|
-
const pendingReason = args.modelCostStatus === "missing" ? "openrouter_cost_missing" : totalMc > Number(authorization.maximum_amount_mc) ? "max_credits_per_run_exceeded" : null;
|
|
12125
|
-
if (pendingReason) {
|
|
12301
|
+
const eventId = existing?.id ?? (0, import_node_crypto4.randomUUID)();
|
|
12302
|
+
if (!existing) {
|
|
12126
12303
|
await getDb().execute({
|
|
12127
|
-
sql: "INSERT INTO billing_events (id, user_id, authorization_id, idempotency_key, billing_class, source_surface, status, amount_mc,
|
|
12304
|
+
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', ?, ?, ?)",
|
|
12128
12305
|
args: [
|
|
12129
12306
|
eventId,
|
|
12130
12307
|
user.id,
|
|
12131
12308
|
authorization.id,
|
|
12132
|
-
|
|
12309
|
+
startKey,
|
|
12133
12310
|
SCHEDULED_RUN_BILLING_CLASS,
|
|
12134
12311
|
SCHEDULED_RUN_SOURCE_SURFACE,
|
|
12135
|
-
|
|
12136
|
-
rawCostNanos,
|
|
12312
|
+
SCHEDULED_RUN_BASE_MC,
|
|
12137
12313
|
SCHEDULED_AGENT_LLM_MARKUP_BPS,
|
|
12138
|
-
|
|
12139
|
-
Math.max(0, Math.round(args.outputTokens ?? 0)),
|
|
12140
|
-
Math.max(0, Math.round(args.cachedTokens ?? 0)),
|
|
12141
|
-
Math.max(0, Math.round(args.modelSteps ?? 0)),
|
|
12142
|
-
safeMetadata({ ...args.metadata, reason: pendingReason })
|
|
12314
|
+
safeMetadata({ ...args.metadata, billingPhase: "start", chargeMode: "base_capture" })
|
|
12143
12315
|
]
|
|
12144
12316
|
});
|
|
12145
|
-
await getDb().execute({
|
|
12146
|
-
sql: "UPDATE billing_authorizations SET status = 'cost_pending', settled_at = datetime('now') WHERE id = ?",
|
|
12147
|
-
args: [authorization.id]
|
|
12148
|
-
});
|
|
12149
|
-
return {
|
|
12150
|
-
id: eventId,
|
|
12151
|
-
authorizationId: authorization.id,
|
|
12152
|
-
status: "cost_pending",
|
|
12153
|
-
amountMc: totalMc,
|
|
12154
|
-
credits: totalMc / MC_PER_CREDIT,
|
|
12155
|
-
baseMc: SCHEDULED_RUN_BASE_MC,
|
|
12156
|
-
modelMc,
|
|
12157
|
-
rawCostUsd,
|
|
12158
|
-
multiplierBps: SCHEDULED_AGENT_LLM_MARKUP_BPS,
|
|
12159
|
-
balanceMc: await reconcileBalanceMc(user.id, user.balance_mc),
|
|
12160
|
-
reason: pendingReason,
|
|
12161
|
-
duplicate: false
|
|
12162
|
-
};
|
|
12163
|
-
}
|
|
12164
|
-
const claimed = await getDb().execute({
|
|
12165
|
-
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', ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
12166
|
-
args: [
|
|
12167
|
-
eventId,
|
|
12168
|
-
user.id,
|
|
12169
|
-
authorization.id,
|
|
12170
|
-
settlementKey,
|
|
12171
|
-
SCHEDULED_RUN_BILLING_CLASS,
|
|
12172
|
-
SCHEDULED_RUN_SOURCE_SURFACE,
|
|
12173
|
-
totalMc,
|
|
12174
|
-
rawCostNanos,
|
|
12175
|
-
SCHEDULED_AGENT_LLM_MARKUP_BPS,
|
|
12176
|
-
Math.max(0, Math.round(args.inputTokens ?? 0)),
|
|
12177
|
-
Math.max(0, Math.round(args.outputTokens ?? 0)),
|
|
12178
|
-
Math.max(0, Math.round(args.cachedTokens ?? 0)),
|
|
12179
|
-
Math.max(0, Math.round(args.modelSteps ?? 0)),
|
|
12180
|
-
safeMetadata(args.metadata)
|
|
12181
|
-
]
|
|
12182
|
-
});
|
|
12183
|
-
if (claimed.rowsAffected === 0) {
|
|
12184
|
-
const raced = await eventByKey(settlementKey);
|
|
12185
|
-
if (!raced) throw new Error("billing settlement raced without a readable winner");
|
|
12186
|
-
if (raced.status === "settling") {
|
|
12187
|
-
throw new UnifiedBillingError("settlement_in_progress", "scheduled-run settlement is already in progress", 409);
|
|
12188
|
-
}
|
|
12189
|
-
return mapEvent(raced, await reconcileBalanceMc(user.id, user.balance_mc), true);
|
|
12190
12317
|
}
|
|
12191
|
-
const debit = await
|
|
12318
|
+
const debit = await debitMcIdempotent(
|
|
12192
12319
|
user.id,
|
|
12193
|
-
|
|
12320
|
+
SCHEDULED_RUN_BASE_MC,
|
|
12194
12321
|
LedgerOperation.SCHEDULED_RUN,
|
|
12195
|
-
"scheduled-run:" + authorization.id
|
|
12322
|
+
"scheduled-run:" + authorization.id + ":base",
|
|
12323
|
+
startKey
|
|
12196
12324
|
);
|
|
12197
12325
|
if (!debit.ok) {
|
|
12198
|
-
const metadata = safeMetadata({ ...args.metadata, reason: "insufficient_credits_at_settlement" });
|
|
12199
12326
|
await getDb().execute({
|
|
12200
|
-
sql: "UPDATE
|
|
12201
|
-
args: [metadata, eventId]
|
|
12202
|
-
});
|
|
12203
|
-
await getDb().execute({
|
|
12204
|
-
sql: "UPDATE billing_authorizations SET status = 'cost_pending', settled_at = datetime('now') WHERE id = ?",
|
|
12327
|
+
sql: "UPDATE billing_authorizations SET status = 'authorized' WHERE id = ? AND status = 'starting'",
|
|
12205
12328
|
args: [authorization.id]
|
|
12206
12329
|
});
|
|
12207
|
-
|
|
12208
|
-
|
|
12209
|
-
|
|
12210
|
-
|
|
12211
|
-
|
|
12212
|
-
|
|
12213
|
-
|
|
12330
|
+
throw new UnifiedBillingError(
|
|
12331
|
+
"insufficient_credits",
|
|
12332
|
+
"the wallet could not capture the scheduled-run base charge",
|
|
12333
|
+
402,
|
|
12334
|
+
{ balanceMc: debit.balance_mc, requiredMc: SCHEDULED_RUN_BASE_MC }
|
|
12335
|
+
);
|
|
12336
|
+
}
|
|
12337
|
+
await getDb().execute({
|
|
12338
|
+
sql: "UPDATE billing_events SET status = 'settled', settled_at = datetime('now') WHERE idempotency_key = ? AND status = 'capturing'",
|
|
12339
|
+
args: [startKey]
|
|
12340
|
+
});
|
|
12341
|
+
await getDb().execute({
|
|
12342
|
+
sql: "UPDATE billing_authorizations SET status = 'started' WHERE id = ? AND status IN ('authorized', 'starting', 'started')",
|
|
12343
|
+
args: [authorization.id]
|
|
12344
|
+
});
|
|
12345
|
+
const started = await eventByKey(startKey);
|
|
12346
|
+
if (!started) throw new Error("scheduled-run base capture completed without an event receipt");
|
|
12347
|
+
return mapStartEvent(started, debit.balance_mc, debit.duplicate);
|
|
12348
|
+
}
|
|
12349
|
+
function validateModelCost(args) {
|
|
12350
|
+
const rawCostUsd = args.rawCostUsd ?? null;
|
|
12351
|
+
if (args.modelCostStatus === "reported" && (rawCostUsd == null || !Number.isFinite(rawCostUsd) || rawCostUsd < 0)) {
|
|
12352
|
+
throw new UnifiedBillingError("invalid_request", "reported model cost must be a finite non-negative number", 400);
|
|
12353
|
+
}
|
|
12354
|
+
if (args.modelCostStatus === "not_applicable" && rawCostUsd != null && rawCostUsd !== 0) {
|
|
12355
|
+
throw new UnifiedBillingError("invalid_request", "not_applicable model cost cannot include a non-zero cost", 400);
|
|
12356
|
+
}
|
|
12357
|
+
return rawCostUsd;
|
|
12358
|
+
}
|
|
12359
|
+
async function recoverSettlement(row, user, authorization, duplicate) {
|
|
12360
|
+
const modelMc = Math.max(0, Number(row.amount_mc));
|
|
12361
|
+
let balanceMc = await reconcileBalanceMc(user.id, user.balance_mc);
|
|
12362
|
+
if (modelMc > 0) {
|
|
12363
|
+
const debit = await debitMcIdempotent(
|
|
12364
|
+
user.id,
|
|
12214
12365
|
modelMc,
|
|
12215
|
-
|
|
12216
|
-
|
|
12217
|
-
|
|
12218
|
-
|
|
12219
|
-
|
|
12220
|
-
|
|
12366
|
+
LedgerOperation.SCHEDULED_RUN,
|
|
12367
|
+
"scheduled-run:" + authorization.id + ":model",
|
|
12368
|
+
authorization.id + ":model"
|
|
12369
|
+
);
|
|
12370
|
+
balanceMc = debit.balance_mc;
|
|
12371
|
+
if (!debit.ok) {
|
|
12372
|
+
await getDb().execute({
|
|
12373
|
+
sql: "UPDATE billing_events SET status = 'cost_pending', metadata = ? WHERE id = ? AND status = 'settling'",
|
|
12374
|
+
args: [safeMetadata({ reason: "insufficient_credits_for_model_delta", billingPhase: "settlement", chargeMode: "model_delta" }), row.id]
|
|
12375
|
+
});
|
|
12376
|
+
await getDb().execute({
|
|
12377
|
+
sql: "UPDATE billing_authorizations SET status = 'cost_pending' WHERE id = ? AND status IN ('started', 'cost_pending')",
|
|
12378
|
+
args: [authorization.id]
|
|
12379
|
+
});
|
|
12380
|
+
const pending = await eventByKey(authorization.id + ":settlement");
|
|
12381
|
+
if (!pending) throw new Error("scheduled-run pending settlement lost its event");
|
|
12382
|
+
return mapSettlementEvent(pending, balanceMc, duplicate);
|
|
12383
|
+
}
|
|
12221
12384
|
}
|
|
12222
12385
|
await getDb().execute({
|
|
12223
|
-
sql: "UPDATE billing_events SET status = 'settled', settled_at = datetime('now') WHERE id = ?",
|
|
12224
|
-
args: [
|
|
12386
|
+
sql: "UPDATE billing_events SET status = 'settled', settled_at = datetime('now') WHERE id = ? AND status = 'settling'",
|
|
12387
|
+
args: [row.id]
|
|
12225
12388
|
});
|
|
12226
12389
|
await getDb().execute({
|
|
12227
|
-
sql: "UPDATE billing_authorizations SET status = 'settled', settled_at = datetime('now') WHERE id = ?",
|
|
12390
|
+
sql: "UPDATE billing_authorizations SET status = 'settled', settled_at = datetime('now') WHERE id = ? AND status IN ('started', 'cost_pending', 'settled')",
|
|
12228
12391
|
args: [authorization.id]
|
|
12229
12392
|
});
|
|
12230
|
-
|
|
12231
|
-
|
|
12232
|
-
|
|
12233
|
-
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
|
|
12393
|
+
const settled = await eventByKey(authorization.id + ":settlement");
|
|
12394
|
+
if (!settled) throw new Error("scheduled-run settlement completed without an event receipt");
|
|
12395
|
+
return mapSettlementEvent(settled, balanceMc, duplicate);
|
|
12396
|
+
}
|
|
12397
|
+
async function settleScheduledRun(args) {
|
|
12398
|
+
const user = await resolveSchedulingUser(args.identity);
|
|
12399
|
+
let authorization = await authorizationById(args.authorizationId);
|
|
12400
|
+
if (!authorization || Number(authorization.user_id) !== user.id) {
|
|
12401
|
+
throw new UnifiedBillingError("authorization_not_found", "billing authorization was not found", 404);
|
|
12402
|
+
}
|
|
12403
|
+
verifyToken(authorization, args.authorizationToken);
|
|
12404
|
+
const rawCostUsd = validateModelCost(args);
|
|
12405
|
+
const startEvent = await eventByKey(authorization.id + ":start");
|
|
12406
|
+
if (!startEvent || startEvent.status !== "settled") {
|
|
12407
|
+
throw new UnifiedBillingError("authorization_closed", "scheduled-run settlement requires a captured start", 409);
|
|
12408
|
+
}
|
|
12409
|
+
if (authorization.status === "starting") {
|
|
12410
|
+
await getDb().execute({
|
|
12411
|
+
sql: "UPDATE billing_authorizations SET status = 'started' WHERE id = ? AND status = 'starting'",
|
|
12412
|
+
args: [authorization.id]
|
|
12413
|
+
});
|
|
12414
|
+
authorization = await authorizationById(authorization.id);
|
|
12415
|
+
}
|
|
12416
|
+
if (!authorization || !["started", "cost_pending", "settled"].includes(authorization.status)) {
|
|
12417
|
+
throw new UnifiedBillingError("authorization_closed", "billing authorization is not open for settlement", 409);
|
|
12418
|
+
}
|
|
12419
|
+
const settlementKey = authorization.id + ":settlement";
|
|
12420
|
+
let existing = await eventByKey(settlementKey);
|
|
12421
|
+
if (existing?.status === "settled") {
|
|
12422
|
+
return mapSettlementEvent(existing, await reconcileBalanceMc(user.id, user.balance_mc), true);
|
|
12423
|
+
}
|
|
12424
|
+
if (existing?.status === "settling") {
|
|
12425
|
+
return recoverSettlement(existing, user, authorization, true);
|
|
12426
|
+
}
|
|
12427
|
+
const hadExistingClaim = Boolean(existing);
|
|
12428
|
+
const rawCostNanos = rawCostUsd == null ? null : Math.round(rawCostUsd * USD_NANOS);
|
|
12429
|
+
const modelMc = rawCostUsd == null ? 0 : vendorCostUsdToMc(rawCostUsd, SCHEDULED_AGENT_LLM_MARKUP_BPS);
|
|
12430
|
+
const maximumModelMc = Math.max(0, Number(authorization.maximum_amount_mc) - SCHEDULED_RUN_BASE_MC);
|
|
12431
|
+
const pendingReason = args.modelCostStatus === "missing" ? "openrouter_cost_missing" : modelMc > maximumModelMc ? "max_credits_per_run_exceeded" : null;
|
|
12432
|
+
const metadata = safeMetadata({
|
|
12433
|
+
...args.metadata,
|
|
12434
|
+
billingPhase: "settlement",
|
|
12435
|
+
chargeMode: "model_delta",
|
|
12436
|
+
...pendingReason ? { reason: pendingReason } : {}
|
|
12437
|
+
});
|
|
12438
|
+
const eventId = existing?.id ?? (0, import_node_crypto4.randomUUID)();
|
|
12439
|
+
const status = pendingReason ? "cost_pending" : "settling";
|
|
12440
|
+
const eventValues = [
|
|
12237
12441
|
modelMc,
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
|
|
12442
|
+
rawCostNanos,
|
|
12443
|
+
SCHEDULED_AGENT_LLM_MARKUP_BPS,
|
|
12444
|
+
Math.max(0, Math.round(args.inputTokens ?? 0)),
|
|
12445
|
+
Math.max(0, Math.round(args.outputTokens ?? 0)),
|
|
12446
|
+
Math.max(0, Math.round(args.cachedTokens ?? 0)),
|
|
12447
|
+
Math.max(0, Math.round(args.modelSteps ?? 0)),
|
|
12448
|
+
metadata
|
|
12449
|
+
];
|
|
12450
|
+
if (existing) {
|
|
12451
|
+
await getDb().execute({
|
|
12452
|
+
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'",
|
|
12453
|
+
args: [status, ...eventValues, existing.id]
|
|
12454
|
+
});
|
|
12455
|
+
} else {
|
|
12456
|
+
await getDb().execute({
|
|
12457
|
+
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
12458
|
+
args: [eventId, user.id, authorization.id, settlementKey, SCHEDULED_RUN_BILLING_CLASS, SCHEDULED_RUN_SOURCE_SURFACE, status, ...eventValues]
|
|
12459
|
+
});
|
|
12460
|
+
}
|
|
12461
|
+
existing = await eventByKey(settlementKey);
|
|
12462
|
+
if (!existing) throw new Error("scheduled-run settlement claim was not readable");
|
|
12463
|
+
const duplicateClaim = hadExistingClaim || eventId !== existing.id;
|
|
12464
|
+
if (existing.status === "settled") {
|
|
12465
|
+
return mapSettlementEvent(existing, await reconcileBalanceMc(user.id, user.balance_mc), true);
|
|
12466
|
+
}
|
|
12467
|
+
if (existing.status === "cost_pending") {
|
|
12468
|
+
await getDb().execute({
|
|
12469
|
+
sql: "UPDATE billing_authorizations SET status = 'cost_pending' WHERE id = ? AND status IN ('started', 'cost_pending')",
|
|
12470
|
+
args: [authorization.id]
|
|
12471
|
+
});
|
|
12472
|
+
return mapSettlementEvent(existing, await reconcileBalanceMc(user.id, user.balance_mc), duplicateClaim);
|
|
12473
|
+
}
|
|
12474
|
+
if (existing.status !== "settling") {
|
|
12475
|
+
throw new UnifiedBillingError("settlement_in_progress", "scheduled-run settlement could not be claimed", 409);
|
|
12476
|
+
}
|
|
12477
|
+
return recoverSettlement(existing, user, authorization, duplicateClaim);
|
|
12243
12478
|
}
|
|
12244
12479
|
async function voidScheduledRunAuthorization(args) {
|
|
12245
|
-
const
|
|
12246
|
-
const user = await getUserByEmail(identity);
|
|
12247
|
-
if (!user) throw new UnifiedBillingError("user_not_found", "billing identity was not found", 404);
|
|
12480
|
+
const user = await resolveSchedulingUser(args.identity);
|
|
12248
12481
|
const authorization = await authorizationById(args.authorizationId);
|
|
12249
12482
|
if (!authorization || Number(authorization.user_id) !== user.id) {
|
|
12250
12483
|
throw new UnifiedBillingError("authorization_not_found", "billing authorization was not found", 404);
|
|
12251
12484
|
}
|
|
12252
12485
|
verifyToken(authorization, args.authorizationToken);
|
|
12253
12486
|
if (authorization.status === "authorized") {
|
|
12254
|
-
await getDb().execute({
|
|
12487
|
+
const result = await getDb().execute({
|
|
12255
12488
|
sql: "UPDATE billing_authorizations SET status = 'void', metadata = ?, settled_at = datetime('now') WHERE id = ? AND status = 'authorized'",
|
|
12256
12489
|
args: [safeMetadata({ reason: args.reason ?? "voided_before_execution" }), authorization.id]
|
|
12257
12490
|
});
|
|
12258
|
-
return { ok: true, status: "void" };
|
|
12491
|
+
if (result.rowsAffected > 0) return { ok: true, status: "void" };
|
|
12492
|
+
const raced = await authorizationById(authorization.id);
|
|
12493
|
+
return { ok: true, status: raced?.status ?? "closed" };
|
|
12259
12494
|
}
|
|
12260
12495
|
return { ok: true, status: authorization.status };
|
|
12261
12496
|
}
|
|
@@ -12266,9 +12501,10 @@ var init_unified_billing = __esm({
|
|
|
12266
12501
|
import_node_crypto4 = require("crypto");
|
|
12267
12502
|
init_db();
|
|
12268
12503
|
init_rates();
|
|
12504
|
+
init_scheduling_access();
|
|
12269
12505
|
SCHEDULED_RUN_BILLING_CLASS = "scheduled_agent_run";
|
|
12270
12506
|
SCHEDULED_RUN_SOURCE_SURFACE = "mastra_scheduler";
|
|
12271
|
-
AUTHORIZATION_TTL_MS =
|
|
12507
|
+
AUTHORIZATION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
12272
12508
|
UnifiedBillingError = class extends Error {
|
|
12273
12509
|
constructor(code, message, httpStatus, details = {}) {
|
|
12274
12510
|
super(message);
|
|
@@ -12284,6 +12520,192 @@ var init_unified_billing = __esm({
|
|
|
12284
12520
|
}
|
|
12285
12521
|
});
|
|
12286
12522
|
|
|
12523
|
+
// src/api/session.ts
|
|
12524
|
+
function getSessionSecret() {
|
|
12525
|
+
const configured = process.env.SESSION_SECRET?.trim();
|
|
12526
|
+
if (configured) return configured;
|
|
12527
|
+
if (isProduction()) throw new Error("SESSION_SECRET is not set \u2014 add it to your Vercel env vars (see src/api/env.ts)");
|
|
12528
|
+
return "dev-secret-change-me";
|
|
12529
|
+
}
|
|
12530
|
+
function safeEqualHex(a, b) {
|
|
12531
|
+
if (a.length !== b.length) return false;
|
|
12532
|
+
try {
|
|
12533
|
+
return (0, import_node_crypto5.timingSafeEqual)(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
12534
|
+
} catch {
|
|
12535
|
+
return false;
|
|
12536
|
+
}
|
|
12537
|
+
}
|
|
12538
|
+
function signSession(userId) {
|
|
12539
|
+
const payload = String(userId);
|
|
12540
|
+
const sig = (0, import_node_crypto5.createHmac)("sha256", secret()).update(payload).digest("hex");
|
|
12541
|
+
return `${payload}.${sig}`;
|
|
12542
|
+
}
|
|
12543
|
+
function verifySession(token) {
|
|
12544
|
+
const dot = token.lastIndexOf(".");
|
|
12545
|
+
if (dot === -1) return null;
|
|
12546
|
+
const payload = token.slice(0, dot);
|
|
12547
|
+
const sig = token.slice(dot + 1);
|
|
12548
|
+
const expected = (0, import_node_crypto5.createHmac)("sha256", secret()).update(payload).digest("hex");
|
|
12549
|
+
if (!safeEqualHex(sig, expected)) return null;
|
|
12550
|
+
const id = parseInt(payload);
|
|
12551
|
+
return isNaN(id) ? null : id;
|
|
12552
|
+
}
|
|
12553
|
+
var import_node_crypto5, isProduction, secret;
|
|
12554
|
+
var init_session = __esm({
|
|
12555
|
+
"src/api/session.ts"() {
|
|
12556
|
+
"use strict";
|
|
12557
|
+
import_node_crypto5 = require("crypto");
|
|
12558
|
+
isProduction = () => process.env.NODE_ENV === "production" || process.env.VERCEL === "1";
|
|
12559
|
+
secret = () => getSessionSecret();
|
|
12560
|
+
}
|
|
12561
|
+
});
|
|
12562
|
+
|
|
12563
|
+
// src/api/memory.ts
|
|
12564
|
+
function parseMemoryRpcBody(text) {
|
|
12565
|
+
const dataLine = text.split("\n").find((line) => line.startsWith("data:"));
|
|
12566
|
+
const payload = dataLine ? dataLine.slice(5).trim() : text;
|
|
12567
|
+
return JSON.parse(payload);
|
|
12568
|
+
}
|
|
12569
|
+
function isMemoryOperator(email) {
|
|
12570
|
+
if (!email) return false;
|
|
12571
|
+
const ops = (process.env.MEMORY_OPERATOR_IDENTITIES ?? "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
12572
|
+
return ops.includes(email.trim().toLowerCase());
|
|
12573
|
+
}
|
|
12574
|
+
function encKey() {
|
|
12575
|
+
return (0, import_node_crypto6.scryptSync)(getSessionSecret(), "mcp-memory-key-v1", 32);
|
|
12576
|
+
}
|
|
12577
|
+
function encryptMemoryKey(secret2) {
|
|
12578
|
+
const iv = (0, import_node_crypto6.randomBytes)(12);
|
|
12579
|
+
const cipher = (0, import_node_crypto6.createCipheriv)("aes-256-gcm", encKey(), iv);
|
|
12580
|
+
const enc = Buffer.concat([cipher.update(secret2, "utf8"), cipher.final()]);
|
|
12581
|
+
const tag = cipher.getAuthTag();
|
|
12582
|
+
return `${iv.toString("base64")}:${tag.toString("base64")}:${enc.toString("base64")}`;
|
|
12583
|
+
}
|
|
12584
|
+
function decryptMemoryKey(stored) {
|
|
12585
|
+
try {
|
|
12586
|
+
const [ivB, tagB, dataB] = stored.split(":");
|
|
12587
|
+
const decipher = (0, import_node_crypto6.createDecipheriv)("aes-256-gcm", encKey(), Buffer.from(ivB, "base64"));
|
|
12588
|
+
decipher.setAuthTag(Buffer.from(tagB, "base64"));
|
|
12589
|
+
return Buffer.concat([decipher.update(Buffer.from(dataB, "base64")), decipher.final()]).toString("utf8");
|
|
12590
|
+
} catch {
|
|
12591
|
+
return null;
|
|
12592
|
+
}
|
|
12593
|
+
}
|
|
12594
|
+
async function memoryCall(toolName, args, userMemoryKey) {
|
|
12595
|
+
try {
|
|
12596
|
+
const res = await fetch(`${MEMORY_BASE_URL()}/mcp`, {
|
|
12597
|
+
method: "POST",
|
|
12598
|
+
headers: {
|
|
12599
|
+
"content-type": "application/json",
|
|
12600
|
+
accept: "application/json, text/event-stream",
|
|
12601
|
+
authorization: `Bearer ${userMemoryKey}`
|
|
12602
|
+
},
|
|
12603
|
+
body: JSON.stringify({
|
|
12604
|
+
jsonrpc: "2.0",
|
|
12605
|
+
id: 1,
|
|
12606
|
+
method: "tools/call",
|
|
12607
|
+
params: { name: toolName, arguments: args }
|
|
12608
|
+
})
|
|
12609
|
+
});
|
|
12610
|
+
const text = await res.text();
|
|
12611
|
+
if (!res.ok) return { ok: false, error: `memory ${toolName} failed (${res.status})` };
|
|
12612
|
+
const body = parseMemoryRpcBody(text);
|
|
12613
|
+
if (body.error) return { ok: false, error: body.error.message ?? `memory ${toolName} failed` };
|
|
12614
|
+
if (body.result?.structuredContent) return body.result.structuredContent;
|
|
12615
|
+
const textContent = body.result?.content?.find((c) => c.type === "text")?.text;
|
|
12616
|
+
if (textContent) {
|
|
12617
|
+
try {
|
|
12618
|
+
return JSON.parse(textContent);
|
|
12619
|
+
} catch {
|
|
12620
|
+
return { ok: false, error: textContent };
|
|
12621
|
+
}
|
|
12622
|
+
}
|
|
12623
|
+
return { ok: false, error: `memory ${toolName} returned no result` };
|
|
12624
|
+
} catch (err) {
|
|
12625
|
+
return { ok: false, error: err?.message ?? "memory call failed" };
|
|
12626
|
+
}
|
|
12627
|
+
}
|
|
12628
|
+
function memoryIdentity(user) {
|
|
12629
|
+
return user.email;
|
|
12630
|
+
}
|
|
12631
|
+
function resolveMemoryIdentity(user) {
|
|
12632
|
+
return memoryIdentity(user);
|
|
12633
|
+
}
|
|
12634
|
+
function memoryPlanForReads(user) {
|
|
12635
|
+
if (isMemoryOperator(user.email)) return "unlimited";
|
|
12636
|
+
if (user.memory_plan === "pro" || user.memory_plan === "team") return user.memory_plan;
|
|
12637
|
+
return "free";
|
|
12638
|
+
}
|
|
12639
|
+
async function provisionMemoryForUser(user) {
|
|
12640
|
+
try {
|
|
12641
|
+
const { key } = await getOrCreateUserMemoryKey(user);
|
|
12642
|
+
if (key) await setMemoryProvisioned(user.id, true);
|
|
12643
|
+
} catch {
|
|
12644
|
+
}
|
|
12645
|
+
}
|
|
12646
|
+
function resolveEffectiveMemoryPlan(user) {
|
|
12647
|
+
return user.memory_plan === "pro" || user.memory_plan === "team" ? user.memory_plan : "free";
|
|
12648
|
+
}
|
|
12649
|
+
async function getOrCreateUserMemoryKey(user) {
|
|
12650
|
+
const creds = await getUserMemoryCreds(user.id);
|
|
12651
|
+
if (creds.memory_key) {
|
|
12652
|
+
const decrypted = decryptMemoryKey(creds.memory_key);
|
|
12653
|
+
if (decrypted) return { key: decrypted };
|
|
12654
|
+
}
|
|
12655
|
+
const admin = ADMIN_KEY();
|
|
12656
|
+
if (!admin) return { key: null, error: "memory not configured (MCP_MEMORY_ADMIN_KEY missing)" };
|
|
12657
|
+
const identity = memoryIdentity(user);
|
|
12658
|
+
const plan = isMemoryOperator(user.email) ? "unlimited" : (await getMemoryPlan(user.id)).plan;
|
|
12659
|
+
const provisioned = await import_provision_defaults.provisionDefaultsTool.execute(
|
|
12660
|
+
{ apiKey: admin, granteeIdentity: identity, issueKey: true, plan },
|
|
12661
|
+
{}
|
|
12662
|
+
);
|
|
12663
|
+
if (!provisioned.ok || !provisioned.secret) {
|
|
12664
|
+
return { key: null, error: provisioned.error ?? "failed to provision default vaults" };
|
|
12665
|
+
}
|
|
12666
|
+
await setUserMemoryCreds(user.id, encryptMemoryKey(provisioned.secret), identity);
|
|
12667
|
+
return { key: provisioned.secret };
|
|
12668
|
+
}
|
|
12669
|
+
async function syncMemoryKeyPlan(user, plan) {
|
|
12670
|
+
const effectivePlan = isMemoryOperator(user.email) ? "unlimited" : plan;
|
|
12671
|
+
const { key, error } = await getOrCreateUserMemoryKey(user);
|
|
12672
|
+
if (!key) return { ok: false, error: error ?? "memory unavailable" };
|
|
12673
|
+
const listed = await memoryCall(
|
|
12674
|
+
"listKeysTool",
|
|
12675
|
+
{},
|
|
12676
|
+
key
|
|
12677
|
+
);
|
|
12678
|
+
if (!listed.ok || !listed.keys?.length) return { ok: false, error: listed.error ?? "no memory keys found" };
|
|
12679
|
+
const target = listed.keys[0];
|
|
12680
|
+
if (target.plan === effectivePlan) return { ok: true };
|
|
12681
|
+
const res = await memoryCall("setScopeTool", { keyId: target.keyId, plan: effectivePlan }, key);
|
|
12682
|
+
return { ok: res.ok, error: res.error };
|
|
12683
|
+
}
|
|
12684
|
+
async function syncScheduledActionCredentials(user) {
|
|
12685
|
+
const admin = ADMIN_KEY();
|
|
12686
|
+
if (!admin) return { ok: false, error: "memory not configured (MCP_MEMORY_ADMIN_KEY missing)" };
|
|
12687
|
+
const identity = resolveMemoryIdentity(user);
|
|
12688
|
+
const res = await import_set_schedule_entitlement.setScheduleEntitlementTool.execute({
|
|
12689
|
+
apiKey: admin,
|
|
12690
|
+
granteeIdentity: identity,
|
|
12691
|
+
mcpScraperApiKey: user.api_key
|
|
12692
|
+
}, {});
|
|
12693
|
+
return { ok: res.ok, error: res.error };
|
|
12694
|
+
}
|
|
12695
|
+
var import_node_crypto6, import_provision_defaults, import_set_schedule_entitlement, MEMORY_BASE_URL, ADMIN_KEY;
|
|
12696
|
+
var init_memory = __esm({
|
|
12697
|
+
"src/api/memory.ts"() {
|
|
12698
|
+
"use strict";
|
|
12699
|
+
import_node_crypto6 = require("crypto");
|
|
12700
|
+
init_session();
|
|
12701
|
+
init_db();
|
|
12702
|
+
import_provision_defaults = require("mcpscraper-memory-tools/tools/vaults/provision-defaults");
|
|
12703
|
+
import_set_schedule_entitlement = require("mcpscraper-memory-tools/tools/schedule/set-schedule-entitlement");
|
|
12704
|
+
MEMORY_BASE_URL = () => (process.env.MCP_MEMORY_BASE_URL ?? "https://memory.mcpscraper.dev").replace(/\/$/, "");
|
|
12705
|
+
ADMIN_KEY = () => process.env.MCP_MEMORY_ADMIN_KEY?.trim() ?? "";
|
|
12706
|
+
}
|
|
12707
|
+
});
|
|
12708
|
+
|
|
12287
12709
|
// src/api/internal-billing-routes.ts
|
|
12288
12710
|
function internalRequestAuthorized(header) {
|
|
12289
12711
|
const expected = process.env.INTERNAL_BILLING_SECRET ?? process.env.INTERNAL_MEMORY_DEBIT_SECRET;
|
|
@@ -12307,15 +12729,22 @@ function billingError(error) {
|
|
|
12307
12729
|
status: 500
|
|
12308
12730
|
};
|
|
12309
12731
|
}
|
|
12310
|
-
var import_hono3, import_zod12, internalBillingApp, metadataSchema, authorizeSchema, settleSchema, voidSchema;
|
|
12732
|
+
var import_hono3, import_zod12, internalBillingApp, metadataSchema, schedulingAccessSchema, authorizeSchema, settleSchema, startSchema, voidSchema;
|
|
12311
12733
|
var init_internal_billing_routes = __esm({
|
|
12312
12734
|
"src/api/internal-billing-routes.ts"() {
|
|
12313
12735
|
"use strict";
|
|
12314
12736
|
import_hono3 = require("hono");
|
|
12315
12737
|
import_zod12 = require("zod");
|
|
12316
12738
|
init_unified_billing();
|
|
12739
|
+
init_scheduling_access();
|
|
12740
|
+
init_db();
|
|
12741
|
+
init_memory();
|
|
12317
12742
|
internalBillingApp = new import_hono3.Hono();
|
|
12318
12743
|
metadataSchema = import_zod12.z.record(import_zod12.z.unknown()).optional();
|
|
12744
|
+
schedulingAccessSchema = import_zod12.z.object({
|
|
12745
|
+
identity: import_zod12.z.string().min(1).max(320),
|
|
12746
|
+
provisionCredentials: import_zod12.z.boolean().optional()
|
|
12747
|
+
});
|
|
12319
12748
|
authorizeSchema = import_zod12.z.object({
|
|
12320
12749
|
identity: import_zod12.z.string().min(1).max(320),
|
|
12321
12750
|
idempotencyKey: import_zod12.z.string().min(1).max(500),
|
|
@@ -12334,6 +12763,12 @@ var init_internal_billing_routes = __esm({
|
|
|
12334
12763
|
modelSteps: import_zod12.z.number().finite().nonnegative().optional(),
|
|
12335
12764
|
metadata: metadataSchema
|
|
12336
12765
|
});
|
|
12766
|
+
startSchema = import_zod12.z.object({
|
|
12767
|
+
identity: import_zod12.z.string().min(1).max(320),
|
|
12768
|
+
authorizationId: import_zod12.z.string().uuid(),
|
|
12769
|
+
authorizationToken: import_zod12.z.string().min(20).max(500),
|
|
12770
|
+
metadata: metadataSchema
|
|
12771
|
+
});
|
|
12337
12772
|
voidSchema = import_zod12.z.object({
|
|
12338
12773
|
identity: import_zod12.z.string().min(1).max(320),
|
|
12339
12774
|
authorizationId: import_zod12.z.string().uuid(),
|
|
@@ -12359,6 +12794,41 @@ var init_internal_billing_routes = __esm({
|
|
|
12359
12794
|
return c.json(response.body, response.status);
|
|
12360
12795
|
}
|
|
12361
12796
|
});
|
|
12797
|
+
internalBillingApp.post("/scheduling-access", async (c) => {
|
|
12798
|
+
const parsed = schedulingAccessSchema.safeParse(await c.req.json().catch(() => null));
|
|
12799
|
+
if (!parsed.success) {
|
|
12800
|
+
return c.json({ ok: false, error_code: "invalid_request", error: "invalid scheduling access request" }, 400);
|
|
12801
|
+
}
|
|
12802
|
+
try {
|
|
12803
|
+
const access2 = await getSchedulingAccess(parsed.data.identity);
|
|
12804
|
+
if (access2.allowed && parsed.data.provisionCredentials) {
|
|
12805
|
+
const user = await getUserByEmail(parsed.data.identity.trim().toLowerCase());
|
|
12806
|
+
if (!user) {
|
|
12807
|
+
return c.json({ ok: false, error_code: "user_not_found", error: "billing identity was not found" }, 404);
|
|
12808
|
+
}
|
|
12809
|
+
const credentials = await syncScheduledActionCredentials(user);
|
|
12810
|
+
if (!credentials.ok) {
|
|
12811
|
+
return c.json({
|
|
12812
|
+
ok: false,
|
|
12813
|
+
error_code: "schedule_credentials_unavailable",
|
|
12814
|
+
error: credentials.error ?? "Scheduled Actions credentials are unavailable."
|
|
12815
|
+
}, 503);
|
|
12816
|
+
}
|
|
12817
|
+
}
|
|
12818
|
+
return c.json({
|
|
12819
|
+
ok: true,
|
|
12820
|
+
allowed: access2.allowed,
|
|
12821
|
+
subscriptionTier: access2.subscriptionTier,
|
|
12822
|
+
...access2.allowed ? {} : {
|
|
12823
|
+
error_code: PAID_PLAN_REQUIRED_CODE,
|
|
12824
|
+
error: PAID_PLAN_REQUIRED_MESSAGE
|
|
12825
|
+
}
|
|
12826
|
+
});
|
|
12827
|
+
} catch (error) {
|
|
12828
|
+
const response = billingError(error);
|
|
12829
|
+
return c.json(response.body, response.status);
|
|
12830
|
+
}
|
|
12831
|
+
});
|
|
12362
12832
|
internalBillingApp.post("/scheduled-run/settle", async (c) => {
|
|
12363
12833
|
const parsed = settleSchema.safeParse(await c.req.json().catch(() => null));
|
|
12364
12834
|
if (!parsed.success) {
|
|
@@ -12372,6 +12842,19 @@ var init_internal_billing_routes = __esm({
|
|
|
12372
12842
|
return c.json(response.body, response.status);
|
|
12373
12843
|
}
|
|
12374
12844
|
});
|
|
12845
|
+
internalBillingApp.post("/scheduled-run/start", async (c) => {
|
|
12846
|
+
const parsed = startSchema.safeParse(await c.req.json().catch(() => null));
|
|
12847
|
+
if (!parsed.success) {
|
|
12848
|
+
return c.json({ ok: false, error_code: "invalid_request", error: "invalid scheduled-run start request" }, 400);
|
|
12849
|
+
}
|
|
12850
|
+
try {
|
|
12851
|
+
const start = await startScheduledRun(parsed.data);
|
|
12852
|
+
return c.json({ ok: true, start });
|
|
12853
|
+
} catch (error) {
|
|
12854
|
+
const response = billingError(error);
|
|
12855
|
+
return c.json(response.body, response.status);
|
|
12856
|
+
}
|
|
12857
|
+
});
|
|
12375
12858
|
internalBillingApp.post("/scheduled-run/void", async (c) => {
|
|
12376
12859
|
const parsed = voidSchema.safeParse(await c.req.json().catch(() => null));
|
|
12377
12860
|
if (!parsed.success) {
|
|
@@ -12658,6 +13141,7 @@ var init_BrowserDriver = __esm({
|
|
|
12658
13141
|
else if (config.headlessMode === "headless") useHeadless = true;
|
|
12659
13142
|
else useHeadless = costCtx?.forceHeadless === true || process.env.KERNEL_HEADLESS_DISABLED !== "true" && !!costCtx?.op && HEADLESS_OPS.has(costCtx.op);
|
|
12660
13143
|
this.kernelHeadlessSent = useHeadless ? true : null;
|
|
13144
|
+
if (costCtx?.headlessSentOut) costCtx.headlessSentOut.value = this.kernelHeadlessSent;
|
|
12661
13145
|
const kernelBrowser = await this.kernelClient.browsers.create({
|
|
12662
13146
|
stealth: true,
|
|
12663
13147
|
timeout_seconds: timeoutSeconds,
|
|
@@ -14785,7 +15269,7 @@ async function acquireConcurrencyGate(user, operation, options = {}) {
|
|
|
14785
15269
|
return { ok: true, lockId: null, active: await countActiveUsageForUser(user.id), limit, operation, reused: true };
|
|
14786
15270
|
}
|
|
14787
15271
|
await expireConcurrencyLocksForUser(user.id);
|
|
14788
|
-
const lockId = `cl_${(0,
|
|
15272
|
+
const lockId = `cl_${(0, import_node_crypto7.randomUUID)().replace(/-/g, "").slice(0, 24)}`;
|
|
14789
15273
|
const res = await getDb().execute({
|
|
14790
15274
|
sql: `INSERT INTO concurrency_locks (id, user_id, operation, status, expires_at, metadata)
|
|
14791
15275
|
SELECT ?, ?, ?, 'active', datetime('now', ?), ?
|
|
@@ -14839,11 +15323,11 @@ async function extendConcurrencyGate(lockId, ttlSeconds = DEFAULT_LOCK_TTL_SECON
|
|
|
14839
15323
|
args: [lockTtlModifier(ttlSeconds), lockId]
|
|
14840
15324
|
});
|
|
14841
15325
|
}
|
|
14842
|
-
var
|
|
15326
|
+
var import_node_crypto7, DEFAULT_LOCK_TTL_SECONDS, DEFAULT_RETRY_AFTER_SECONDS, MAX_LOCK_TTL_SECONDS, schemaReady;
|
|
14843
15327
|
var init_concurrency_gates = __esm({
|
|
14844
15328
|
"src/api/concurrency-gates.ts"() {
|
|
14845
15329
|
"use strict";
|
|
14846
|
-
|
|
15330
|
+
import_node_crypto7 = require("crypto");
|
|
14847
15331
|
init_db();
|
|
14848
15332
|
init_rates();
|
|
14849
15333
|
DEFAULT_LOCK_TTL_SECONDS = 15 * 60;
|
|
@@ -17786,194 +18270,6 @@ var init_reddit_routes = __esm({
|
|
|
17786
18270
|
}
|
|
17787
18271
|
});
|
|
17788
18272
|
|
|
17789
|
-
// src/api/session.ts
|
|
17790
|
-
function getSessionSecret() {
|
|
17791
|
-
const configured = process.env.SESSION_SECRET?.trim();
|
|
17792
|
-
if (configured) return configured;
|
|
17793
|
-
if (isProduction()) throw new Error("SESSION_SECRET is not set \u2014 add it to your Vercel env vars (see src/api/env.ts)");
|
|
17794
|
-
return "dev-secret-change-me";
|
|
17795
|
-
}
|
|
17796
|
-
function safeEqualHex(a, b) {
|
|
17797
|
-
if (a.length !== b.length) return false;
|
|
17798
|
-
try {
|
|
17799
|
-
return (0, import_node_crypto6.timingSafeEqual)(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
17800
|
-
} catch {
|
|
17801
|
-
return false;
|
|
17802
|
-
}
|
|
17803
|
-
}
|
|
17804
|
-
function signSession(userId) {
|
|
17805
|
-
const payload = String(userId);
|
|
17806
|
-
const sig = (0, import_node_crypto6.createHmac)("sha256", secret()).update(payload).digest("hex");
|
|
17807
|
-
return `${payload}.${sig}`;
|
|
17808
|
-
}
|
|
17809
|
-
function verifySession(token) {
|
|
17810
|
-
const dot = token.lastIndexOf(".");
|
|
17811
|
-
if (dot === -1) return null;
|
|
17812
|
-
const payload = token.slice(0, dot);
|
|
17813
|
-
const sig = token.slice(dot + 1);
|
|
17814
|
-
const expected = (0, import_node_crypto6.createHmac)("sha256", secret()).update(payload).digest("hex");
|
|
17815
|
-
if (!safeEqualHex(sig, expected)) return null;
|
|
17816
|
-
const id = parseInt(payload);
|
|
17817
|
-
return isNaN(id) ? null : id;
|
|
17818
|
-
}
|
|
17819
|
-
var import_node_crypto6, isProduction, secret;
|
|
17820
|
-
var init_session = __esm({
|
|
17821
|
-
"src/api/session.ts"() {
|
|
17822
|
-
"use strict";
|
|
17823
|
-
import_node_crypto6 = require("crypto");
|
|
17824
|
-
isProduction = () => process.env.NODE_ENV === "production" || process.env.VERCEL === "1";
|
|
17825
|
-
secret = () => getSessionSecret();
|
|
17826
|
-
}
|
|
17827
|
-
});
|
|
17828
|
-
|
|
17829
|
-
// src/api/memory.ts
|
|
17830
|
-
function parseMemoryRpcBody(text) {
|
|
17831
|
-
const dataLine = text.split("\n").find((line) => line.startsWith("data:"));
|
|
17832
|
-
const payload = dataLine ? dataLine.slice(5).trim() : text;
|
|
17833
|
-
return JSON.parse(payload);
|
|
17834
|
-
}
|
|
17835
|
-
function isMemoryOperator(email) {
|
|
17836
|
-
if (!email) return false;
|
|
17837
|
-
const ops = (process.env.MEMORY_OPERATOR_IDENTITIES ?? "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
17838
|
-
return ops.includes(email.trim().toLowerCase());
|
|
17839
|
-
}
|
|
17840
|
-
function encKey() {
|
|
17841
|
-
return (0, import_node_crypto7.scryptSync)(getSessionSecret(), "mcp-memory-key-v1", 32);
|
|
17842
|
-
}
|
|
17843
|
-
function encryptMemoryKey(secret2) {
|
|
17844
|
-
const iv = (0, import_node_crypto7.randomBytes)(12);
|
|
17845
|
-
const cipher = (0, import_node_crypto7.createCipheriv)("aes-256-gcm", encKey(), iv);
|
|
17846
|
-
const enc = Buffer.concat([cipher.update(secret2, "utf8"), cipher.final()]);
|
|
17847
|
-
const tag = cipher.getAuthTag();
|
|
17848
|
-
return `${iv.toString("base64")}:${tag.toString("base64")}:${enc.toString("base64")}`;
|
|
17849
|
-
}
|
|
17850
|
-
function decryptMemoryKey(stored) {
|
|
17851
|
-
try {
|
|
17852
|
-
const [ivB, tagB, dataB] = stored.split(":");
|
|
17853
|
-
const decipher = (0, import_node_crypto7.createDecipheriv)("aes-256-gcm", encKey(), Buffer.from(ivB, "base64"));
|
|
17854
|
-
decipher.setAuthTag(Buffer.from(tagB, "base64"));
|
|
17855
|
-
return Buffer.concat([decipher.update(Buffer.from(dataB, "base64")), decipher.final()]).toString("utf8");
|
|
17856
|
-
} catch {
|
|
17857
|
-
return null;
|
|
17858
|
-
}
|
|
17859
|
-
}
|
|
17860
|
-
async function memoryCall(toolName, args, userMemoryKey) {
|
|
17861
|
-
try {
|
|
17862
|
-
const res = await fetch(`${MEMORY_BASE_URL()}/mcp`, {
|
|
17863
|
-
method: "POST",
|
|
17864
|
-
headers: {
|
|
17865
|
-
"content-type": "application/json",
|
|
17866
|
-
accept: "application/json, text/event-stream",
|
|
17867
|
-
authorization: `Bearer ${userMemoryKey}`
|
|
17868
|
-
},
|
|
17869
|
-
body: JSON.stringify({
|
|
17870
|
-
jsonrpc: "2.0",
|
|
17871
|
-
id: 1,
|
|
17872
|
-
method: "tools/call",
|
|
17873
|
-
params: { name: toolName, arguments: args }
|
|
17874
|
-
})
|
|
17875
|
-
});
|
|
17876
|
-
const text = await res.text();
|
|
17877
|
-
if (!res.ok) return { ok: false, error: `memory ${toolName} failed (${res.status})` };
|
|
17878
|
-
const body = parseMemoryRpcBody(text);
|
|
17879
|
-
if (body.error) return { ok: false, error: body.error.message ?? `memory ${toolName} failed` };
|
|
17880
|
-
if (body.result?.structuredContent) return body.result.structuredContent;
|
|
17881
|
-
const textContent = body.result?.content?.find((c) => c.type === "text")?.text;
|
|
17882
|
-
if (textContent) {
|
|
17883
|
-
try {
|
|
17884
|
-
return JSON.parse(textContent);
|
|
17885
|
-
} catch {
|
|
17886
|
-
return { ok: false, error: textContent };
|
|
17887
|
-
}
|
|
17888
|
-
}
|
|
17889
|
-
return { ok: false, error: `memory ${toolName} returned no result` };
|
|
17890
|
-
} catch (err) {
|
|
17891
|
-
return { ok: false, error: err?.message ?? "memory call failed" };
|
|
17892
|
-
}
|
|
17893
|
-
}
|
|
17894
|
-
function memoryIdentity(user) {
|
|
17895
|
-
return user.email;
|
|
17896
|
-
}
|
|
17897
|
-
function resolveMemoryIdentity(user) {
|
|
17898
|
-
return memoryIdentity(user);
|
|
17899
|
-
}
|
|
17900
|
-
function memoryPlanForReads(user) {
|
|
17901
|
-
if (isMemoryOperator(user.email)) return "unlimited";
|
|
17902
|
-
if (user.memory_plan === "pro" || user.memory_plan === "team") return user.memory_plan;
|
|
17903
|
-
return "free";
|
|
17904
|
-
}
|
|
17905
|
-
async function provisionMemoryForUser(user) {
|
|
17906
|
-
try {
|
|
17907
|
-
const { key } = await getOrCreateUserMemoryKey(user);
|
|
17908
|
-
if (key) await setMemoryProvisioned(user.id, true);
|
|
17909
|
-
} catch {
|
|
17910
|
-
}
|
|
17911
|
-
}
|
|
17912
|
-
function resolveEffectiveMemoryPlan(user) {
|
|
17913
|
-
return user.memory_plan === "pro" || user.memory_plan === "team" ? user.memory_plan : "free";
|
|
17914
|
-
}
|
|
17915
|
-
async function getOrCreateUserMemoryKey(user) {
|
|
17916
|
-
const creds = await getUserMemoryCreds(user.id);
|
|
17917
|
-
if (creds.memory_key) {
|
|
17918
|
-
const decrypted = decryptMemoryKey(creds.memory_key);
|
|
17919
|
-
if (decrypted) return { key: decrypted };
|
|
17920
|
-
}
|
|
17921
|
-
const admin = ADMIN_KEY();
|
|
17922
|
-
if (!admin) return { key: null, error: "memory not configured (MCP_MEMORY_ADMIN_KEY missing)" };
|
|
17923
|
-
const identity = memoryIdentity(user);
|
|
17924
|
-
const plan = isMemoryOperator(user.email) ? "unlimited" : (await getMemoryPlan(user.id)).plan;
|
|
17925
|
-
const provisioned = await import_provision_defaults.provisionDefaultsTool.execute(
|
|
17926
|
-
{ apiKey: admin, granteeIdentity: identity, issueKey: true, plan },
|
|
17927
|
-
{}
|
|
17928
|
-
);
|
|
17929
|
-
if (!provisioned.ok || !provisioned.secret) {
|
|
17930
|
-
return { key: null, error: provisioned.error ?? "failed to provision default vaults" };
|
|
17931
|
-
}
|
|
17932
|
-
await setUserMemoryCreds(user.id, encryptMemoryKey(provisioned.secret), identity);
|
|
17933
|
-
return { key: provisioned.secret };
|
|
17934
|
-
}
|
|
17935
|
-
async function syncMemoryKeyPlan(user, plan) {
|
|
17936
|
-
const effectivePlan = isMemoryOperator(user.email) ? "unlimited" : plan;
|
|
17937
|
-
const { key, error } = await getOrCreateUserMemoryKey(user);
|
|
17938
|
-
if (!key) return { ok: false, error: error ?? "memory unavailable" };
|
|
17939
|
-
const listed = await memoryCall(
|
|
17940
|
-
"listKeysTool",
|
|
17941
|
-
{},
|
|
17942
|
-
key
|
|
17943
|
-
);
|
|
17944
|
-
if (!listed.ok || !listed.keys?.length) return { ok: false, error: listed.error ?? "no memory keys found" };
|
|
17945
|
-
const target = listed.keys[0];
|
|
17946
|
-
if (target.plan === effectivePlan) return { ok: true };
|
|
17947
|
-
const res = await memoryCall("setScopeTool", { keyId: target.keyId, plan: effectivePlan }, key);
|
|
17948
|
-
return { ok: res.ok, error: res.error };
|
|
17949
|
-
}
|
|
17950
|
-
async function syncScheduleEntitlement(user, enabled, quotaPerPeriod) {
|
|
17951
|
-
const admin = ADMIN_KEY();
|
|
17952
|
-
if (!admin) return { ok: false, error: "memory not configured (MCP_MEMORY_ADMIN_KEY missing)" };
|
|
17953
|
-
const identity = resolveMemoryIdentity(user);
|
|
17954
|
-
const res = await import_set_schedule_entitlement.setScheduleEntitlementTool.execute({
|
|
17955
|
-
apiKey: admin,
|
|
17956
|
-
granteeIdentity: identity,
|
|
17957
|
-
enabled,
|
|
17958
|
-
quotaPerPeriod,
|
|
17959
|
-
mcpScraperApiKey: user.api_key
|
|
17960
|
-
}, {});
|
|
17961
|
-
return { ok: res.ok, error: res.error };
|
|
17962
|
-
}
|
|
17963
|
-
var import_node_crypto7, import_provision_defaults, import_set_schedule_entitlement, MEMORY_BASE_URL, ADMIN_KEY;
|
|
17964
|
-
var init_memory = __esm({
|
|
17965
|
-
"src/api/memory.ts"() {
|
|
17966
|
-
"use strict";
|
|
17967
|
-
import_node_crypto7 = require("crypto");
|
|
17968
|
-
init_session();
|
|
17969
|
-
init_db();
|
|
17970
|
-
import_provision_defaults = require("mcpscraper-memory-tools/tools/vaults/provision-defaults");
|
|
17971
|
-
import_set_schedule_entitlement = require("mcpscraper-memory-tools/tools/schedule/set-schedule-entitlement");
|
|
17972
|
-
MEMORY_BASE_URL = () => (process.env.MCP_MEMORY_BASE_URL ?? "https://memory.mcpscraper.dev").replace(/\/$/, "");
|
|
17973
|
-
ADMIN_KEY = () => process.env.MCP_MEMORY_ADMIN_KEY?.trim() ?? "";
|
|
17974
|
-
}
|
|
17975
|
-
});
|
|
17976
|
-
|
|
17977
18273
|
// src/api/video-routes.ts
|
|
17978
18274
|
function invalidRequest5(message) {
|
|
17979
18275
|
return { error_code: "invalid_request", message };
|
|
@@ -28401,7 +28697,7 @@ var PACKAGE_VERSION;
|
|
|
28401
28697
|
var init_version = __esm({
|
|
28402
28698
|
"src/version.ts"() {
|
|
28403
28699
|
"use strict";
|
|
28404
|
-
PACKAGE_VERSION = "0.21.
|
|
28700
|
+
PACKAGE_VERSION = "0.21.3";
|
|
28405
28701
|
}
|
|
28406
28702
|
});
|
|
28407
28703
|
|
|
@@ -34324,7 +34620,7 @@ var init_memory_tool_schemas = __esm({
|
|
|
34324
34620
|
CreateScheduledActionSchema = {
|
|
34325
34621
|
id: "create-scheduled-action",
|
|
34326
34622
|
upstreamName: "createScheduledActionTool",
|
|
34327
|
-
description: "Create a Credit-metered scheduled action in agent mode (default) or connection_sync mode. Each execution has a 75-Credit base charge; agent model usage is added at 1.5 times OpenRouter's actual reported cost. Agent mode follows the description and writes a result into the target vault. connection_sync deterministically runs approved read-only tools on bound service connections and ingests their data. Cadence 'once' runs a single time then completes permanently. Requires write access to the target vault.",
|
|
34623
|
+
description: "Create a Credit-metered scheduled action for an active MCP Scraper Starter plan or higher, in agent mode (default) or connection_sync mode. Each execution has a 75-Credit base charge; agent model usage is added at 1.5 times OpenRouter's actual reported cost. Agent mode follows the description and writes a result into the target vault. connection_sync deterministically runs approved read-only tools on bound service connections and ingests their data. Cadence 'once' runs a single time then completes permanently. Requires write access to the target vault.",
|
|
34328
34624
|
input: {
|
|
34329
34625
|
description: import_zod36.z.string().min(1).describe("Free-text description of what this action should do each time it runs."),
|
|
34330
34626
|
vault: import_zod36.z.string().min(1).describe("The vault this action writes its results into. You must already have write access to it."),
|
|
@@ -34373,12 +34669,13 @@ var init_memory_tool_schemas = __esm({
|
|
|
34373
34669
|
GetScheduleLinkSchema = {
|
|
34374
34670
|
id: "get-schedule-link",
|
|
34375
34671
|
upstreamName: "getScheduleLinkTool",
|
|
34376
|
-
description: "Get your durable, bookmarkable link to the hosted Scheduled Actions page
|
|
34672
|
+
description: "Get your durable, bookmarkable link to the hosted Scheduled Actions page. Requires an active MCP Scraper Starter plan or higher. The embedded secret is shown only once, on first call; it cannot be re-shown, only revoked and reissued via revoke-schedule-link.",
|
|
34377
34673
|
input: {},
|
|
34378
34674
|
output: {
|
|
34379
34675
|
ok: import_zod36.z.boolean().describe("True on success; false on auth error."),
|
|
34380
34676
|
url: import_zod36.z.string().optional().describe("The schedule link. Present only the first time a link is minted for this identity."),
|
|
34381
34677
|
alreadyExists: import_zod36.z.boolean().optional().describe("True when a link already exists and was NOT re-shown. Use revoke-schedule-link then call this again to get a fresh one."),
|
|
34678
|
+
code: import_zod36.z.string().optional().describe("Machine-readable denial code when the link cannot be issued."),
|
|
34382
34679
|
error: import_zod36.z.string().optional().describe("Human-readable failure reason when ok is false.")
|
|
34383
34680
|
},
|
|
34384
34681
|
annotations: {
|
|
@@ -34392,7 +34689,7 @@ var init_memory_tool_schemas = __esm({
|
|
|
34392
34689
|
GetScheduleStatusSchema = {
|
|
34393
34690
|
id: "get-schedule-status",
|
|
34394
34691
|
upstreamName: "getScheduleStatusTool",
|
|
34395
|
-
description: "Get the Credit-metered Scheduled Actions policy. Scheduling
|
|
34692
|
+
description: "Get the Credit-metered Scheduled Actions access and billing policy. Scheduling requires an active MCP Scraper Starter plan or higher but has no separate subscription: each execution has a 75-Credit base charge, and agent model usage is billed at 1.5 times OpenRouter's actual reported cost.",
|
|
34396
34693
|
input: {},
|
|
34397
34694
|
output: {
|
|
34398
34695
|
ok: import_zod36.z.boolean(),
|
|
@@ -34403,6 +34700,8 @@ var init_memory_tool_schemas = __esm({
|
|
|
34403
34700
|
quotaPerPeriod: import_zod36.z.number().optional(),
|
|
34404
34701
|
usedThisPeriod: import_zod36.z.number().optional(),
|
|
34405
34702
|
periodStart: import_zod36.z.string().optional(),
|
|
34703
|
+
defaultTimezone: import_zod36.z.string().nullable().optional().describe("IANA timezone applied to new schedules that name a time without a zone; null means UTC."),
|
|
34704
|
+
code: import_zod36.z.string().optional().describe("Machine-readable reason scheduling is unavailable."),
|
|
34406
34705
|
error: import_zod36.z.string().optional()
|
|
34407
34706
|
},
|
|
34408
34707
|
annotations: {
|
|
@@ -34496,6 +34795,7 @@ var init_memory_tool_schemas = __esm({
|
|
|
34496
34795
|
output: {
|
|
34497
34796
|
ok: import_zod36.z.boolean(),
|
|
34498
34797
|
nextRunAt: import_zod36.z.string().optional(),
|
|
34798
|
+
code: import_zod36.z.string().optional().describe("Machine-readable denial code when the action cannot be resumed."),
|
|
34499
34799
|
error: import_zod36.z.string().optional()
|
|
34500
34800
|
},
|
|
34501
34801
|
annotations: {
|
|
@@ -34527,11 +34827,11 @@ var init_memory_tool_schemas = __esm({
|
|
|
34527
34827
|
SetScheduleEntitlementSchema = {
|
|
34528
34828
|
id: "set-schedule-entitlement",
|
|
34529
34829
|
upstreamName: "setScheduleEntitlementTool",
|
|
34530
|
-
description: "
|
|
34830
|
+
description: "Admin-only scheduled-action credential provisioning and historical-row recovery. Rotate the encrypted delegated MCP Scraper API key without changing legacy enabled/quota fields by omitting those fields. Legacy entitlement values are not runtime access controls; paid-plan access is checked against MCP Scraper directly.",
|
|
34531
34831
|
input: {
|
|
34532
34832
|
granteeIdentity: import_zod36.z.string().min(1).describe("Identity whose scheduling entitlement is being set (e.g. an email)."),
|
|
34533
|
-
enabled: import_zod36.z.boolean().describe("
|
|
34534
|
-
quotaPerPeriod: import_zod36.z.number().optional().describe("
|
|
34833
|
+
enabled: import_zod36.z.boolean().optional().describe("Historical entitlement value for migration/recovery only. Omit to preserve the stored value."),
|
|
34834
|
+
quotaPerPeriod: import_zod36.z.number().optional().describe("Historical monthly execution quota retained only for migration compatibility. Omit to preserve the stored value."),
|
|
34535
34835
|
mcpScraperApiKey: import_zod36.z.string().optional().describe("The identity's mcp-scraper API key, stored encrypted, used to reach mcp-scraper tools during scheduled-action execution.")
|
|
34536
34836
|
},
|
|
34537
34837
|
output: {
|
|
@@ -35269,6 +35569,7 @@ var init_memory_inprocess_executor = __esm({
|
|
|
35269
35569
|
"src/mcp/memory-inprocess-executor.ts"() {
|
|
35270
35570
|
"use strict";
|
|
35271
35571
|
init_memory();
|
|
35572
|
+
init_scheduling_access();
|
|
35272
35573
|
CUTOVER = /* @__PURE__ */ new Map();
|
|
35273
35574
|
HybridMemoryToolExecutor = class {
|
|
35274
35575
|
constructor(httpExecutor, user) {
|
|
@@ -35285,6 +35586,29 @@ var init_memory_inprocess_executor = __esm({
|
|
|
35285
35586
|
return this.mkKeyPromise;
|
|
35286
35587
|
}
|
|
35287
35588
|
async callMemoryTool(upstreamName, args) {
|
|
35589
|
+
const accessDenied = memoryScheduleAccessPayload(upstreamName, this.user);
|
|
35590
|
+
if (accessDenied) {
|
|
35591
|
+
return {
|
|
35592
|
+
content: [{ type: "text", text: JSON.stringify(accessDenied) }],
|
|
35593
|
+
structuredContent: accessDenied,
|
|
35594
|
+
isError: false
|
|
35595
|
+
};
|
|
35596
|
+
}
|
|
35597
|
+
if (memoryScheduleToolNeedsCredentials(upstreamName)) {
|
|
35598
|
+
const credentials = await syncScheduledActionCredentials(this.user);
|
|
35599
|
+
if (!credentials.ok) {
|
|
35600
|
+
const result = {
|
|
35601
|
+
ok: false,
|
|
35602
|
+
code: "schedule_credentials_unavailable",
|
|
35603
|
+
error: credentials.error ?? "Scheduled Actions credentials are unavailable."
|
|
35604
|
+
};
|
|
35605
|
+
return {
|
|
35606
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
35607
|
+
structuredContent: result,
|
|
35608
|
+
isError: false
|
|
35609
|
+
};
|
|
35610
|
+
}
|
|
35611
|
+
}
|
|
35288
35612
|
const handler = CUTOVER.get(upstreamName);
|
|
35289
35613
|
if (!handler) return this.httpExecutor.callMemoryTool(upstreamName, args);
|
|
35290
35614
|
const mkKey = await this.resolveMkKey();
|
|
@@ -38331,6 +38655,8 @@ var init_stripe_routes = __esm({
|
|
|
38331
38655
|
if (user && invoice.id && !await ledgerExistsForStripePI(invoice.id)) {
|
|
38332
38656
|
await creditMc(user.id, tier.credits_mc, LedgerOperation.SUBSCRIPTION, `${tier.label} subscription credits`, invoice.id);
|
|
38333
38657
|
await setSubscriptionTier(user.id, tier.tier, tier.concurrency, invoice.subscription ?? user.subscription_id);
|
|
38658
|
+
const credentials = await syncScheduledActionCredentials(user);
|
|
38659
|
+
if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
|
|
38334
38660
|
}
|
|
38335
38661
|
}
|
|
38336
38662
|
}
|
|
@@ -38348,15 +38674,6 @@ var init_stripe_routes = __esm({
|
|
|
38348
38674
|
}
|
|
38349
38675
|
return c.json({ received: true });
|
|
38350
38676
|
}
|
|
38351
|
-
if (priceId && priceId in SCHEDULING_PLANS) {
|
|
38352
|
-
const sched = SCHEDULING_PLANS[priceId];
|
|
38353
|
-
const user2 = await resolveUser(sub.customer, sub.customer_email);
|
|
38354
|
-
if (user2) {
|
|
38355
|
-
const live = sub.status === "active" || sub.status === "trialing";
|
|
38356
|
-
await syncScheduleEntitlement(user2, live, sched.quota_per_period);
|
|
38357
|
-
}
|
|
38358
|
-
return c.json({ received: true });
|
|
38359
|
-
}
|
|
38360
38677
|
const user = await getUserByStripeCustomerId(sub.customer);
|
|
38361
38678
|
if (!user) return c.json({ received: true });
|
|
38362
38679
|
if (priceId && priceId in SUBSCRIPTION_TIERS) {
|
|
@@ -38364,6 +38681,10 @@ var init_stripe_routes = __esm({
|
|
|
38364
38681
|
const live = sub.status === "active" || sub.status === "trialing";
|
|
38365
38682
|
const newTier = live ? tier.tier : null;
|
|
38366
38683
|
await setSubscriptionTier(user.id, newTier, live ? tier.concurrency : 0, live ? sub.id : null);
|
|
38684
|
+
if (live) {
|
|
38685
|
+
const credentials = await syncScheduledActionCredentials(user);
|
|
38686
|
+
if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
|
|
38687
|
+
}
|
|
38367
38688
|
} else if (priceId === CONCURRENCY_PRICE_ID && event.type === "customer.subscription.created") {
|
|
38368
38689
|
await setExtraConcurrencySlots(user.id, user.extra_concurrency_slots + 1);
|
|
38369
38690
|
await setConcurrencySubId(user.id, sub.id);
|
|
@@ -38380,11 +38701,6 @@ var init_stripe_routes = __esm({
|
|
|
38380
38701
|
}
|
|
38381
38702
|
return c.json({ received: true });
|
|
38382
38703
|
}
|
|
38383
|
-
if (priceId && priceId in SCHEDULING_PLANS) {
|
|
38384
|
-
const user2 = await getUserByStripeCustomerId(sub.customer);
|
|
38385
|
-
if (user2) await syncScheduleEntitlement(user2, false);
|
|
38386
|
-
return c.json({ received: true });
|
|
38387
|
-
}
|
|
38388
38704
|
const user = await getUserByStripeCustomerId(sub.customer);
|
|
38389
38705
|
if (!user) return c.json({ received: true });
|
|
38390
38706
|
if (priceId && priceId in SUBSCRIPTION_TIERS) {
|
|
@@ -42189,8 +42505,9 @@ async function processJob(job) {
|
|
|
42189
42505
|
running++;
|
|
42190
42506
|
try {
|
|
42191
42507
|
const opts = typeof job.options === "string" ? JSON.parse(job.options) : job.options;
|
|
42508
|
+
const headlessSentOut = { value: null };
|
|
42192
42509
|
const result = await runWithCostContext(
|
|
42193
|
-
{ op: opts.serpOnly ? "serp" : "paa", userId: Number(job.user_id) },
|
|
42510
|
+
{ op: opts.serpOnly ? "serp" : "paa", userId: Number(job.user_id), headlessSentOut },
|
|
42194
42511
|
() => harvest({
|
|
42195
42512
|
...opts,
|
|
42196
42513
|
kernelApiKey: browserServiceApiKey(),
|
|
@@ -42207,6 +42524,11 @@ async function processJob(job) {
|
|
|
42207
42524
|
const diff = opts.billingHoldMc - actualCost;
|
|
42208
42525
|
if (diff > 0) await creditMc(job.user_id, diff, "paa_refund", "overestimate refund");
|
|
42209
42526
|
else if (diff < 0) await debitMc(job.user_id, -diff, "paa", opts.query ?? job.query);
|
|
42527
|
+
} else if (opts.serpOnly && typeof opts.billingHoldMc === "number") {
|
|
42528
|
+
const actualCost = headlessSentOut.value === true ? MC_COSTS.serp_headless : MC_COSTS.serp_headful;
|
|
42529
|
+
const diff = opts.billingHoldMc - actualCost;
|
|
42530
|
+
if (diff > 0) await creditMc(job.user_id, diff, "serp_refund", "headless-mode pricing settle");
|
|
42531
|
+
else if (diff < 0) await debitMc(job.user_id, -diff, "serp", opts.query ?? job.query);
|
|
42210
42532
|
}
|
|
42211
42533
|
if (job.callback_url) {
|
|
42212
42534
|
await deliverWebhook(job.callback_url, { job_id: job.id, status: "done", result, attempts });
|
|
@@ -42514,7 +42836,7 @@ async function checkHarvestLimits(user, reuseLockId) {
|
|
|
42514
42836
|
}
|
|
42515
42837
|
return null;
|
|
42516
42838
|
}
|
|
42517
|
-
var import_resend2, import_hono24, import_hono25, import_factory6, import_cookie2, import_stripe2, secureCookies2, isProduction2, sessionCookieOptions2, requireAllowedOrigin, auth2, adminAuth, sessionAuth, requireIntegrationsTier, app, STRIPE_API_VERSION, SYNC_HARVEST_TIMEOUT_OVERRIDE_MS;
|
|
42839
|
+
var import_resend2, import_hono24, import_hono25, import_factory6, import_cookie2, import_stripe2, secureCookies2, isProduction2, sessionCookieOptions2, requireAllowedOrigin, auth2, adminAuth, sessionAuth, requireIntegrationsTier, requirePaidSchedulingTier, app, STRIPE_API_VERSION, SYNC_HARVEST_TIMEOUT_OVERRIDE_MS;
|
|
42518
42840
|
var init_server = __esm({
|
|
42519
42841
|
"src/api/server.ts"() {
|
|
42520
42842
|
"use strict";
|
|
@@ -42586,6 +42908,7 @@ var init_server = __esm({
|
|
|
42586
42908
|
init_harvest_attempt_events();
|
|
42587
42909
|
init_session();
|
|
42588
42910
|
init_memory();
|
|
42911
|
+
init_scheduling_access();
|
|
42589
42912
|
init_memory_db();
|
|
42590
42913
|
init_nango_control();
|
|
42591
42914
|
init_resend_control();
|
|
@@ -42645,6 +42968,11 @@ var init_server = __esm({
|
|
|
42645
42968
|
}
|
|
42646
42969
|
return next();
|
|
42647
42970
|
});
|
|
42971
|
+
requirePaidSchedulingTier = (0, import_factory6.createMiddleware)(async (c, next) => {
|
|
42972
|
+
const user = c.get("user");
|
|
42973
|
+
if (!hasPaidSchedulingAccess(user)) return c.json(paidPlanRequiredPayload(), 403);
|
|
42974
|
+
return next();
|
|
42975
|
+
});
|
|
42648
42976
|
app = new import_hono24.Hono();
|
|
42649
42977
|
STRIPE_API_VERSION = "2026-02-25.clover";
|
|
42650
42978
|
app.use("*", async (c, next) => {
|
|
@@ -42799,11 +43127,7 @@ var init_server = __esm({
|
|
|
42799
43127
|
return c.json(CATALOG);
|
|
42800
43128
|
});
|
|
42801
43129
|
app.get("/rates", (c) => {
|
|
42802
|
-
|
|
42803
|
-
...cost,
|
|
42804
|
-
...cost.notes ? { notes: cost.notes.replace(/ via fal\.ai/g, "") } : {}
|
|
42805
|
-
}));
|
|
42806
|
-
return c.json({ creditsPerDollarStarter: Math.round(1 / 15e-5), costs });
|
|
43130
|
+
return c.json(getPublicRatesPayload());
|
|
42807
43131
|
});
|
|
42808
43132
|
app.get("/me", async (c) => {
|
|
42809
43133
|
const token = (0, import_cookie2.getCookie)(c, "session");
|
|
@@ -42894,6 +43218,18 @@ var init_server = __esm({
|
|
|
42894
43218
|
const user = c.get("user");
|
|
42895
43219
|
const body = await c.req.json().catch(() => ({}));
|
|
42896
43220
|
if (!body.toolName) return c.json({ ok: false, error: "toolName is required" }, 400);
|
|
43221
|
+
const accessDenied = memoryScheduleAccessPayload(body.toolName, user);
|
|
43222
|
+
if (accessDenied) return c.json(accessDenied);
|
|
43223
|
+
if (memoryScheduleToolNeedsCredentials(body.toolName)) {
|
|
43224
|
+
const credentials = await syncScheduledActionCredentials(user);
|
|
43225
|
+
if (!credentials.ok) {
|
|
43226
|
+
return c.json({
|
|
43227
|
+
ok: false,
|
|
43228
|
+
code: "schedule_credentials_unavailable",
|
|
43229
|
+
error: credentials.error ?? "Scheduled Actions credentials are unavailable."
|
|
43230
|
+
}, 503);
|
|
43231
|
+
}
|
|
43232
|
+
}
|
|
42897
43233
|
const { key, error } = await getOrCreateUserMemoryKey(user);
|
|
42898
43234
|
if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 502);
|
|
42899
43235
|
const result = await memoryCall(body.toolName, body.args ?? {}, key);
|
|
@@ -43093,7 +43429,7 @@ var init_server = __esm({
|
|
|
43093
43429
|
llmCostMultiplier: SCHEDULED_AGENT_LLM_MARKUP
|
|
43094
43430
|
}, 410);
|
|
43095
43431
|
});
|
|
43096
|
-
app.get("/schedule-status", auth2, async (c) => {
|
|
43432
|
+
app.get("/schedule-status", auth2, requirePaidSchedulingTier, async (c) => {
|
|
43097
43433
|
try {
|
|
43098
43434
|
const user = c.get("user");
|
|
43099
43435
|
const { key, error } = await getOrCreateUserMemoryKey(user);
|
|
@@ -43460,9 +43796,13 @@ var init_server = __esm({
|
|
|
43460
43796
|
return scheduleConnectionError(c, err, "Unable to run this connection action.");
|
|
43461
43797
|
}
|
|
43462
43798
|
});
|
|
43463
|
-
app.post("/schedule-link", auth2, async (c) => {
|
|
43799
|
+
app.post("/schedule-link", auth2, requirePaidSchedulingTier, async (c) => {
|
|
43464
43800
|
try {
|
|
43465
43801
|
const user = c.get("user");
|
|
43802
|
+
const credentials = await syncScheduledActionCredentials(user);
|
|
43803
|
+
if (!credentials.ok) {
|
|
43804
|
+
return c.json({ ok: false, code: "schedule_credentials_unavailable", error: credentials.error ?? "Scheduled Actions credentials are unavailable." }, 503);
|
|
43805
|
+
}
|
|
43466
43806
|
const { key, error } = await getOrCreateUserMemoryKey(user);
|
|
43467
43807
|
if (!key) return c.json({ error: error ?? "memory unavailable" }, 503);
|
|
43468
43808
|
const r = await memoryCall("getScheduleLinkTool", {}, key);
|
|
@@ -43500,8 +43840,12 @@ var init_server = __esm({
|
|
|
43500
43840
|
});
|
|
43501
43841
|
}
|
|
43502
43842
|
});
|
|
43503
|
-
app.post("/schedule-actions", auth2, async (c) => {
|
|
43843
|
+
app.post("/schedule-actions", auth2, requirePaidSchedulingTier, async (c) => {
|
|
43504
43844
|
const user = c.get("user");
|
|
43845
|
+
const credentials = await syncScheduledActionCredentials(user);
|
|
43846
|
+
if (!credentials.ok) {
|
|
43847
|
+
return c.json({ ok: false, code: "schedule_credentials_unavailable", error: credentials.error ?? "Scheduled Actions credentials are unavailable." }, 503);
|
|
43848
|
+
}
|
|
43505
43849
|
const { key, error } = await getOrCreateUserMemoryKey(user);
|
|
43506
43850
|
if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
|
|
43507
43851
|
const body = await c.req.json().catch(() => ({}));
|
|
@@ -43593,7 +43937,7 @@ var init_server = __esm({
|
|
|
43593
43937
|
const r = await memoryCall("pauseScheduledActionTool", { id: c.req.param("id") }, key);
|
|
43594
43938
|
return c.json(r);
|
|
43595
43939
|
});
|
|
43596
|
-
app.post("/schedule-actions/:id/resume", auth2, async (c) => {
|
|
43940
|
+
app.post("/schedule-actions/:id/resume", auth2, requirePaidSchedulingTier, async (c) => {
|
|
43597
43941
|
const user = c.get("user");
|
|
43598
43942
|
const { key, error } = await getOrCreateUserMemoryKey(user);
|
|
43599
43943
|
if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
|
|
@@ -43698,7 +44042,7 @@ var init_server = __esm({
|
|
|
43698
44042
|
serpOnly: body.serpOnly ?? false,
|
|
43699
44043
|
pages: Math.min(2, Math.max(1, body.pages ?? 1))
|
|
43700
44044
|
};
|
|
43701
|
-
const harvestCost = options.serpOnly ? MC_COSTS.
|
|
44045
|
+
const harvestCost = options.serpOnly ? MC_COSTS.serp_headful : paaCostForQuestionCount2(options.maxQuestions);
|
|
43702
44046
|
const { ok: harvestOk, balance_mc: harvestBal } = await debitMc(user.id, harvestCost, options.serpOnly ? LedgerOperation.SERP : LedgerOperation.PAA, options.query);
|
|
43703
44047
|
if (!harvestOk) return c.json(insufficientBalanceResponse(harvestBal, harvestCost), 402);
|
|
43704
44048
|
const jobId = await createJob(user.id, options.query, { ...options, billingHoldMc: harvestCost }, body.callback_url);
|
|
@@ -43734,7 +44078,7 @@ var init_server = __esm({
|
|
|
43734
44078
|
serpOnly: body.serpOnly ?? false,
|
|
43735
44079
|
pages: Math.min(2, Math.max(1, body.pages ?? 1))
|
|
43736
44080
|
};
|
|
43737
|
-
const syncCost = options.serpOnly ? MC_COSTS.
|
|
44081
|
+
const syncCost = options.serpOnly ? MC_COSTS.serp_headful : paaCostForQuestionCount2(options.maxQuestions);
|
|
43738
44082
|
const { ok: syncOk, balance_mc: syncBal } = await debitMc(user.id, syncCost, options.serpOnly ? LedgerOperation.SERP : LedgerOperation.PAA, options.query);
|
|
43739
44083
|
if (!syncOk) return c.json(insufficientBalanceResponse(syncBal, syncCost), 402);
|
|
43740
44084
|
const jobId = await createRunningJob(user.id, options.query, options);
|
|
@@ -43744,10 +44088,11 @@ var init_server = __esm({
|
|
|
43744
44088
|
c.req.raw.signal,
|
|
43745
44089
|
AbortSignal.timeout(syncTimeoutMs)
|
|
43746
44090
|
]);
|
|
44091
|
+
const headlessSentOut = { value: null };
|
|
43747
44092
|
try {
|
|
43748
44093
|
const harvestCtx = currentCostContext();
|
|
43749
44094
|
const result = await runWithCostContext(
|
|
43750
|
-
{ ...harvestCtx, op: options.serpOnly ? "serp" : "paa", userId: user.id },
|
|
44095
|
+
{ ...harvestCtx, op: options.serpOnly ? "serp" : "paa", userId: user.id, headlessSentOut },
|
|
43751
44096
|
() => harvest({
|
|
43752
44097
|
...options,
|
|
43753
44098
|
kernelApiKey: browserServiceApiKey(),
|
|
@@ -43766,6 +44111,11 @@ var init_server = __esm({
|
|
|
43766
44111
|
const diff = syncCost - actualCost;
|
|
43767
44112
|
if (diff > 0) await creditMc(user.id, diff, LedgerOperation.PAA_REFUND, "overestimate refund");
|
|
43768
44113
|
else if (diff < 0) await debitMc(user.id, -diff, LedgerOperation.PAA, options.query);
|
|
44114
|
+
} else {
|
|
44115
|
+
const actualCost = headlessSentOut.value === true ? MC_COSTS.serp_headless : MC_COSTS.serp_headful;
|
|
44116
|
+
const diff = syncCost - actualCost;
|
|
44117
|
+
if (diff > 0) await creditMc(user.id, diff, LedgerOperation.SERP_REFUND, "headless-mode pricing settle");
|
|
44118
|
+
else if (diff < 0) await debitMc(user.id, -diff, LedgerOperation.SERP, options.query);
|
|
43769
44119
|
}
|
|
43770
44120
|
return c.json({ job_id: jobId, status: "done", result: sanitizeHarvestResult(result), attempts: sanitizeAttempts(attempts) });
|
|
43771
44121
|
} catch (err) {
|