mcp-scraper 0.4.4 → 0.4.6

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.
Files changed (47) hide show
  1. package/dist/bin/api-server.cjs +579 -40
  2. package/dist/bin/api-server.cjs.map +1 -1
  3. package/dist/bin/api-server.js +3 -3
  4. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  5. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  6. package/dist/bin/mcp-scraper-cli.js +1 -1
  7. package/dist/bin/mcp-scraper-install.cjs +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  9. package/dist/bin/mcp-scraper-install.js +1 -1
  10. package/dist/bin/mcp-stdio-server.cjs +197 -3
  11. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  12. package/dist/bin/mcp-stdio-server.js +2 -2
  13. package/dist/bin/paa-harvest.cjs.map +1 -1
  14. package/dist/bin/paa-harvest.js +2 -2
  15. package/dist/chunk-5BB6Y3BB.js +7 -0
  16. package/dist/chunk-5BB6Y3BB.js.map +1 -0
  17. package/dist/{chunk-NJK5BTUK.js → chunk-5GAD2AN2.js} +2 -2
  18. package/dist/{chunk-FUVQR6GK.js → chunk-GL4BW4CP.js} +49 -1
  19. package/dist/chunk-GL4BW4CP.js.map +1 -0
  20. package/dist/{chunk-3UH3BEIZ.js → chunk-HE2LQPJ2.js} +2 -2
  21. package/dist/{chunk-Z34NGK64.js → chunk-IFXACD4K.js} +198 -4
  22. package/dist/chunk-IFXACD4K.js.map +1 -0
  23. package/dist/{chunk-5LO6EHEH.js → chunk-IVQYNY45.js} +14 -3
  24. package/dist/chunk-IVQYNY45.js.map +1 -0
  25. package/dist/{chunk-4ZZWFI6L.js → chunk-NYAWN7CZ.js} +3 -3
  26. package/dist/{db-H3S3M6KK.js → db-LIOTIWVN.js} +6 -2
  27. package/dist/{extract-bundle-UKE273TV.js → extract-bundle-COS56ZDO.js} +2 -2
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.js +2 -2
  30. package/dist/{server-IKT3QVFB.js → server-R3KVQNOT.js} +316 -35
  31. package/dist/server-R3KVQNOT.js.map +1 -0
  32. package/dist/{site-extract-repository-THBVEXMP.js → site-extract-repository-LI2S3S6E.js} +4 -4
  33. package/dist/{worker-YHKUJR5P.js → worker-VASAJ7FZ.js} +5 -5
  34. package/package.json +2 -1
  35. package/dist/chunk-5LO6EHEH.js.map +0 -1
  36. package/dist/chunk-FUVQR6GK.js.map +0 -1
  37. package/dist/chunk-TZRPP45I.js +0 -7
  38. package/dist/chunk-TZRPP45I.js.map +0 -1
  39. package/dist/chunk-Z34NGK64.js.map +0 -1
  40. package/dist/server-IKT3QVFB.js.map +0 -1
  41. /package/dist/{chunk-NJK5BTUK.js.map → chunk-5GAD2AN2.js.map} +0 -0
  42. /package/dist/{chunk-3UH3BEIZ.js.map → chunk-HE2LQPJ2.js.map} +0 -0
  43. /package/dist/{chunk-4ZZWFI6L.js.map → chunk-NYAWN7CZ.js.map} +0 -0
  44. /package/dist/{db-H3S3M6KK.js.map → db-LIOTIWVN.js.map} +0 -0
  45. /package/dist/{extract-bundle-UKE273TV.js.map → extract-bundle-COS56ZDO.js.map} +0 -0
  46. /package/dist/{site-extract-repository-THBVEXMP.js.map → site-extract-repository-LI2S3S6E.js.map} +0 -0
  47. /package/dist/{worker-YHKUJR5P.js.map → worker-VASAJ7FZ.js.map} +0 -0
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  harvest
3
- } from "./chunk-NJK5BTUK.js";
3
+ } from "./chunk-5GAD2AN2.js";
4
4
  import "./chunk-HPV4VOQX.js";
5
5
  import "./chunk-K443GQY5.js";
6
6
  import "./chunk-XGIPATLV.js";
7
7
  import "./chunk-M2S27J6Z.js";
8
- import "./chunk-FUVQR6GK.js";
8
+ import "./chunk-GL4BW4CP.js";
9
9
 
10
10
  // src/video/VideoGenerator.ts
11
11
  import { execSync as execSync2 } from "child_process";
@@ -3,7 +3,7 @@ import {
3
3
  createHarvestAttemptRecorder,
4
4
  harvestProblemResponse,
5
5
  serializeHarvestProblem
6
- } from "./chunk-3UH3BEIZ.js";
6
+ } from "./chunk-HE2LQPJ2.js";
7
7
  import {
8
8
  BrowserDriver,
9
9
  MapsSelectors,
@@ -16,7 +16,7 @@ import {
16
16
  recordVendorUsage,
17
17
  resolveKernelProxyId,
18
18
  runWithCostContext
19
- } from "./chunk-NJK5BTUK.js";
19
+ } from "./chunk-5GAD2AN2.js";
20
20
  import {
21
21
  HttpMcpToolExecutor,
22
22
  MemoryMcpToolExecutor,
@@ -31,7 +31,7 @@ import {
31
31
  registerSerpIntelligenceCaptureTools,
32
32
  sanitizeAttempts,
33
33
  sanitizeHarvestResult
34
- } from "./chunk-Z34NGK64.js";
34
+ } from "./chunk-IFXACD4K.js";
35
35
  import {
36
36
  auditImages,
37
37
  getBlobStore
@@ -65,7 +65,7 @@ import {
65
65
  RawMapsOverviewSchema,
66
66
  RawMapsReviewStatsSchema
67
67
  } from "./chunk-XGIPATLV.js";
68
- import "./chunk-TZRPP45I.js";
68
+ import "./chunk-5BB6Y3BB.js";
69
69
  import {
70
70
  completeExtractJob,
71
71
  countSuccessfulPages,
@@ -76,7 +76,7 @@ import {
76
76
  saveExtractPages,
77
77
  setExtractJobTotal,
78
78
  settleExtractJob
79
- } from "./chunk-4ZZWFI6L.js";
79
+ } from "./chunk-NYAWN7CZ.js";
80
80
  import {
81
81
  BROWSER_OPEN_MIN_BALANCE_MC,
82
82
  CONCURRENCY_PRICE_ID,
@@ -95,7 +95,7 @@ import {
95
95
  browserActiveCostMc,
96
96
  concurrencySlotBillingInfo,
97
97
  insufficientBalanceResponse
98
- } from "./chunk-5LO6EHEH.js";
98
+ } from "./chunk-IVQYNY45.js";
99
99
  import {
100
100
  CaptchaError,
101
101
  RECAPTCHA_INSTRUCTIONS,
@@ -131,6 +131,7 @@ import {
131
131
  getLedger,
132
132
  getMemoryPlan,
133
133
  getMemoryProvisioned,
134
+ getPageSnapshot,
134
135
  getRefresh,
135
136
  getUserByApiKey,
136
137
  getUserByEmail,
@@ -175,8 +176,9 @@ import {
175
176
  setStripeCustomerId,
176
177
  setSubscriptionTier,
177
178
  setUserMemoryCreds,
179
+ upsertPageSnapshot,
178
180
  verifyPassword
179
- } from "./chunk-FUVQR6GK.js";
181
+ } from "./chunk-GL4BW4CP.js";
180
182
 
181
183
  // src/blog/registry.ts
182
184
  var posts = [
@@ -7529,7 +7531,7 @@ var siteExtractFn = inngest.createFunction(
7529
7531
  return auditImages(pages, { concurrency: 12, timeoutMs: 12e3 });
7530
7532
  }) : null;
7531
7533
  const artifacts = await step.run("finalize", async () => {
7532
- const { assembleExtractArtifacts } = await import("./extract-bundle-UKE273TV.js");
7534
+ const { assembleExtractArtifacts } = await import("./extract-bundle-COS56ZDO.js");
7533
7535
  const stored = await assembleExtractArtifacts(job, { branding, imageAudit });
7534
7536
  await completeExtractJob(jobId, stored);
7535
7537
  return stored;
@@ -8935,6 +8937,11 @@ var ExtractUrlBodySchema = z11.object({
8935
8937
  depositToVault: z11.boolean().optional(),
8936
8938
  vaultName: z11.string().trim().min(1).max(120).optional()
8937
8939
  });
8940
+ var DiffPageBodySchema = z11.object({
8941
+ url: z11.string().min(1, "url is required"),
8942
+ allowLocal: z11.boolean().optional(),
8943
+ resetBaseline: z11.boolean().optional()
8944
+ });
8938
8945
  var MapUrlsBodySchema = z11.object({
8939
8946
  url: z11.string().min(1, "url is required"),
8940
8947
  maxUrls: z11.number().int().min(1).max(2e3).optional(),
@@ -11935,23 +11942,8 @@ async function provisionMemoryForUser(user) {
11935
11942
  } catch {
11936
11943
  }
11937
11944
  }
11938
- function memoryPlanForUser(user) {
11939
- switch (user.subscription_tier) {
11940
- case "starter":
11941
- return "pro";
11942
- case "growth":
11943
- return "team";
11944
- case "scale":
11945
- return "enterprise";
11946
- default:
11947
- return "free";
11948
- }
11949
- }
11950
- var MEMORY_PLAN_RANK = { free: 0, pro: 1, team: 2, enterprise: 3 };
11951
11945
  function resolveEffectiveMemoryPlan(user) {
11952
- const tierPlan = memoryPlanForUser(user);
11953
- const memoryProPlan = user.memory_plan === "pro" || user.memory_plan === "team" ? user.memory_plan : "free";
11954
- return MEMORY_PLAN_RANK[tierPlan] >= MEMORY_PLAN_RANK[memoryProPlan] ? tierPlan : memoryProPlan;
11946
+ return user.memory_plan === "pro" || user.memory_plan === "team" ? user.memory_plan : "free";
11955
11947
  }
11956
11948
  async function getOrCreateUserMemoryKey(user) {
11957
11949
  const creds = await getUserMemoryCreds(user.id);
@@ -15958,6 +15950,7 @@ mcpApp.all("/", async (c) => {
15958
15950
  });
15959
15951
 
15960
15952
  // src/api/browser-agent-routes.ts
15953
+ import { randomUUID as randomUUID3 } from "crypto";
15961
15954
  import { Hono as Hono17 } from "hono";
15962
15955
 
15963
15956
  // src/api/browser-agent-db.ts
@@ -16046,8 +16039,58 @@ async function migrateBrowserAgent() {
16046
16039
  } catch {
16047
16040
  }
16048
16041
  await db.execute(`CREATE INDEX IF NOT EXISTS browser_auth_connections_profile ON browser_auth_connections(profile)`);
16042
+ await db.execute(`
16043
+ CREATE TABLE IF NOT EXISTS browser_agent_extensions (
16044
+ id TEXT PRIMARY KEY,
16045
+ user_id INTEGER NOT NULL,
16046
+ name TEXT NOT NULL,
16047
+ backend_id TEXT NOT NULL,
16048
+ backend_name TEXT NOT NULL,
16049
+ source TEXT NOT NULL DEFAULT 'store',
16050
+ source_url TEXT,
16051
+ size_bytes INTEGER,
16052
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
16053
+ )
16054
+ `);
16055
+ await db.execute(`CREATE UNIQUE INDEX IF NOT EXISTS browser_agent_extensions_user_name ON browser_agent_extensions(user_id, name)`);
16056
+ await db.execute(`CREATE INDEX IF NOT EXISTS browser_agent_extensions_user ON browser_agent_extensions(user_id)`);
16049
16057
  _ready = true;
16050
16058
  }
16059
+ async function createExtensionRow(input) {
16060
+ const db = getDb();
16061
+ const id = `bext_${randomUUID2().replace(/-/g, "").slice(0, 20)}`;
16062
+ await db.execute({
16063
+ sql: `INSERT INTO browser_agent_extensions (id, user_id, name, backend_id, backend_name, source, source_url, size_bytes)
16064
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
16065
+ args: [id, input.userId, input.name, input.backendId, input.backendName, input.source, input.sourceUrl, input.sizeBytes]
16066
+ });
16067
+ const row = await getExtensionRow(input.userId, input.name);
16068
+ if (!row) throw new Error("extension insert failed");
16069
+ return row;
16070
+ }
16071
+ async function getExtensionRow(userId, name) {
16072
+ const db = getDb();
16073
+ const res = await db.execute({
16074
+ sql: `SELECT * FROM browser_agent_extensions WHERE user_id = ? AND name = ?`,
16075
+ args: [userId, name]
16076
+ });
16077
+ return res.rows[0] ?? null;
16078
+ }
16079
+ async function listExtensionRows(userId) {
16080
+ const db = getDb();
16081
+ const res = await db.execute({
16082
+ sql: `SELECT * FROM browser_agent_extensions WHERE user_id = ? ORDER BY created_at DESC`,
16083
+ args: [userId]
16084
+ });
16085
+ return res.rows;
16086
+ }
16087
+ async function deleteExtensionRow(userId, name) {
16088
+ const db = getDb();
16089
+ await db.execute({
16090
+ sql: `DELETE FROM browser_agent_extensions WHERE user_id = ? AND name = ?`,
16091
+ args: [userId, name]
16092
+ });
16093
+ }
16051
16094
  async function createAuthConnectionRow(input) {
16052
16095
  const db = getDb();
16053
16096
  const connectionId = `authc_${randomUUID2().replace(/-/g, "").slice(0, 20)}`;
@@ -16904,6 +16947,28 @@ function isProfileConflict(err) {
16904
16947
  const message = err instanceof Error ? err.message : String(err);
16905
16948
  return /\b409\b|conflict|already exists/i.test(message);
16906
16949
  }
16950
+ function isNotFound(err) {
16951
+ const message = err instanceof Error ? err.message : String(err);
16952
+ return /\b404\b|not found/i.test(message);
16953
+ }
16954
+ async function importExtensionFromStore(storeUrl, backendName) {
16955
+ const k = client();
16956
+ const archive = await k.extensions.downloadFromChromeStore({ url: storeUrl });
16957
+ const uploaded = await k.extensions.upload({ file: archive, name: backendName });
16958
+ return {
16959
+ backendId: uploaded.id,
16960
+ backendName: uploaded.name ?? backendName,
16961
+ sizeBytes: typeof uploaded.size_bytes === "number" ? uploaded.size_bytes : null
16962
+ };
16963
+ }
16964
+ async function deleteExtensionBackend(backendName) {
16965
+ const k = client();
16966
+ try {
16967
+ await k.extensions.delete(backendName);
16968
+ } catch (err) {
16969
+ if (!isNotFound(err)) throw err;
16970
+ }
16971
+ }
16907
16972
  async function ensureProfile(k, name) {
16908
16973
  try {
16909
16974
  await k.profiles.create({ name });
@@ -16922,6 +16987,7 @@ async function createSession(opts = {}) {
16922
16987
  const resolvedSaveProfileChanges = opts.saveProfileChanges ?? browserServiceProfileSaveChanges();
16923
16988
  const disableDefaultProxy = opts.disableDefaultProxy ?? !resolvedProxyId;
16924
16989
  const startUrl = opts.startUrl?.trim();
16990
+ const extensionNames = (opts.extensionNames ?? []).map((name) => name.trim()).filter(Boolean);
16925
16991
  if (resolvedProfileName && resolvedSaveProfileChanges === true) {
16926
16992
  await ensureProfile(k, resolvedProfileName);
16927
16993
  }
@@ -16935,7 +17001,8 @@ async function createSession(opts = {}) {
16935
17001
  name: resolvedProfileName,
16936
17002
  ...typeof resolvedSaveProfileChanges === "boolean" ? { save_changes: resolvedSaveProfileChanges } : {}
16937
17003
  }
16938
- } : {}
17004
+ } : {},
17005
+ ...extensionNames.length ? { extensions: extensionNames.map((name) => ({ name })) } : {}
16939
17006
  };
16940
17007
  const browser = await k.browsers.create(createPayload);
16941
17008
  const runtimeSessionId = browser.session_id;
@@ -17323,6 +17390,16 @@ function parseAgentDate(value) {
17323
17390
  const legacyParsed = Date.parse(`${value.replace(" ", "T")}Z`);
17324
17391
  return Number.isFinite(legacyParsed) ? legacyParsed : null;
17325
17392
  }
17393
+ var EXTENSION_NAME_RE = /^[\w .-]{1,64}$/;
17394
+ function publicExtension(row) {
17395
+ return {
17396
+ name: row.name,
17397
+ source: row.source,
17398
+ source_url: row.source_url,
17399
+ size_bytes: row.size_bytes,
17400
+ created_at: row.created_at
17401
+ };
17402
+ }
17326
17403
  function replayClock(row) {
17327
17404
  if (!row) return null;
17328
17405
  const startedAtMs = parseAgentDate(row.started_at);
@@ -17469,6 +17546,15 @@ function buildBrowserAgentRoutes() {
17469
17546
  const body = await c.req.json().catch(() => ({}));
17470
17547
  const timeoutSeconds = typeof body.timeout_seconds === "number" ? body.timeout_seconds : void 0;
17471
17548
  const startUrl = typeof body.url === "string" ? body.url : void 0;
17549
+ const extensionNames = Array.isArray(body.extension_names) ? body.extension_names.filter((v) => typeof v === "string") : void 0;
17550
+ let resolvedExtensionBackendNames;
17551
+ if (extensionNames && extensionNames.length) {
17552
+ const names = extensionNames;
17553
+ const rows = await Promise.all(names.map((name) => getExtensionRow(user.id, name)));
17554
+ const missing = names.filter((_, i) => !rows[i]);
17555
+ if (missing.length) return c.json({ error: `extension(s) not found: ${missing.join(", ")}` }, 400);
17556
+ resolvedExtensionBackendNames = rows.map((row) => row.backend_name);
17557
+ }
17472
17558
  const gate = await acquireConcurrencyGate(user, "browser_agent_session", {
17473
17559
  ttlSeconds: browserSessionLockTtlSeconds(timeoutSeconds),
17474
17560
  metadata: { label: typeof body.label === "string" ? body.label : null }
@@ -17483,7 +17569,8 @@ function buildBrowserAgentRoutes() {
17483
17569
  saveProfileChanges: typeof body.save_profile_changes === "boolean" ? body.save_profile_changes : void 0,
17484
17570
  startUrl,
17485
17571
  disableDefaultProxy: typeof body.disable_default_proxy === "boolean" ? body.disable_default_proxy : void 0,
17486
- viewport: body.viewport && typeof body.viewport === "object" ? body.viewport : void 0
17572
+ viewport: body.viewport && typeof body.viewport === "object" ? body.viewport : void 0,
17573
+ extensionNames: resolvedExtensionBackendNames
17487
17574
  });
17488
17575
  runtimeSessionId = created.runtimeSessionId;
17489
17576
  const row = await createSessionRow({
@@ -17786,6 +17873,52 @@ function buildBrowserAgentRoutes() {
17786
17873
  }))
17787
17874
  });
17788
17875
  });
17876
+ app2.post("/extensions/import", async (c) => {
17877
+ const user = c.get("user");
17878
+ const body = await c.req.json().catch(() => ({}));
17879
+ const storeUrl = typeof body.store_url === "string" ? body.store_url.trim() : "";
17880
+ const name = typeof body.name === "string" ? body.name.trim() : "";
17881
+ if (!storeUrl) return c.json({ error: "store_url is required" }, 400);
17882
+ if (!name || !EXTENSION_NAME_RE.test(name)) {
17883
+ return c.json({ error: "name is required (1-64 characters: letters, numbers, spaces, dots, underscores, hyphens)" }, 400);
17884
+ }
17885
+ const existing = await getExtensionRow(user.id, name);
17886
+ if (existing) return c.json({ error: `an extension named "${name}" already exists \u2014 delete it first or pick another name` }, 409);
17887
+ const backendName = `u${user.id}_${randomUUID3().replace(/-/g, "")}`;
17888
+ try {
17889
+ const imported = await importExtensionFromStore(storeUrl, backendName);
17890
+ const row = await createExtensionRow({
17891
+ userId: user.id,
17892
+ name,
17893
+ backendId: imported.backendId,
17894
+ backendName: imported.backendName,
17895
+ source: "store",
17896
+ sourceUrl: storeUrl,
17897
+ sizeBytes: imported.sizeBytes
17898
+ });
17899
+ return c.json(publicExtension(row));
17900
+ } catch (err) {
17901
+ return c.json(failure(err), 502);
17902
+ }
17903
+ });
17904
+ app2.get("/extensions", async (c) => {
17905
+ const user = c.get("user");
17906
+ const rows = await listExtensionRows(user.id);
17907
+ return c.json({ extensions: rows.map(publicExtension) });
17908
+ });
17909
+ app2.delete("/extensions/:name", async (c) => {
17910
+ const user = c.get("user");
17911
+ const name = c.req.param("name");
17912
+ const row = await getExtensionRow(user.id, name);
17913
+ if (!row) return c.json({ error: "not found" }, 404);
17914
+ try {
17915
+ await deleteExtensionBackend(row.backend_name);
17916
+ } catch (err) {
17917
+ return c.json(failure(err), 502);
17918
+ }
17919
+ await deleteExtensionRow(user.id, name);
17920
+ return c.json({ ok: true });
17921
+ });
17789
17922
  app2.get("/sessions/:id/replays/:replayId/download", async (c) => {
17790
17923
  const user = c.get("user");
17791
17924
  const row = await loadOpenSession(c.req.param("id"), user.id);
@@ -18141,7 +18274,6 @@ stripeApp.post("/webhooks", async (c) => {
18141
18274
  const live = sub.status === "active" || sub.status === "trialing";
18142
18275
  const newTier = live ? tier.tier : null;
18143
18276
  await setSubscriptionTier(user.id, newTier, live ? tier.concurrency : 0, live ? sub.id : null);
18144
- await syncMemoryKeyPlan(user, resolveEffectiveMemoryPlan({ ...user, subscription_tier: newTier }));
18145
18277
  } else if (priceId === CONCURRENCY_PRICE_ID && event.type === "customer.subscription.created") {
18146
18278
  await setExtraConcurrencySlots(user.id, user.extra_concurrency_slots + 1);
18147
18279
  await setConcurrencySubId(user.id, sub.id);
@@ -18167,7 +18299,6 @@ stripeApp.post("/webhooks", async (c) => {
18167
18299
  if (!user) return c.json({ received: true });
18168
18300
  if (priceId && priceId in SUBSCRIPTION_TIERS) {
18169
18301
  await setSubscriptionTier(user.id, null, 0, null);
18170
- await syncMemoryKeyPlan(user, resolveEffectiveMemoryPlan({ ...user, subscription_tier: null }));
18171
18302
  } else if (priceId === CONCURRENCY_PRICE_ID) {
18172
18303
  await setExtraConcurrencySlots(user.id, Math.max(0, user.extra_concurrency_slots - 1));
18173
18304
  await setConcurrencySubId(user.id, null);
@@ -18191,7 +18322,7 @@ async function resolveUser(customerId, emailFallback) {
18191
18322
  // src/api/oauth-routes.ts
18192
18323
  import { Hono as Hono19 } from "hono";
18193
18324
  import { getCookie, setCookie } from "hono/cookie";
18194
- import { createHash as createHash3, randomBytes as randomBytes2, randomUUID as randomUUID3 } from "crypto";
18325
+ import { createHash as createHash3, randomBytes as randomBytes2, randomUUID as randomUUID4 } from "crypto";
18195
18326
  import { importPKCS8, exportJWK, SignJWT } from "jose";
18196
18327
  var ISSUER = "https://mcpscraper.dev";
18197
18328
  var RESOURCE = () => process.env.MCP_MEMORY_RESOURCE_URL ?? "https://memory.mcpscraper.dev/mcp";
@@ -18539,7 +18670,7 @@ function pkceMatches(verifier, challenge) {
18539
18670
  }
18540
18671
  async function mintAccessToken(identity, scope, plan, audience) {
18541
18672
  const { privateKey, kid } = await getKeys();
18542
- return new SignJWT({ scope, plan }).setProtectedHeader({ alg: "RS256", kid }).setIssuer(ISSUER).setSubject(identity).setAudience(audience).setIssuedAt().setJti(randomUUID3()).setExpirationTime(`${ACCESS_TTL_SECONDS}s`).sign(privateKey);
18673
+ return new SignJWT({ scope, plan }).setProtectedHeader({ alg: "RS256", kid }).setIssuer(ISSUER).setSubject(identity).setAudience(audience).setIssuedAt().setJti(randomUUID4()).setExpirationTime(`${ACCESS_TTL_SECONDS}s`).sign(privateKey);
18543
18674
  }
18544
18675
  function tokenErrorResponse(c, error, description, status) {
18545
18676
  return c.json({ error, error_description: description }, status);
@@ -19645,6 +19776,71 @@ import { createMiddleware as createMiddleware3 } from "hono/factory";
19645
19776
  import { getCookie as getCookie2, setCookie as setCookie2, deleteCookie } from "hono/cookie";
19646
19777
  import Stripe2 from "stripe";
19647
19778
 
19779
+ // src/api/page-diff.ts
19780
+ import { createHash as createHash4 } from "crypto";
19781
+ import { diffLines } from "diff";
19782
+ var MAX_SNAPSHOT_CONTENT_CHARS = 25e4;
19783
+ var MAX_DIFF_HUNKS = 200;
19784
+ var MAX_DIFF_LINES_PER_RESPONSE = 2e3;
19785
+ function sha256Hex(value) {
19786
+ return createHash4("sha256").update(value).digest("hex");
19787
+ }
19788
+ function truncateForStorage(value, maxChars = MAX_SNAPSHOT_CONTENT_CHARS) {
19789
+ if (value.length <= maxChars) return { value, truncated: false };
19790
+ return { value: value.slice(0, maxChars), truncated: true };
19791
+ }
19792
+ function changeLines(change) {
19793
+ const lines = change.value.split("\n");
19794
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
19795
+ return lines;
19796
+ }
19797
+ function diffPageContent(oldContent, newContent) {
19798
+ const changes = diffLines(oldContent, newContent);
19799
+ const rawHunks = [];
19800
+ let linesAdded = 0;
19801
+ let linesRemoved = 0;
19802
+ let totalChangedLineCount = 0;
19803
+ for (const change of changes) {
19804
+ if (!change.added && !change.removed) continue;
19805
+ const lines = changeLines(change);
19806
+ totalChangedLineCount += lines.length;
19807
+ if (change.added) linesAdded += lines.length;
19808
+ if (change.removed) linesRemoved += lines.length;
19809
+ rawHunks.push({ type: change.added ? "added" : "removed", lines });
19810
+ }
19811
+ let hunksTruncated = false;
19812
+ const hunksWithinCount = rawHunks.length > MAX_DIFF_HUNKS ? rawHunks.slice(0, MAX_DIFF_HUNKS) : rawHunks;
19813
+ if (rawHunks.length > MAX_DIFF_HUNKS) hunksTruncated = true;
19814
+ const hunks = [];
19815
+ let emittedLines = 0;
19816
+ for (const hunk of hunksWithinCount) {
19817
+ if (emittedLines >= MAX_DIFF_LINES_PER_RESPONSE) {
19818
+ hunksTruncated = true;
19819
+ break;
19820
+ }
19821
+ const remaining = MAX_DIFF_LINES_PER_RESPONSE - emittedLines;
19822
+ if (hunk.lines.length > remaining) {
19823
+ hunks.push({ type: hunk.type, lines: hunk.lines.slice(0, remaining) });
19824
+ emittedLines += remaining;
19825
+ hunksTruncated = true;
19826
+ break;
19827
+ }
19828
+ hunks.push(hunk);
19829
+ emittedLines += hunk.lines.length;
19830
+ }
19831
+ const denominator = Math.max(oldContent.split("\n").length, newContent.split("\n").length, 1);
19832
+ const percentChanged = totalChangedLineCount > 0 ? Math.min(100, Math.round(totalChangedLineCount / denominator * 1e3) / 10) : 0;
19833
+ return {
19834
+ hunks,
19835
+ linesAdded,
19836
+ linesRemoved,
19837
+ percentChanged,
19838
+ hunksTruncated,
19839
+ hunksTruncatedReason: hunksTruncated ? `Showing ${emittedLines} of ${totalChangedLineCount} changed lines (capped at ${MAX_DIFF_HUNKS} hunks / ${MAX_DIFF_LINES_PER_RESPONSE} lines). Use contentHash/previousContentHash to confirm the full scope of change.` : null,
19840
+ totalChangedLineCount
19841
+ };
19842
+ }
19843
+
19648
19844
  // src/api/scrape-vault-sink.ts
19649
19845
  var MAX_DEPOSIT_CHARS = 5e6;
19650
19846
  var SCRAPE_FALLBACK_PREFIX = "scrape-fallback/";
@@ -20974,6 +21170,91 @@ app.post("/extract-url", auth2, async (c) => {
20974
21170
  await releaseConcurrencyGate(gate.lockId);
20975
21171
  }
20976
21172
  });
21173
+ app.post("/diff-page", auth2, async (c) => {
21174
+ const raw = await c.req.json().catch(() => ({}));
21175
+ const bodyResult = DiffPageBodySchema.safeParse(raw);
21176
+ if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
21177
+ const { url, allowLocal, resetBaseline } = bodyResult.data;
21178
+ if (!allowLocal) {
21179
+ const checked = await validatePublicHttpUrl(url, { field: "URL" });
21180
+ if (checked.error || !checked.parsed) return c.json({ error: checked.error ?? "Invalid URL" }, 400);
21181
+ } else {
21182
+ try {
21183
+ new URL(url);
21184
+ } catch {
21185
+ return c.json({ error: "Invalid URL" }, 400);
21186
+ }
21187
+ }
21188
+ const canonicalUrl = (() => {
21189
+ try {
21190
+ return new URL(url).href;
21191
+ } catch {
21192
+ return url;
21193
+ }
21194
+ })();
21195
+ const user = c.get("user");
21196
+ const gate = await acquireConcurrencyGate(user, "diff_page", {
21197
+ reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"),
21198
+ metadata: { url: canonicalUrl }
21199
+ });
21200
+ if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
21201
+ let debited = false;
21202
+ try {
21203
+ const { ok: dpOk, balance_mc: dpBal } = await debitMc(user.id, MC_COSTS.diff_page, LedgerOperation.DIFF_PAGE, new URL(canonicalUrl).hostname);
21204
+ if (!dpOk) return c.json(insufficientBalanceResponse(dpBal, MC_COSTS.diff_page), 402);
21205
+ debited = true;
21206
+ const existingSnapshot = await getPageSnapshot(user.id, canonicalUrl);
21207
+ const previous = resetBaseline ? null : existingSnapshot;
21208
+ const kernelApiKey = browserServiceApiKey();
21209
+ const result = await extractKpo({ url: canonicalUrl, kernelApiKey });
21210
+ const currentContent = result.bodyMarkdown ?? "";
21211
+ const currentHash = sha256Hex(currentContent);
21212
+ const { value: storedContent, truncated: contentTruncated } = truncateForStorage(currentContent);
21213
+ const currentCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
21214
+ await upsertPageSnapshot(user.id, canonicalUrl, {
21215
+ contentHash: currentHash,
21216
+ content: storedContent,
21217
+ title: result.title ?? null,
21218
+ contentBytes: Buffer.byteLength(currentContent, "utf8"),
21219
+ truncated: contentTruncated
21220
+ });
21221
+ let status;
21222
+ let diff = { hunks: [], linesAdded: 0, linesRemoved: 0, percentChanged: 0, hunksTruncated: false, hunksTruncatedReason: null, totalChangedLineCount: 0 };
21223
+ if (!previous) {
21224
+ status = "baseline";
21225
+ diff.percentChanged = null;
21226
+ } else if (previous.contentHash === currentHash) {
21227
+ status = "unchanged";
21228
+ } else {
21229
+ status = "changed";
21230
+ diff = diffPageContent(previous.content, storedContent);
21231
+ }
21232
+ await logRequestEvent({ userId: user.id, source: "diff_page", status: "done", query: canonicalUrl, result: { status } });
21233
+ return c.json({
21234
+ url: canonicalUrl,
21235
+ title: result.title ?? null,
21236
+ status,
21237
+ isReset: !!resetBaseline && !!existingSnapshot,
21238
+ previousCheckedAt: previous?.checkedAt ?? null,
21239
+ currentCheckedAt,
21240
+ contentHash: currentHash,
21241
+ previousContentHash: previous?.contentHash ?? null,
21242
+ summary: { linesAdded: diff.linesAdded, linesRemoved: diff.linesRemoved, percentChanged: diff.percentChanged },
21243
+ hunks: diff.hunks,
21244
+ contentTruncated,
21245
+ hunksTruncated: diff.hunksTruncated,
21246
+ hunksTruncatedReason: diff.hunksTruncatedReason,
21247
+ totalChangedLineCount: diff.totalChangedLineCount
21248
+ });
21249
+ } catch (err) {
21250
+ const msg = err instanceof Error ? err.message : String(err);
21251
+ if (debited) await creditMc(user.id, MC_COSTS.diff_page, LedgerOperation.DIFF_PAGE, "failed call");
21252
+ await logRequestEvent({ userId: user.id, source: "diff_page", status: "failed", query: canonicalUrl, error: msg });
21253
+ return c.json({ error: msg }, 500);
21254
+ } finally {
21255
+ await releaseConcurrencyGate(gate.lockId);
21256
+ }
21257
+ });
20977
21258
  app.post("/map-urls", auth2, async (c) => {
20978
21259
  const raw = await c.req.json().catch(() => ({}));
20979
21260
  const bodyResult = MapUrlsBodySchema.safeParse(raw);
@@ -21400,7 +21681,7 @@ app.get("/cron/tick", async (c) => {
21400
21681
  if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
21401
21682
  return c.json({ error: "Unauthorized" }, 401);
21402
21683
  }
21403
- const { drainQueue } = await import("./worker-YHKUJR5P.js");
21684
+ const { drainQueue } = await import("./worker-VASAJ7FZ.js");
21404
21685
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
21405
21686
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
21406
21687
  const [results, sweepResult, reapResult, expiredResult, blobCleanup] = await Promise.all([
@@ -21418,15 +21699,15 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
21418
21699
  return c.json({ error: "Unauthorized" }, 401);
21419
21700
  }
21420
21701
  const jobId = c.req.param("id");
21421
- const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-THBVEXMP.js");
21422
- const { assembleExtractArtifacts } = await import("./extract-bundle-UKE273TV.js");
21702
+ const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-LI2S3S6E.js");
21703
+ const { assembleExtractArtifacts } = await import("./extract-bundle-COS56ZDO.js");
21423
21704
  const job = await getExtractJob2(jobId);
21424
21705
  if (!job) return c.json({ error: "job not found" }, 404);
21425
21706
  const stored = await assembleExtractArtifacts(job, {});
21426
21707
  await completeExtractJob2(jobId, stored);
21427
21708
  let settlement = "already_settled_or_refunded";
21428
21709
  if (job.billedMc == null && job.userId != null) {
21429
- const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-THBVEXMP.js");
21710
+ const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-LI2S3S6E.js");
21430
21711
  const heldMc = Number(job.options.heldMc ?? 0);
21431
21712
  const successful = await countSuccessfulPages2(jobId);
21432
21713
  const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
@@ -21590,4 +21871,4 @@ app.get("/blog/:slug/", (c) => {
21590
21871
  export {
21591
21872
  app
21592
21873
  };
21593
- //# sourceMappingURL=server-IKT3QVFB.js.map
21874
+ //# sourceMappingURL=server-R3KVQNOT.js.map