mcp-scraper 0.2.7 → 0.2.9

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 (64) hide show
  1. package/README.md +44 -5
  2. package/dist/bin/api-server.cjs +2417 -648
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +3 -3
  5. package/dist/bin/browser-agent-stdio-server.cjs +1 -1
  6. package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
  7. package/dist/bin/browser-agent-stdio-server.js +2 -2
  8. package/dist/bin/mcp-scraper-cli.cjs +1276 -0
  9. package/dist/bin/mcp-scraper-cli.cjs.map +1 -0
  10. package/dist/bin/mcp-scraper-cli.d.cts +1 -0
  11. package/dist/bin/mcp-scraper-cli.d.ts +1 -0
  12. package/dist/bin/mcp-scraper-cli.js +476 -0
  13. package/dist/bin/mcp-scraper-cli.js.map +1 -0
  14. package/dist/bin/mcp-scraper-combined-stdio-server.cjs +69 -14
  15. package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
  16. package/dist/bin/mcp-scraper-combined-stdio-server.js +3 -3
  17. package/dist/bin/mcp-scraper-install.cjs +5 -1
  18. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  19. package/dist/bin/mcp-scraper-install.js +5 -1
  20. package/dist/bin/mcp-scraper-install.js.map +1 -1
  21. package/dist/bin/mcp-stdio-server.cjs +69 -14
  22. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  23. package/dist/bin/mcp-stdio-server.js +2 -2
  24. package/dist/bin/paa-harvest.cjs +42 -6
  25. package/dist/bin/paa-harvest.cjs.map +1 -1
  26. package/dist/bin/paa-harvest.js +1 -1
  27. package/dist/{chunk-3TF6UT2P.js → chunk-5E6KJC26.js} +2 -2
  28. package/dist/{chunk-D4CJBZBY.js → chunk-7GCCOT3M.js} +372 -1
  29. package/dist/chunk-7GCCOT3M.js.map +1 -0
  30. package/dist/chunk-BJDCPCIE.js +7 -0
  31. package/dist/chunk-BJDCPCIE.js.map +1 -0
  32. package/dist/{chunk-MY3S7EX7.js → chunk-CQTAKXBN.js} +43 -7
  33. package/dist/chunk-CQTAKXBN.js.map +1 -0
  34. package/dist/chunk-L6IS63WS.js +869 -0
  35. package/dist/chunk-L6IS63WS.js.map +1 -0
  36. package/dist/{chunk-IQOCZGJJ.js → chunk-ROS67BNV.js} +5 -5
  37. package/dist/{chunk-7SI6XIR3.js → chunk-XAY5U67D.js} +70 -15
  38. package/dist/chunk-XAY5U67D.js.map +1 -0
  39. package/dist/{db-YWCNHBLH.js → db-BVHYI57K.js} +38 -2
  40. package/dist/index.cjs +42 -6
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.js +1 -1
  43. package/dist/{server-RIKRBDOI.js → server-BKJ7LULQ.js} +818 -394
  44. package/dist/server-BKJ7LULQ.js.map +1 -0
  45. package/dist/{worker-NAKGTIF5.js → worker-TDJQ6TH3.js} +8 -8
  46. package/docs/mcp-tool-quality-spec.md +3 -1
  47. package/docs/specs/agent-ready-seo-packet-spec.md +237 -0
  48. package/docs/specs/cli-agent-wiring-spec.md +203 -0
  49. package/docs/specs/deferred-work-spec.md +12 -0
  50. package/docs/specs/local-competitive-audit-spec.md +312 -0
  51. package/docs/specs/scheduled-workflows-api-spec.md +304 -0
  52. package/docs/specs/seo-cli-growth-roadmap-spec.md +179 -0
  53. package/docs/specs/seo-workflow-runner-and-reports-spec.md +241 -0
  54. package/package.json +3 -2
  55. package/dist/chunk-7SI6XIR3.js.map +0 -1
  56. package/dist/chunk-D4CJBZBY.js.map +0 -1
  57. package/dist/chunk-IZE5UW7Y.js +0 -7
  58. package/dist/chunk-IZE5UW7Y.js.map +0 -1
  59. package/dist/chunk-MY3S7EX7.js.map +0 -1
  60. package/dist/server-RIKRBDOI.js.map +0 -1
  61. /package/dist/{chunk-3TF6UT2P.js.map → chunk-5E6KJC26.js.map} +0 -0
  62. /package/dist/{chunk-IQOCZGJJ.js.map → chunk-ROS67BNV.js.map} +0 -0
  63. /package/dist/{db-YWCNHBLH.js.map → db-BVHYI57K.js.map} +0 -0
  64. /package/dist/{worker-NAKGTIF5.js.map → worker-TDJQ6TH3.js.map} +0 -0
@@ -15,45 +15,14 @@ import {
15
15
  harvestProblemResponse,
16
16
  insufficientBalanceResponse,
17
17
  serializeHarvestProblem
18
- } from "./chunk-IQOCZGJJ.js";
19
- import {
20
- BrowserDriver,
21
- MapsPlaceOptionsSchema,
22
- MapsSearchOptionsSchema,
23
- MapsSelectors,
24
- RawMapsAboutAttributeSchema,
25
- RawMapsHoursRowSchema,
26
- RawMapsOverviewSchema,
27
- RawMapsReviewStatsSchema,
28
- browserServiceApiKey,
29
- browserServiceProxyId,
30
- buildYouTubeChannelVideosUrl,
31
- deleteKernelProxyId,
32
- harvest,
33
- resolveKernelProxyId
34
- } from "./chunk-MY3S7EX7.js";
35
- import {
36
- CaptureSerpPageSnapshotsInputSchema,
37
- CaptureSerpSnapshotInputSchema,
38
- HttpMcpToolExecutor,
39
- buildPaaExtractorMcpServer,
40
- configureReportSaving,
41
- harvestTimeoutBudget,
42
- liveWebToolAnnotations,
43
- outputBaseDir
44
- } from "./chunk-7SI6XIR3.js";
45
- import {
46
- CaptchaError,
47
- RECAPTCHA_INSTRUCTIONS,
48
- sanitizeVendorName
49
- } from "./chunk-M2S27J6Z.js";
50
- import "./chunk-IZE5UW7Y.js";
18
+ } from "./chunk-ROS67BNV.js";
51
19
  import {
52
20
  SiteAuditJobRowSchema,
53
21
  cancelJob,
54
22
  checkRateLimit,
55
23
  claimMonthlyFreeRefresh,
56
24
  completeJob,
25
+ completeWorkflowRunRecord,
57
26
  consumePasswordResetToken,
58
27
  countActiveJobsForUser,
59
28
  countActiveUsers,
@@ -61,10 +30,14 @@ import {
61
30
  createPasswordResetToken,
62
31
  createRunningJob,
63
32
  createUser,
33
+ createWorkflowRun,
34
+ createWorkflowSchedule,
64
35
  creditMc,
65
36
  deactivateUser,
66
37
  debitMc,
38
+ deleteWorkflowSchedule,
67
39
  failJob,
40
+ failWorkflowRunRecord,
68
41
  getDb,
69
42
  getJob,
70
43
  getLedger,
@@ -73,15 +46,27 @@ import {
73
46
  getUserById,
74
47
  getUserByStripeCustomerId,
75
48
  getUserStats,
49
+ getWorkflowArtifact,
50
+ getWorkflowRun,
51
+ getWorkflowSchedule,
76
52
  ledgerExistsForOperation,
77
53
  ledgerExistsForStripePI,
54
+ listDueWorkflowSchedules,
78
55
  listHarvestAttempts,
79
56
  listJobs,
80
57
  listRequestEvents,
81
58
  listUsers,
59
+ listWorkflowArtifacts,
60
+ listWorkflowRuns,
61
+ listWorkflowSchedules,
82
62
  logRequestEvent,
63
+ markWorkflowRunRunning,
64
+ markWorkflowScheduleRan,
65
+ patchWorkflowSchedule,
83
66
  reconcileBalanceMc,
84
67
  recordStripeEvent,
68
+ recordWorkflowWebhookDelivery,
69
+ replaceWorkflowArtifacts,
85
70
  revokeApiKey,
86
71
  rotateApiKey,
87
72
  setConcurrencySubId,
@@ -89,7 +74,47 @@ import {
89
74
  setPassword,
90
75
  setStripeCustomerId,
91
76
  verifyPassword
92
- } from "./chunk-D4CJBZBY.js";
77
+ } from "./chunk-7GCCOT3M.js";
78
+ import {
79
+ BrowserDriver,
80
+ MapsPlaceOptionsSchema,
81
+ MapsSearchOptionsSchema,
82
+ MapsSelectors,
83
+ RawMapsAboutAttributeSchema,
84
+ RawMapsHoursRowSchema,
85
+ RawMapsOverviewSchema,
86
+ RawMapsReviewStatsSchema,
87
+ browserServiceApiKey,
88
+ browserServiceProxyId,
89
+ buildYouTubeChannelVideosUrl,
90
+ deleteKernelProxyId,
91
+ harvest,
92
+ resolveKernelProxyId
93
+ } from "./chunk-CQTAKXBN.js";
94
+ import {
95
+ CaptureSerpPageSnapshotsInputSchema,
96
+ CaptureSerpSnapshotInputSchema,
97
+ HttpMcpToolExecutor,
98
+ buildPaaExtractorMcpServer,
99
+ configureReportSaving,
100
+ harvestTimeoutBudget,
101
+ liveWebToolAnnotations,
102
+ outputBaseDir
103
+ } from "./chunk-XAY5U67D.js";
104
+ import {
105
+ CaptchaError,
106
+ RECAPTCHA_INSTRUCTIONS,
107
+ sanitizeVendorName
108
+ } from "./chunk-M2S27J6Z.js";
109
+ import {
110
+ csvRecords,
111
+ listWorkflowDefinitions,
112
+ rowsToCsv,
113
+ runWorkflow,
114
+ workflowDefinition,
115
+ workflowOutputBaseDir
116
+ } from "./chunk-L6IS63WS.js";
117
+ import "./chunk-BJDCPCIE.js";
93
118
 
94
119
  // src/api/outbound-sanitize.ts
95
120
  var KEY_RENAMES = {
@@ -4848,7 +4873,7 @@ async function extractSite(opts) {
4848
4873
  }
4849
4874
 
4850
4875
  // src/api/server.ts
4851
- import { Hono as Hono11 } from "hono";
4876
+ import { Hono as Hono12 } from "hono";
4852
4877
  import { serve as serveInngest } from "inngest/hono";
4853
4878
 
4854
4879
  // src/inngest/client.ts
@@ -9893,24 +9918,205 @@ var MapsSearchExtractor = class {
9893
9918
  }
9894
9919
  };
9895
9920
 
9896
- // src/api/maps-routes.ts
9897
- function mapsErrorResponse(c, msg, errorCode) {
9898
- const blocked = msg.includes("CAPTCHA") || msg.includes("blocked");
9899
- return c.json({
9900
- error: sanitizeVendorName(msg),
9901
- error_code: blocked ? "captcha_or_blocked" : errorCode,
9902
- retryable: blocked
9903
- }, blocked ? 503 : 500);
9921
+ // src/maps-search-rotation.ts
9922
+ var MAPS_SEARCH_MAX_ATTEMPTS = 3;
9923
+ var MAPS_SEARCH_LOCATION_PROXY_MAX_ATTEMPTS = 5;
9924
+ var MapsSearchRotationError = class extends Error {
9925
+ constructor(message, attempts, retryable, errorCode = "maps_search_failed") {
9926
+ super(message);
9927
+ this.attempts = attempts;
9928
+ this.retryable = retryable;
9929
+ this.errorCode = errorCode;
9930
+ }
9931
+ attempts;
9932
+ retryable;
9933
+ errorCode;
9934
+ name = "MapsSearchRotationError";
9935
+ };
9936
+ function mapsSearchMaxAttemptsForProxyMode(proxyMode) {
9937
+ return proxyMode === "location" ? MAPS_SEARCH_LOCATION_PROXY_MAX_ATTEMPTS : MAPS_SEARCH_MAX_ATTEMPTS;
9938
+ }
9939
+ function mapsSearchErrorMessage(err) {
9940
+ return sanitizeVendorName(err instanceof Error ? err.message : String(err));
9941
+ }
9942
+ function looksLikeProxyTunnelFailure(message) {
9943
+ return /ERR_TUNNEL_CONNECTION_FAILED|ERR_PROXY_CONNECTION_FAILED|ERR_SOCKS_CONNECTION_FAILED|tunnel connection failed|proxy connection failed|transport error: proxy/i.test(message);
9944
+ }
9945
+ function looksLikeProxyUnavailable(message) {
9946
+ return /proxy unavailable|proxy_unavailable|connection_test_failed|did not return a proxy id|configured fallback/i.test(message);
9947
+ }
9948
+ function looksLikeBrowserClosed(message) {
9949
+ return /Target page, context or browser has been closed|browser has been closed|context has been closed|page has been closed|browser not found|CDP session closed|Connection closed|WebSocket.*closed|Protocol error.*closed|Browser closed|disconnected/i.test(message);
9950
+ }
9951
+ function mapsSearchFailureOutcome(err) {
9952
+ if (err instanceof CaptchaError) return "captcha";
9953
+ const message = mapsSearchErrorMessage(err);
9954
+ if (/timeout|timed out|Timeout \d+ms exceeded|deadline/i.test(message)) return "timeout";
9955
+ if (looksLikeProxyTunnelFailure(message)) return "proxy_tunnel_failed";
9956
+ if (looksLikeProxyUnavailable(message)) return "proxy_unavailable";
9957
+ if (looksLikeBrowserClosed(message)) return "browser_closed";
9958
+ return "unknown_error";
9959
+ }
9960
+ function retryableMapsSearchError(err, proxyMode) {
9961
+ if (err instanceof CaptchaError) return true;
9962
+ const message = mapsSearchErrorMessage(err);
9963
+ if (/timeout|timed out|Timeout \d+ms exceeded|deadline/i.test(message)) return true;
9964
+ if (looksLikeBrowserClosed(message)) return true;
9965
+ return proxyMode === "location" && (looksLikeProxyTunnelFailure(message) || looksLikeProxyUnavailable(message));
9966
+ }
9967
+ function suffix(value) {
9968
+ return value ? value.slice(-8) : null;
9969
+ }
9970
+ function attemptFromDebug(input) {
9971
+ const network = input.debug.networkLocation;
9972
+ const browserSessionId = input.debug.kernel?.sessionId ?? null;
9973
+ const target = input.resolution?.target ?? null;
9974
+ return {
9975
+ attemptNumber: input.attemptIndex + 1,
9976
+ maxAttempts: input.maxAttempts,
9977
+ status: input.status,
9978
+ outcome: input.outcome,
9979
+ willRetry: input.willRetry,
9980
+ durationMs: input.durationMs,
9981
+ resultCount: input.resultCount,
9982
+ error: input.error,
9983
+ proxyMode: input.proxyMode,
9984
+ proxyResolutionSource: input.resolution?.source ?? null,
9985
+ proxyIdSuffix: input.resolution?.proxyIdSuffix ?? null,
9986
+ proxyTargetLevel: target?.level ?? null,
9987
+ proxyTargetLocation: target?.canonicalLocation ?? null,
9988
+ proxyTargetZip: target?.zip ?? null,
9989
+ browserSessionIdSuffix: suffix(browserSessionId),
9990
+ observedIp: network?.ip ?? null,
9991
+ observedCity: network?.city ?? null,
9992
+ observedRegion: network?.region ?? null
9993
+ };
9904
9994
  }
9905
- async function cleanupDisposableProxy(kernelApiKey, proxyId) {
9995
+ async function cleanupDisposableProxy(kernelApiKey, proxyId, eventName) {
9906
9996
  if (!kernelApiKey || !proxyId) return;
9907
- await deleteKernelProxyId(kernelApiKey, proxyId).catch((err) => {
9997
+ try {
9998
+ await deleteKernelProxyId(kernelApiKey, proxyId);
9999
+ } catch (err) {
9908
10000
  console.warn(JSON.stringify({
9909
- event: "maps_search_proxy_delete_failed",
9910
- proxy_id_suffix: proxyId.slice(-6),
9911
- message: err instanceof Error ? err.message : String(err)
10001
+ event: eventName,
10002
+ proxy_id_suffix: suffix(proxyId),
10003
+ message: mapsSearchErrorMessage(err)
9912
10004
  }));
9913
- });
10005
+ }
10006
+ }
10007
+ async function runMapsSearchWithRotation(options) {
10008
+ const attempts = [];
10009
+ const maxAttempts = mapsSearchMaxAttemptsForProxyMode(options.proxyMode);
10010
+ const started = Date.now();
10011
+ let lastError = null;
10012
+ for (let attemptIndex = 0; attemptIndex < maxAttempts; attemptIndex += 1) {
10013
+ const attemptStarted = Date.now();
10014
+ const driver = new BrowserDriver();
10015
+ const extractor = new MapsSearchExtractor(driver);
10016
+ const proxyZip = options.proxyZipForAttempt?.(attemptIndex) ?? options.proxyZip;
10017
+ let resolution = null;
10018
+ let disposableProxyId;
10019
+ try {
10020
+ resolution = await resolveKernelProxyId({
10021
+ kernelApiKey: options.kernelApiKey,
10022
+ proxyMode: options.proxyMode,
10023
+ configuredKernelProxyId: options.kernelProxyId ?? options.configuredKernelProxyId,
10024
+ location: options.location,
10025
+ proxyZip,
10026
+ gl: options.gl,
10027
+ attemptIndex,
10028
+ fresh: options.proxyMode === "location"
10029
+ });
10030
+ disposableProxyId = resolution.disposableProxyId;
10031
+ const result = await extractor.extract({
10032
+ ...options,
10033
+ proxyZip,
10034
+ kernelProxyId: options.proxyMode === "none" ? void 0 : resolution.kernelProxyId,
10035
+ kernelProxyResolution: resolution.resolution
10036
+ });
10037
+ const attempt = attemptFromDebug({
10038
+ attemptIndex,
10039
+ maxAttempts,
10040
+ status: "ok",
10041
+ outcome: "success",
10042
+ willRetry: false,
10043
+ durationMs: Date.now() - attemptStarted,
10044
+ resultCount: result.resultCount,
10045
+ error: null,
10046
+ proxyMode: options.proxyMode,
10047
+ resolution: resolution.resolution,
10048
+ debug: driver.getDebugSnapshot()
10049
+ });
10050
+ attempts.push(attempt);
10051
+ return {
10052
+ ...result,
10053
+ attempts: attempts.slice(),
10054
+ durationMs: Date.now() - started
10055
+ };
10056
+ } catch (err) {
10057
+ lastError = err;
10058
+ const retryable = retryableMapsSearchError(err, options.proxyMode);
10059
+ const willRetry = attemptIndex < maxAttempts - 1 && retryable;
10060
+ const attempt = attemptFromDebug({
10061
+ attemptIndex,
10062
+ maxAttempts,
10063
+ status: "failed",
10064
+ outcome: mapsSearchFailureOutcome(err),
10065
+ willRetry,
10066
+ durationMs: Date.now() - attemptStarted,
10067
+ resultCount: 0,
10068
+ error: mapsSearchErrorMessage(err),
10069
+ proxyMode: options.proxyMode,
10070
+ resolution: resolution?.resolution ?? null,
10071
+ debug: driver.getDebugSnapshot()
10072
+ });
10073
+ attempts.push(attempt);
10074
+ console.warn(JSON.stringify({
10075
+ event: options.failureEventName ?? "maps_search_attempt_failed",
10076
+ ...options.logContext,
10077
+ attempt_number: attempt.attemptNumber,
10078
+ max_attempts: attempt.maxAttempts,
10079
+ outcome: attempt.outcome,
10080
+ will_retry: attempt.willRetry,
10081
+ proxy_resolution_source: attempt.proxyResolutionSource,
10082
+ proxy_id_suffix: attempt.proxyIdSuffix,
10083
+ browser_session_id_suffix: attempt.browserSessionIdSuffix,
10084
+ observed_city: attempt.observedCity,
10085
+ observed_region: attempt.observedRegion,
10086
+ message: attempt.error
10087
+ }));
10088
+ if (!willRetry) {
10089
+ throw new MapsSearchRotationError(
10090
+ mapsSearchErrorMessage(err),
10091
+ attempts,
10092
+ retryable,
10093
+ err instanceof CaptchaError ? "captcha_or_blocked" : "maps_search_failed"
10094
+ );
10095
+ }
10096
+ } finally {
10097
+ await cleanupDisposableProxy(options.kernelApiKey, disposableProxyId, "maps_search_proxy_delete_failed");
10098
+ await driver.close();
10099
+ }
10100
+ }
10101
+ throw new MapsSearchRotationError(
10102
+ lastError ? mapsSearchErrorMessage(lastError) : "Maps search failed",
10103
+ attempts,
10104
+ true
10105
+ );
10106
+ }
10107
+
10108
+ // src/api/maps-routes.ts
10109
+ function mapsErrorResponse(c, err, errorCode) {
10110
+ const msg = err instanceof Error ? err.message : String(err);
10111
+ const rotationError = err instanceof MapsSearchRotationError ? err : null;
10112
+ const blocked = msg.includes("CAPTCHA") || msg.includes("blocked");
10113
+ const retryable = rotationError?.retryable ?? blocked;
10114
+ return c.json({
10115
+ error: sanitizeVendorName(msg),
10116
+ error_code: rotationError?.errorCode ?? (blocked ? "captcha_or_blocked" : errorCode),
10117
+ retryable,
10118
+ attempts: rotationError?.attempts ?? void 0
10119
+ }, retryable ? 503 : 500);
9914
10120
  }
9915
10121
  var mapsApp = new Hono5();
9916
10122
  mapsApp.post("/search", createApiKeyAuth(), async (c) => {
@@ -9930,24 +10136,10 @@ mapsApp.post("/search", createApiKeyAuth(), async (c) => {
9930
10136
  [parsed.data.query, parsed.data.location].filter(Boolean).join(" ")
9931
10137
  );
9932
10138
  if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.maps_search), 402);
9933
- const driver = new BrowserDriver();
9934
- const extractor = new MapsSearchExtractor(driver);
9935
- let disposableProxyId;
9936
10139
  try {
9937
- const resolution = await resolveKernelProxyId({
9938
- kernelApiKey: parsed.data.kernelApiKey,
9939
- proxyMode: parsed.data.proxyMode,
9940
- configuredKernelProxyId: browserServiceProxyId(),
9941
- location: parsed.data.location,
9942
- proxyZip: parsed.data.proxyZip,
9943
- gl: parsed.data.gl,
9944
- fresh: parsed.data.proxyMode === "location"
9945
- });
9946
- disposableProxyId = resolution.disposableProxyId;
9947
- const result = await extractor.extract({
10140
+ const result = await runMapsSearchWithRotation({
9948
10141
  ...parsed.data,
9949
- kernelProxyId: parsed.data.proxyMode === "none" ? void 0 : resolution.kernelProxyId,
9950
- kernelProxyResolution: resolution.resolution
10142
+ configuredKernelProxyId: browserServiceProxyId()
9951
10143
  });
9952
10144
  await logRequestEvent({
9953
10145
  userId: user.id,
@@ -9968,12 +10160,10 @@ mapsApp.post("/search", createApiKeyAuth(), async (c) => {
9968
10160
  status: "failed",
9969
10161
  query: [parsed.data.query, parsed.data.location].filter(Boolean).join(" "),
9970
10162
  location: parsed.data.location,
9971
- error: msg
10163
+ error: msg,
10164
+ result: err instanceof MapsSearchRotationError ? { attempts: err.attempts } : void 0
9972
10165
  });
9973
- return mapsErrorResponse(c, msg, "maps_search_failed");
9974
- } finally {
9975
- await cleanupDisposableProxy(parsed.data.kernelApiKey, disposableProxyId);
9976
- await driver.close();
10166
+ return mapsErrorResponse(c, err, "maps_search_failed");
9977
10167
  }
9978
10168
  });
9979
10169
  mapsApp.post("/place", createApiKeyAuth(), async (c) => {
@@ -10056,69 +10246,6 @@ import { mkdir as mkdir2, writeFile } from "fs/promises";
10056
10246
  import { join as join4 } from "path";
10057
10247
  import { z as z15 } from "zod";
10058
10248
 
10059
- // src/directory/csv.ts
10060
- function parseCsv(text) {
10061
- const rows = [];
10062
- let row = [];
10063
- let field = "";
10064
- let quoted = false;
10065
- for (let i = 0; i < text.length; i += 1) {
10066
- const ch = text[i];
10067
- const next = text[i + 1];
10068
- if (quoted) {
10069
- if (ch === '"' && next === '"') {
10070
- field += '"';
10071
- i += 1;
10072
- } else if (ch === '"') {
10073
- quoted = false;
10074
- } else {
10075
- field += ch;
10076
- }
10077
- continue;
10078
- }
10079
- if (ch === '"') {
10080
- quoted = true;
10081
- } else if (ch === ",") {
10082
- row.push(field);
10083
- field = "";
10084
- } else if (ch === "\n") {
10085
- row.push(field);
10086
- rows.push(row);
10087
- row = [];
10088
- field = "";
10089
- } else if (ch !== "\r") {
10090
- field += ch;
10091
- }
10092
- }
10093
- if (field.length > 0 || row.length > 0) {
10094
- row.push(field);
10095
- rows.push(row);
10096
- }
10097
- return rows;
10098
- }
10099
- function csvRecords(text) {
10100
- const rows = parseCsv(text).filter((row) => row.some((cell) => cell.trim() !== ""));
10101
- const header = rows[0]?.map((cell) => cell.trim()) ?? [];
10102
- return rows.slice(1).map((row) => {
10103
- const record = {};
10104
- for (let i = 0; i < header.length; i += 1) {
10105
- record[header[i]] = row[i] ?? "";
10106
- }
10107
- return record;
10108
- });
10109
- }
10110
- function csvCell(value) {
10111
- if (value === null || value === void 0) return "";
10112
- const text = String(value);
10113
- return /[",\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
10114
- }
10115
- function rowsToCsv(headers, rows) {
10116
- return [
10117
- headers.join(","),
10118
- ...rows.map((row) => headers.map((header) => csvCell(row[header])).join(","))
10119
- ].join("\n") + "\n";
10120
- }
10121
-
10122
10249
  // src/directory/location-db.ts
10123
10250
  import { access, readFile } from "fs/promises";
10124
10251
  var POPULATION_YEARS = [2020, 2021, 2022, 2023, 2024, 2025];
@@ -10278,8 +10405,6 @@ async function resolveDirectoryMarkets(options) {
10278
10405
  }
10279
10406
 
10280
10407
  // src/directory/directory-workflow.ts
10281
- var DIRECTORY_MAX_ATTEMPTS = 3;
10282
- var DIRECTORY_LOCATION_PROXY_MAX_ATTEMPTS = 5;
10283
10408
  var DirectoryWorkflowOptionsSchema = z15.object({
10284
10409
  query: z15.string().min(1),
10285
10410
  state: z15.string().min(2).default("TN"),
@@ -10299,36 +10424,9 @@ var DirectoryWorkflowOptionsSchema = z15.object({
10299
10424
  headless: z15.boolean().default(true),
10300
10425
  kernelApiKey: z15.string().optional()
10301
10426
  });
10302
- async function cleanupDisposableProxy2(kernelApiKey, proxyId) {
10303
- if (!kernelApiKey || !proxyId) return;
10304
- try {
10305
- await deleteKernelProxyId(kernelApiKey, proxyId);
10306
- } catch (err) {
10307
- console.warn(JSON.stringify({
10308
- event: "directory_workflow_proxy_delete_failed",
10309
- proxy_id_suffix: proxyId.slice(-6),
10310
- message: err instanceof Error ? err.message : String(err)
10311
- }));
10312
- }
10313
- }
10314
- function maxAttemptsForProxyMode(proxyMode) {
10315
- return proxyMode === "location" ? DIRECTORY_LOCATION_PROXY_MAX_ATTEMPTS : DIRECTORY_MAX_ATTEMPTS;
10316
- }
10317
10427
  function errorMessage(err) {
10318
10428
  return err instanceof Error ? err.message : String(err);
10319
10429
  }
10320
- function looksLikeProxyTunnelFailure(message) {
10321
- return /ERR_TUNNEL_CONNECTION_FAILED|ERR_PROXY_CONNECTION_FAILED|ERR_SOCKS_CONNECTION_FAILED|tunnel connection failed|proxy connection failed|transport error: proxy/i.test(message);
10322
- }
10323
- function looksLikeProxyUnavailable(message) {
10324
- return /proxy unavailable|proxy_unavailable|connection_test_failed|did not return a proxy id|configured fallback/i.test(message);
10325
- }
10326
- function retryableCitySearchError(err, proxyMode) {
10327
- if (err instanceof CaptchaError) return true;
10328
- const message = errorMessage(err);
10329
- if (/timeout|timed out|Timeout \d+ms exceeded|deadline/i.test(message)) return true;
10330
- return proxyMode === "location" && (looksLikeProxyTunnelFailure(message) || looksLikeProxyUnavailable(message));
10331
- }
10332
10430
  function proxyZipForAttempt(options, market, attemptIndex) {
10333
10431
  if (options.proxyZip) return options.proxyZip;
10334
10432
  if (!market.zips.length) return void 0;
@@ -10347,25 +10445,10 @@ async function mapLimit(items, limit, fn) {
10347
10445
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
10348
10446
  return out;
10349
10447
  }
10350
- async function searchCityAttempt(options, market, attemptIndex) {
10351
- const driver = new BrowserDriver();
10352
- const extractor = new MapsSearchExtractor(driver);
10353
- const start = Date.now();
10354
- let disposableProxyId;
10448
+ async function searchCity(options, market) {
10449
+ const started = Date.now();
10355
10450
  try {
10356
- const proxyZip = proxyZipForAttempt(options, market, attemptIndex);
10357
- const resolution = await resolveKernelProxyId({
10358
- kernelApiKey: options.kernelApiKey,
10359
- proxyMode: options.proxyMode,
10360
- configuredKernelProxyId: browserServiceProxyId(),
10361
- location: market.location,
10362
- proxyZip,
10363
- gl: options.gl,
10364
- attemptIndex,
10365
- fresh: options.proxyMode === "location"
10366
- });
10367
- disposableProxyId = resolution.disposableProxyId;
10368
- const result = await extractor.extract({
10451
+ const result = await runMapsSearchWithRotation({
10369
10452
  query: options.query,
10370
10453
  location: market.location,
10371
10454
  gl: options.gl,
@@ -10373,11 +10456,16 @@ async function searchCityAttempt(options, market, attemptIndex) {
10373
10456
  maxResults: options.maxResultsPerCity,
10374
10457
  headless: options.headless,
10375
10458
  kernelApiKey: options.kernelApiKey,
10376
- kernelProxyId: options.proxyMode === "none" ? void 0 : resolution.kernelProxyId,
10377
- kernelProxyResolution: resolution.resolution,
10378
10459
  proxyMode: options.proxyMode,
10379
- proxyZip,
10380
- debug: options.debug
10460
+ proxyZip: options.proxyZip,
10461
+ debug: options.debug,
10462
+ configuredKernelProxyId: browserServiceProxyId(),
10463
+ proxyZipForAttempt: (attemptIndex) => proxyZipForAttempt(options, market, attemptIndex),
10464
+ failureEventName: "directory_workflow_city_attempt_failed",
10465
+ logContext: {
10466
+ city: market.city,
10467
+ state: market.state
10468
+ }
10381
10469
  });
10382
10470
  return {
10383
10471
  city: market.city,
@@ -10393,50 +10481,29 @@ async function searchCityAttempt(options, market, attemptIndex) {
10393
10481
  error: null,
10394
10482
  resultCount: result.resultCount,
10395
10483
  durationMs: result.durationMs,
10484
+ attempts: result.attempts ?? [],
10396
10485
  results: result.results
10397
10486
  };
10398
- } finally {
10399
- await cleanupDisposableProxy2(options.kernelApiKey, disposableProxyId);
10400
- }
10401
- }
10402
- async function searchCity(options, market) {
10403
- const started = Date.now();
10404
- const maxAttempts = maxAttemptsForProxyMode(options.proxyMode);
10405
- let lastError = null;
10406
- for (let attemptIndex = 0; attemptIndex < maxAttempts; attemptIndex += 1) {
10407
- try {
10408
- return await searchCityAttempt(options, market, attemptIndex);
10409
- } catch (err) {
10410
- lastError = err;
10411
- const willRetry = attemptIndex < maxAttempts - 1 && retryableCitySearchError(err, options.proxyMode);
10412
- console.warn(JSON.stringify({
10413
- event: "directory_workflow_city_attempt_failed",
10414
- city: market.city,
10415
- state: market.state,
10416
- attempt_number: attemptIndex + 1,
10417
- max_attempts: maxAttempts,
10418
- will_retry: willRetry,
10419
- message: errorMessage(err)
10420
- }));
10421
- if (!willRetry) break;
10422
- }
10487
+ } catch (err) {
10488
+ const attempts = err instanceof MapsSearchRotationError ? err.attempts : [];
10489
+ return {
10490
+ city: market.city,
10491
+ state: market.state,
10492
+ location: market.location,
10493
+ cityKey: market.cityKey,
10494
+ censusName: market.censusName,
10495
+ population: market.population,
10496
+ populationYear: market.populationYear,
10497
+ zips: market.zips,
10498
+ counties: market.counties,
10499
+ status: "failed",
10500
+ error: errorMessage(err),
10501
+ resultCount: 0,
10502
+ durationMs: Date.now() - started,
10503
+ attempts,
10504
+ results: []
10505
+ };
10423
10506
  }
10424
- return {
10425
- city: market.city,
10426
- state: market.state,
10427
- location: market.location,
10428
- cityKey: market.cityKey,
10429
- censusName: market.censusName,
10430
- population: market.population,
10431
- populationYear: market.populationYear,
10432
- zips: market.zips,
10433
- counties: market.counties,
10434
- status: "failed",
10435
- error: lastError ? errorMessage(lastError) : "City Maps search failed",
10436
- resultCount: 0,
10437
- durationMs: Date.now() - started,
10438
- results: []
10439
- };
10440
10507
  }
10441
10508
  function csvRowsFor(result) {
10442
10509
  const rows = [];
@@ -10457,6 +10524,7 @@ function csvRowsFor(result) {
10457
10524
  result_position: null,
10458
10525
  business_name: null,
10459
10526
  review_stars: null,
10527
+ review_count: null,
10460
10528
  category: null,
10461
10529
  address: null,
10462
10530
  phone: null,
@@ -10490,6 +10558,7 @@ function csvRowsFor(result) {
10490
10558
  result_position: business.position,
10491
10559
  business_name: business.name,
10492
10560
  review_stars: business.rating,
10561
+ review_count: business.reviewCount,
10493
10562
  category: business.category,
10494
10563
  address: business.address,
10495
10564
  phone: business.phone,
@@ -10530,6 +10599,7 @@ async function saveDirectoryCsv(result) {
10530
10599
  "result_position",
10531
10600
  "business_name",
10532
10601
  "review_stars",
10602
+ "review_count",
10533
10603
  "category",
10534
10604
  "address",
10535
10605
  "phone",
@@ -10632,8 +10702,360 @@ directoryApp.post("/run", createApiKeyAuth(), async (c) => {
10632
10702
  }
10633
10703
  });
10634
10704
 
10635
- // src/api/serp-intelligence-routes.ts
10705
+ // src/api/workflow-routes.ts
10706
+ import { createHmac } from "crypto";
10707
+ import { readFile as readFile2 } from "fs/promises";
10636
10708
  import { Hono as Hono7 } from "hono";
10709
+ import { z as z16 } from "zod";
10710
+ var workflowApp = new Hono7();
10711
+ var WorkflowInputSchema = z16.record(z16.unknown()).default({});
10712
+ var WorkflowIdSchema = z16.string().min(1);
10713
+ var CadenceSchema = z16.enum(["daily", "weekly", "monthly"]);
10714
+ var ScheduleStatusSchema = z16.enum(["active", "paused"]);
10715
+ var RunBodySchema = z16.object({
10716
+ workflowId: WorkflowIdSchema,
10717
+ input: WorkflowInputSchema,
10718
+ webhookUrl: z16.string().url().optional()
10719
+ });
10720
+ var ScheduleCreateSchema = z16.object({
10721
+ workflowId: WorkflowIdSchema,
10722
+ name: z16.string().min(1).max(120).optional(),
10723
+ input: WorkflowInputSchema,
10724
+ cadence: CadenceSchema.default("weekly"),
10725
+ timezone: z16.string().min(1).max(64).default("UTC"),
10726
+ webhookUrl: z16.string().url().optional(),
10727
+ nextRunAt: z16.string().datetime().optional()
10728
+ });
10729
+ var SchedulePatchSchema = z16.object({
10730
+ name: z16.string().min(1).max(120).optional(),
10731
+ status: ScheduleStatusSchema.optional(),
10732
+ input: WorkflowInputSchema.optional(),
10733
+ cadence: CadenceSchema.optional(),
10734
+ timezone: z16.string().min(1).max(64).optional(),
10735
+ webhookUrl: z16.string().url().nullable().optional(),
10736
+ nextRunAt: z16.string().datetime().nullable().optional()
10737
+ });
10738
+ function hostedWorkflowOutputDir() {
10739
+ return workflowOutputBaseDir(process.env.MCP_SCRAPER_WORKFLOW_OUTPUT_DIR?.trim() || "/tmp/mcp-scraper-workflows");
10740
+ }
10741
+ function originFromUrl(url) {
10742
+ const parsed = new URL(url);
10743
+ return `${parsed.protocol}//${parsed.host}`;
10744
+ }
10745
+ function addCadence(fromIso, cadence) {
10746
+ const next = new Date(fromIso);
10747
+ if (!Number.isFinite(next.getTime())) return new Date(Date.now() + 24 * 60 * 60 * 1e3).toISOString();
10748
+ if (cadence === "daily") next.setUTCDate(next.getUTCDate() + 1);
10749
+ else if (cadence === "weekly") next.setUTCDate(next.getUTCDate() + 7);
10750
+ else next.setUTCMonth(next.getUTCMonth() + 1);
10751
+ return next.toISOString();
10752
+ }
10753
+ function defaultNextRunAt(cadence) {
10754
+ return addCadence((/* @__PURE__ */ new Date()).toISOString(), cadence);
10755
+ }
10756
+ function validateWorkflowInput(workflowId, input) {
10757
+ const definition = workflowDefinition(workflowId);
10758
+ return definition.inputSchema.parse(input);
10759
+ }
10760
+ async function validateWebhookUrl(url) {
10761
+ if (!url) return null;
10762
+ const result = await validatePublicHttpUrl(url, { field: "webhookUrl", requireHttps: true });
10763
+ if (result.error) throw new Error(result.error);
10764
+ return url;
10765
+ }
10766
+ function runDownloadUrl(c, runId, artifactId) {
10767
+ return `${originFromUrl(c.req.url)}/workflows/runs/${runId}/artifacts/${artifactId}`;
10768
+ }
10769
+ function exposeRun(c, run, artifacts = []) {
10770
+ return {
10771
+ ...run,
10772
+ artifacts: artifacts.map((artifact) => ({
10773
+ ...artifact,
10774
+ downloadUrl: runDownloadUrl(c, run.id, artifact.id)
10775
+ }))
10776
+ };
10777
+ }
10778
+ async function persistRunArtifacts(runId, userId, manifest) {
10779
+ if (!manifest) return [];
10780
+ return replaceWorkflowArtifacts(runId, userId, manifest.artifacts);
10781
+ }
10782
+ async function readManifestFromSummary(summary) {
10783
+ if (!summary.reportPath) return null;
10784
+ const manifestPath = summary.reportPath.replace(/report\.html$/, "manifest.json");
10785
+ try {
10786
+ return JSON.parse(await readFile2(manifestPath, "utf8"));
10787
+ } catch {
10788
+ return null;
10789
+ }
10790
+ }
10791
+ function webhookSignature(body, timestamp) {
10792
+ const secret2 = process.env.MCP_SCRAPER_WEBHOOK_SECRET?.trim();
10793
+ if (!secret2) return null;
10794
+ return createHmac("sha256", secret2).update(`${timestamp}.${body}`).digest("hex");
10795
+ }
10796
+ async function deliverWorkflowWebhook(input) {
10797
+ if (!input.webhookUrl) return;
10798
+ const payload = {
10799
+ event: input.run.status === "failed" ? "workflow.run.failed" : input.run.status === "partial" ? "workflow.run.partial" : "workflow.run.succeeded",
10800
+ runId: input.run.id,
10801
+ scheduleId: input.scheduleId,
10802
+ workflowId: input.run.workflow_id,
10803
+ status: input.run.status,
10804
+ completedAt: input.run.completed_at ?? (/* @__PURE__ */ new Date()).toISOString(),
10805
+ summary: {
10806
+ title: input.summary?.title ?? input.run.workflow_id,
10807
+ counts: input.summary?.counts ?? {},
10808
+ warnings: input.summary?.warnings ?? [],
10809
+ errors: input.summary?.errors ?? (input.run.error_message ? [input.run.error_message] : [])
10810
+ },
10811
+ artifacts: input.artifacts.map((artifact) => ({
10812
+ kind: artifact.kind,
10813
+ label: artifact.label,
10814
+ path: artifact.path
10815
+ }))
10816
+ };
10817
+ const body = JSON.stringify(payload);
10818
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
10819
+ const headers = {
10820
+ "Content-Type": "application/json",
10821
+ "x-mcp-scraper-timestamp": timestamp
10822
+ };
10823
+ const signature = webhookSignature(body, timestamp);
10824
+ if (signature) headers["x-mcp-scraper-signature"] = signature;
10825
+ let lastError = null;
10826
+ for (let attempt = 1; attempt <= 3; attempt++) {
10827
+ try {
10828
+ const res = await fetch(input.webhookUrl, { method: "POST", headers, body, signal: AbortSignal.timeout(15e3) });
10829
+ if (res.ok) {
10830
+ await recordWorkflowWebhookDelivery({ runId: input.run.id, scheduleId: input.scheduleId, userId: input.userId, webhookUrl: input.webhookUrl, status: "delivered", attempts: attempt });
10831
+ return;
10832
+ }
10833
+ lastError = `HTTP ${res.status}`;
10834
+ } catch (err) {
10835
+ lastError = err instanceof Error ? err.message : String(err);
10836
+ }
10837
+ await new Promise((resolve) => setTimeout(resolve, attempt * 500));
10838
+ }
10839
+ await recordWorkflowWebhookDelivery({ runId: input.run.id, scheduleId: input.scheduleId, userId: input.userId, webhookUrl: input.webhookUrl, status: "failed", attempts: 3, lastError });
10840
+ }
10841
+ async function executeWorkflowRun(input) {
10842
+ await markWorkflowRunRunning(input.runId);
10843
+ try {
10844
+ const summary = await runWorkflow(input.workflowId, input.workflowInput, {
10845
+ apiKey: input.user.api_key,
10846
+ apiUrl: input.apiUrl,
10847
+ outputDir: hostedWorkflowOutputDir(),
10848
+ runId: input.runId
10849
+ });
10850
+ const manifest = await readManifestFromSummary(summary);
10851
+ await completeWorkflowRunRecord(input.runId, summary.status, manifest ?? { summary });
10852
+ const run = await getWorkflowRun(input.runId, input.user.id);
10853
+ if (!run) throw new Error("Workflow run disappeared after execution");
10854
+ const artifacts = await persistRunArtifacts(input.runId, input.user.id, manifest);
10855
+ await deliverWorkflowWebhook({ userId: input.user.id, run, scheduleId: input.scheduleId ?? null, webhookUrl: input.webhookUrl ?? null, summary, artifacts });
10856
+ return { run, summary, artifacts };
10857
+ } catch (err) {
10858
+ const message = err instanceof Error ? err.message : String(err);
10859
+ await failWorkflowRunRecord(input.runId, message, { error: message });
10860
+ const run = await getWorkflowRun(input.runId, input.user.id);
10861
+ if (!run) throw err;
10862
+ await deliverWorkflowWebhook({ userId: input.user.id, run, scheduleId: input.scheduleId ?? null, webhookUrl: input.webhookUrl ?? null, summary: null, artifacts: [] });
10863
+ throw err;
10864
+ }
10865
+ }
10866
+ workflowApp.get("/definitions", createApiKeyAuth(), (c) => {
10867
+ return c.json({ workflows: listWorkflowDefinitions() });
10868
+ });
10869
+ workflowApp.post("/run", createApiKeyAuth(), async (c) => {
10870
+ const user = c.get("user");
10871
+ const parsed = RunBodySchema.safeParse(await c.req.json().catch(() => ({})));
10872
+ if (!parsed.success) return c.json({ error: parsed.error.issues[0]?.message ?? "Invalid request" }, 400);
10873
+ let input;
10874
+ let webhookUrl;
10875
+ try {
10876
+ input = validateWorkflowInput(parsed.data.workflowId, parsed.data.input);
10877
+ webhookUrl = await validateWebhookUrl(parsed.data.webhookUrl);
10878
+ } catch (err) {
10879
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 400);
10880
+ }
10881
+ const run = await createWorkflowRun({ userId: user.id, workflowId: parsed.data.workflowId, workflowInput: input });
10882
+ try {
10883
+ const executed = await executeWorkflowRun({
10884
+ runId: run.id,
10885
+ user,
10886
+ workflowId: parsed.data.workflowId,
10887
+ workflowInput: input,
10888
+ apiUrl: originFromUrl(c.req.url),
10889
+ webhookUrl
10890
+ });
10891
+ return c.json({ run: exposeRun(c, executed.run, executed.artifacts), summary: executed.summary });
10892
+ } catch (err) {
10893
+ const failed = await getWorkflowRun(run.id, user.id);
10894
+ return c.json({ run: failed, error: err instanceof Error ? err.message : String(err) }, 500);
10895
+ }
10896
+ });
10897
+ workflowApp.get("/runs", createApiKeyAuth(), async (c) => {
10898
+ const user = c.get("user");
10899
+ const runs = await listWorkflowRuns(user.id);
10900
+ return c.json({ runs });
10901
+ });
10902
+ workflowApp.get("/runs/:id", createApiKeyAuth(), async (c) => {
10903
+ const user = c.get("user");
10904
+ const run = await getWorkflowRun(c.req.param("id"), user.id);
10905
+ if (!run) return c.json({ error: "Run not found" }, 404);
10906
+ const artifacts = await listWorkflowArtifacts(run.id, user.id);
10907
+ return c.json({ run: exposeRun(c, run, artifacts) });
10908
+ });
10909
+ workflowApp.get("/runs/:id/artifacts/:artifactId", createApiKeyAuth(), async (c) => {
10910
+ const user = c.get("user");
10911
+ const run = await getWorkflowRun(c.req.param("id"), user.id);
10912
+ if (!run) return c.json({ error: "Run not found" }, 404);
10913
+ const artifact = await getWorkflowArtifact(c.req.param("artifactId"), user.id);
10914
+ if (!artifact || artifact.run_id !== run.id) return c.json({ error: "Artifact not found" }, 404);
10915
+ try {
10916
+ const content = await readFile2(artifact.path);
10917
+ return new Response(content, {
10918
+ headers: {
10919
+ "Content-Type": artifact.content_type,
10920
+ "Content-Disposition": `attachment; filename="${artifact.path.split("/").pop() || artifact.id}"`
10921
+ }
10922
+ });
10923
+ } catch {
10924
+ return c.json({ error: "Artifact file is no longer available" }, 410);
10925
+ }
10926
+ });
10927
+ workflowApp.post("/schedules", createApiKeyAuth(), async (c) => {
10928
+ const user = c.get("user");
10929
+ const parsed = ScheduleCreateSchema.safeParse(await c.req.json().catch(() => ({})));
10930
+ if (!parsed.success) return c.json({ error: parsed.error.issues[0]?.message ?? "Invalid request" }, 400);
10931
+ let input;
10932
+ let webhookUrl;
10933
+ try {
10934
+ input = validateWorkflowInput(parsed.data.workflowId, parsed.data.input);
10935
+ webhookUrl = await validateWebhookUrl(parsed.data.webhookUrl);
10936
+ } catch (err) {
10937
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 400);
10938
+ }
10939
+ const schedule = await createWorkflowSchedule({
10940
+ userId: user.id,
10941
+ workflowId: parsed.data.workflowId,
10942
+ name: parsed.data.name ?? workflowDefinition(parsed.data.workflowId).title,
10943
+ workflowInput: input,
10944
+ cadence: parsed.data.cadence,
10945
+ timezone: parsed.data.timezone,
10946
+ webhookUrl,
10947
+ nextRunAt: parsed.data.nextRunAt ?? defaultNextRunAt(parsed.data.cadence)
10948
+ });
10949
+ return c.json({ schedule }, 201);
10950
+ });
10951
+ workflowApp.get("/schedules", createApiKeyAuth(), async (c) => {
10952
+ const user = c.get("user");
10953
+ return c.json({ schedules: await listWorkflowSchedules(user.id) });
10954
+ });
10955
+ workflowApp.patch("/schedules/:id", createApiKeyAuth(), async (c) => {
10956
+ const user = c.get("user");
10957
+ const existing = await getWorkflowSchedule(c.req.param("id"), user.id);
10958
+ if (!existing) return c.json({ error: "Schedule not found" }, 404);
10959
+ const parsed = SchedulePatchSchema.safeParse(await c.req.json().catch(() => ({})));
10960
+ if (!parsed.success) return c.json({ error: parsed.error.issues[0]?.message ?? "Invalid request" }, 400);
10961
+ let nextInput;
10962
+ let webhookUrl;
10963
+ try {
10964
+ nextInput = parsed.data.input ? validateWorkflowInput(existing.workflow_id, parsed.data.input) : void 0;
10965
+ webhookUrl = parsed.data.webhookUrl === void 0 ? void 0 : await validateWebhookUrl(parsed.data.webhookUrl);
10966
+ } catch (err) {
10967
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 400);
10968
+ }
10969
+ const schedule = await patchWorkflowSchedule({
10970
+ id: existing.id,
10971
+ userId: user.id,
10972
+ name: parsed.data.name,
10973
+ status: parsed.data.status,
10974
+ workflowInput: nextInput,
10975
+ cadence: parsed.data.cadence,
10976
+ timezone: parsed.data.timezone,
10977
+ webhookUrl,
10978
+ nextRunAt: parsed.data.nextRunAt
10979
+ });
10980
+ return c.json({ schedule });
10981
+ });
10982
+ workflowApp.delete("/schedules/:id", createApiKeyAuth(), async (c) => {
10983
+ const user = c.get("user");
10984
+ const deleted = await deleteWorkflowSchedule(c.req.param("id"), user.id);
10985
+ if (!deleted) return c.json({ error: "Schedule not found" }, 404);
10986
+ return c.json({ ok: true });
10987
+ });
10988
+ workflowApp.post("/schedules/:id/run", createApiKeyAuth(), async (c) => {
10989
+ const user = c.get("user");
10990
+ const schedule = await getWorkflowSchedule(c.req.param("id"), user.id);
10991
+ if (!schedule) return c.json({ error: "Schedule not found" }, 404);
10992
+ const run = await createWorkflowRun({ userId: user.id, scheduleId: schedule.id, workflowId: schedule.workflow_id, workflowInput: schedule.input });
10993
+ try {
10994
+ const executed = await executeWorkflowRun({
10995
+ runId: run.id,
10996
+ user,
10997
+ workflowId: schedule.workflow_id,
10998
+ workflowInput: schedule.input,
10999
+ apiUrl: originFromUrl(c.req.url),
11000
+ scheduleId: schedule.id,
11001
+ webhookUrl: schedule.webhook_url
11002
+ });
11003
+ return c.json({ run: exposeRun(c, executed.run, executed.artifacts), summary: executed.summary });
11004
+ } catch (err) {
11005
+ const failed = await getWorkflowRun(run.id, user.id);
11006
+ return c.json({ run: failed, error: err instanceof Error ? err.message : String(err) }, 500);
11007
+ }
11008
+ });
11009
+ async function dispatchDueWorkflowSchedules(apiUrl, limit = 3) {
11010
+ const now = (/* @__PURE__ */ new Date()).toISOString();
11011
+ const due = await listDueWorkflowSchedules(now, limit);
11012
+ const results = [];
11013
+ for (const schedule of due) {
11014
+ const user = await getUserById(schedule.user_id);
11015
+ if (!user) {
11016
+ results.push({ scheduleId: schedule.id, status: "failed", error: "User not found" });
11017
+ continue;
11018
+ }
11019
+ const scheduledFor = schedule.next_run_at ?? now;
11020
+ const run = await createWorkflowRun({
11021
+ userId: user.id,
11022
+ scheduleId: schedule.id,
11023
+ workflowId: schedule.workflow_id,
11024
+ workflowInput: schedule.input,
11025
+ idempotencyKey: `${schedule.id}:${scheduledFor}`
11026
+ });
11027
+ await markWorkflowScheduleRan(schedule.id, now, addCadence(scheduledFor, schedule.cadence));
11028
+ if (run.status !== "queued") {
11029
+ results.push({ scheduleId: schedule.id, runId: run.id, status: run.status });
11030
+ continue;
11031
+ }
11032
+ try {
11033
+ const executed = await executeWorkflowRun({
11034
+ runId: run.id,
11035
+ user,
11036
+ workflowId: schedule.workflow_id,
11037
+ workflowInput: schedule.input,
11038
+ apiUrl,
11039
+ scheduleId: schedule.id,
11040
+ webhookUrl: schedule.webhook_url
11041
+ });
11042
+ results.push({ scheduleId: schedule.id, runId: executed.run.id, status: executed.run.status });
11043
+ } catch (err) {
11044
+ results.push({ scheduleId: schedule.id, runId: run.id, status: "failed", error: err instanceof Error ? err.message : String(err) });
11045
+ }
11046
+ }
11047
+ return { dispatched: results.length, results };
11048
+ }
11049
+ workflowApp.post("/cron/dispatch", async (c) => {
11050
+ const secret2 = c.req.header("authorization");
11051
+ if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
11052
+ return c.json({ error: "Unauthorized" }, 401);
11053
+ }
11054
+ return c.json(await dispatchDueWorkflowSchedules(originFromUrl(c.req.url)));
11055
+ });
11056
+
11057
+ // src/api/serp-intelligence-routes.ts
11058
+ import { Hono as Hono8 } from "hono";
10637
11059
 
10638
11060
  // src/serp-intelligence/page-snapshot-extractor.ts
10639
11061
  import { createHash } from "crypto";
@@ -10945,7 +11367,7 @@ async function capturePageSnapshots(targets, options = {}) {
10945
11367
  }
10946
11368
 
10947
11369
  // src/serp-intelligence/schemas.ts
10948
- import { z as z16 } from "zod";
11370
+ import { z as z17 } from "zod";
10949
11371
  var SerpIntelligenceDeviceValues = ["desktop", "mobile"];
10950
11372
  var SerpIntelligenceProxyModeValues = ["location", "configured", "none"];
10951
11373
  var SerpIntelligenceAttemptOutcomeValues = [
@@ -11007,171 +11429,171 @@ function isPublicHttpUrl(value) {
11007
11429
  return false;
11008
11430
  }
11009
11431
  }
11010
- var SerpIntelligencePublicHttpUrlSchema = z16.string().url().refine(isPublicHttpUrl, "url must be a public HTTP or HTTPS URL");
11011
- var SerpIntelligenceCaptureBodySchema = z16.object({
11012
- query: z16.string().trim().min(1, "query is required"),
11013
- location: z16.string().trim().min(1).optional(),
11014
- gl: z16.string().trim().length(2).default("us"),
11015
- hl: z16.string().trim().length(2).default("en"),
11016
- device: z16.enum(SerpIntelligenceDeviceValues).default("desktop"),
11017
- proxyMode: z16.enum(SerpIntelligenceProxyModeValues).default("location"),
11018
- proxyZip: z16.string().regex(/^\d{5}$/).optional(),
11019
- pages: z16.number().int().min(1).max(2).default(1),
11020
- debug: z16.boolean().default(false),
11021
- includePageSnapshots: z16.boolean().default(false),
11022
- pageSnapshotLimit: z16.number().int().min(0).max(10).default(0)
11432
+ var SerpIntelligencePublicHttpUrlSchema = z17.string().url().refine(isPublicHttpUrl, "url must be a public HTTP or HTTPS URL");
11433
+ var SerpIntelligenceCaptureBodySchema = z17.object({
11434
+ query: z17.string().trim().min(1, "query is required"),
11435
+ location: z17.string().trim().min(1).optional(),
11436
+ gl: z17.string().trim().length(2).default("us"),
11437
+ hl: z17.string().trim().length(2).default("en"),
11438
+ device: z17.enum(SerpIntelligenceDeviceValues).default("desktop"),
11439
+ proxyMode: z17.enum(SerpIntelligenceProxyModeValues).default("location"),
11440
+ proxyZip: z17.string().regex(/^\d{5}$/).optional(),
11441
+ pages: z17.number().int().min(1).max(2).default(1),
11442
+ debug: z17.boolean().default(false),
11443
+ includePageSnapshots: z17.boolean().default(false),
11444
+ pageSnapshotLimit: z17.number().int().min(0).max(10).default(0)
11023
11445
  }).strict();
11024
- var SerpIntelligencePageSnapshotRequestSchema = z16.object({
11446
+ var SerpIntelligencePageSnapshotRequestSchema = z17.object({
11025
11447
  url: SerpIntelligencePublicHttpUrlSchema,
11026
- sourceKind: z16.enum(SerpPageSnapshotSourceKindValues).default("configured_target"),
11027
- sourcePosition: z16.number().int().min(1).optional()
11448
+ sourceKind: z17.enum(SerpPageSnapshotSourceKindValues).default("configured_target"),
11449
+ sourcePosition: z17.number().int().min(1).optional()
11028
11450
  }).strict();
11029
- var SerpIntelligencePageSnapshotsBodySchema = z16.object({
11030
- urls: z16.array(SerpIntelligencePublicHttpUrlSchema).min(1).max(25),
11031
- targets: z16.array(SerpIntelligencePageSnapshotRequestSchema).min(1).max(25).optional(),
11032
- maxConcurrency: z16.number().int().min(1).max(5).default(2),
11033
- timeoutMs: z16.number().int().min(1e3).max(6e4).default(15e3),
11034
- debug: z16.boolean().default(false)
11451
+ var SerpIntelligencePageSnapshotsBodySchema = z17.object({
11452
+ urls: z17.array(SerpIntelligencePublicHttpUrlSchema).min(1).max(25),
11453
+ targets: z17.array(SerpIntelligencePageSnapshotRequestSchema).min(1).max(25).optional(),
11454
+ maxConcurrency: z17.number().int().min(1).max(5).default(2),
11455
+ timeoutMs: z17.number().int().min(1e3).max(6e4).default(15e3),
11456
+ debug: z17.boolean().default(false)
11035
11457
  }).strict();
11036
- var SerpIntelligenceAICitationSchema = z16.object({
11037
- text: z16.string(),
11038
- href: z16.string()
11458
+ var SerpIntelligenceAICitationSchema = z17.object({
11459
+ text: z17.string(),
11460
+ href: z17.string()
11039
11461
  }).strict();
11040
- var SerpIntelligenceOrganicResultSchema = z16.object({
11041
- position: z16.number().int().min(1),
11042
- title: z16.string(),
11043
- url: z16.string(),
11044
- domain: z16.string(),
11045
- cite: z16.string().nullable(),
11046
- snippet: z16.string().nullable(),
11047
- isRedditStyle: z16.boolean(),
11048
- inlineRating: z16.object({
11049
- value: z16.string(),
11050
- count: z16.string()
11462
+ var SerpIntelligenceOrganicResultSchema = z17.object({
11463
+ position: z17.number().int().min(1),
11464
+ title: z17.string(),
11465
+ url: z17.string(),
11466
+ domain: z17.string(),
11467
+ cite: z17.string().nullable(),
11468
+ snippet: z17.string().nullable(),
11469
+ isRedditStyle: z17.boolean(),
11470
+ inlineRating: z17.object({
11471
+ value: z17.string(),
11472
+ count: z17.string()
11051
11473
  }).strict().nullable()
11052
11474
  }).strict();
11053
- var SerpIntelligenceLocationEvidenceSchema = z16.object({
11054
- status: z16.enum(SerpIntelligenceLocalizationStatusValues),
11055
- expected: z16.object({
11056
- city: z16.string(),
11057
- regionCode: z16.string().nullable(),
11058
- canonicalLocation: z16.string()
11475
+ var SerpIntelligenceLocationEvidenceSchema = z17.object({
11476
+ status: z17.enum(SerpIntelligenceLocalizationStatusValues),
11477
+ expected: z17.object({
11478
+ city: z17.string(),
11479
+ regionCode: z17.string().nullable(),
11480
+ canonicalLocation: z17.string()
11059
11481
  }).strict().nullable(),
11060
- candidates: z16.array(z16.object({
11061
- city: z16.string(),
11062
- regionCode: z16.string(),
11063
- count: z16.number().int().min(0),
11064
- examples: z16.array(z16.string())
11482
+ candidates: z17.array(z17.object({
11483
+ city: z17.string(),
11484
+ regionCode: z17.string(),
11485
+ count: z17.number().int().min(0),
11486
+ examples: z17.array(z17.string())
11065
11487
  }).strict())
11066
11488
  }).strict();
11067
- var SerpIntelligenceHarvestResultSchema = z16.object({
11068
- seed: z16.string(),
11069
- location: z16.string().nullable(),
11070
- extractedAt: z16.string(),
11071
- totalQuestions: z16.number().int().min(0),
11072
- surface: z16.enum(["web", "aim", "unknown"]),
11073
- aiOverview: z16.object({
11074
- detected: z16.boolean(),
11075
- text: z16.string().nullable(),
11076
- citations: z16.array(SerpIntelligenceAICitationSchema),
11077
- expanded: z16.boolean().optional(),
11078
- fullyExpanded: z16.boolean().optional(),
11079
- sections: z16.array(z16.string()).optional()
11489
+ var SerpIntelligenceHarvestResultSchema = z17.object({
11490
+ seed: z17.string(),
11491
+ location: z17.string().nullable(),
11492
+ extractedAt: z17.string(),
11493
+ totalQuestions: z17.number().int().min(0),
11494
+ surface: z17.enum(["web", "aim", "unknown"]),
11495
+ aiOverview: z17.object({
11496
+ detected: z17.boolean(),
11497
+ text: z17.string().nullable(),
11498
+ citations: z17.array(SerpIntelligenceAICitationSchema),
11499
+ expanded: z17.boolean().optional(),
11500
+ fullyExpanded: z17.boolean().optional(),
11501
+ sections: z17.array(z17.string()).optional()
11080
11502
  }).strict(),
11081
- aiMode: z16.object({
11082
- detected: z16.boolean(),
11083
- text: z16.string().nullable(),
11084
- citations: z16.array(SerpIntelligenceAICitationSchema)
11503
+ aiMode: z17.object({
11504
+ detected: z17.boolean(),
11505
+ text: z17.string().nullable(),
11506
+ citations: z17.array(SerpIntelligenceAICitationSchema)
11085
11507
  }).strict(),
11086
- tree: z16.array(z16.unknown()),
11087
- flat: z16.array(z16.unknown()),
11088
- videos: z16.array(z16.unknown()),
11089
- forums: z16.array(z16.unknown()),
11090
- organicResults: z16.array(SerpIntelligenceOrganicResultSchema),
11091
- localPack: z16.array(z16.unknown()),
11092
- entityIds: z16.object({
11093
- entities: z16.array(z16.object({
11094
- name: z16.string(),
11095
- kgId: z16.string().nullable(),
11096
- cid: z16.string().nullable(),
11097
- gcid: z16.string().nullable()
11508
+ tree: z17.array(z17.unknown()),
11509
+ flat: z17.array(z17.unknown()),
11510
+ videos: z17.array(z17.unknown()),
11511
+ forums: z17.array(z17.unknown()),
11512
+ organicResults: z17.array(SerpIntelligenceOrganicResultSchema),
11513
+ localPack: z17.array(z17.unknown()),
11514
+ entityIds: z17.object({
11515
+ entities: z17.array(z17.object({
11516
+ name: z17.string(),
11517
+ kgId: z17.string().nullable(),
11518
+ cid: z17.string().nullable(),
11519
+ gcid: z17.string().nullable()
11098
11520
  }).strict()),
11099
- kgIds: z16.array(z16.string()),
11100
- cids: z16.array(z16.string()),
11101
- gcids: z16.array(z16.string())
11521
+ kgIds: z17.array(z17.string()),
11522
+ cids: z17.array(z17.string()),
11523
+ gcids: z17.array(z17.string())
11102
11524
  }).strict(),
11103
- stats: z16.object({
11104
- seed: z16.string(),
11105
- totalQuestions: z16.number().int().min(0),
11106
- maxDepthReached: z16.number().int().min(0),
11107
- durationMs: z16.number().min(0),
11108
- errorCount: z16.number().int().min(0)
11525
+ stats: z17.object({
11526
+ seed: z17.string(),
11527
+ totalQuestions: z17.number().int().min(0),
11528
+ maxDepthReached: z17.number().int().min(0),
11529
+ durationMs: z17.number().min(0),
11530
+ errorCount: z17.number().int().min(0)
11109
11531
  }).strict(),
11110
- diagnostics: z16.object({
11111
- completionStatus: z16.enum(["paa_found", "no_paa", "serp_only"]),
11112
- problem: z16.null(),
11113
- warnings: z16.array(z16.unknown()).optional(),
11114
- debug: z16.object({
11532
+ diagnostics: z17.object({
11533
+ completionStatus: z17.enum(["paa_found", "no_paa", "serp_only"]),
11534
+ problem: z17.null(),
11535
+ warnings: z17.array(z17.unknown()).optional(),
11536
+ debug: z17.object({
11115
11537
  locationEvidence: SerpIntelligenceLocationEvidenceSchema.optional()
11116
11538
  }).passthrough().optional()
11117
11539
  }).passthrough(),
11118
- whatPeopleSaying: z16.array(z16.unknown())
11540
+ whatPeopleSaying: z17.array(z17.unknown())
11119
11541
  }).strict();
11120
- var SerpIntelligenceCaptureAttemptSchema = z16.object({
11121
- attemptNumber: z16.number().int().min(1),
11122
- outcome: z16.enum(SerpIntelligenceAttemptOutcomeValues),
11123
- startedAt: z16.string().optional(),
11124
- completedAt: z16.string().optional(),
11125
- durationMs: z16.number().min(0).optional(),
11126
- problemCode: z16.string().optional(),
11127
- message: z16.string().optional(),
11128
- kernelSessionId: z16.string().nullable().optional(),
11129
- cleanupSucceeded: z16.boolean().nullable().optional()
11542
+ var SerpIntelligenceCaptureAttemptSchema = z17.object({
11543
+ attemptNumber: z17.number().int().min(1),
11544
+ outcome: z17.enum(SerpIntelligenceAttemptOutcomeValues),
11545
+ startedAt: z17.string().optional(),
11546
+ completedAt: z17.string().optional(),
11547
+ durationMs: z17.number().min(0).optional(),
11548
+ problemCode: z17.string().optional(),
11549
+ message: z17.string().optional(),
11550
+ kernelSessionId: z17.string().nullable().optional(),
11551
+ cleanupSucceeded: z17.boolean().nullable().optional()
11130
11552
  }).strict();
11131
- var SerpPageSnapshotCaptureSchema = z16.object({
11553
+ var SerpPageSnapshotCaptureSchema = z17.object({
11132
11554
  url: SerpIntelligencePublicHttpUrlSchema,
11133
11555
  requestedUrl: SerpIntelligencePublicHttpUrlSchema,
11134
11556
  finalUrl: SerpIntelligencePublicHttpUrlSchema.nullable(),
11135
- sourceKind: z16.enum(SerpPageSnapshotSourceKindValues),
11136
- sourcePosition: z16.number().int().min(1).nullable(),
11137
- status: z16.enum(SerpPageFetchStatusValues),
11138
- fetchedVia: z16.enum(SerpPageFetchedViaValues).nullable(),
11139
- httpStatus: z16.number().int().min(100).max(599).nullable(),
11140
- contentType: z16.string().nullable(),
11141
- title: z16.string().nullable(),
11557
+ sourceKind: z17.enum(SerpPageSnapshotSourceKindValues),
11558
+ sourcePosition: z17.number().int().min(1).nullable(),
11559
+ status: z17.enum(SerpPageFetchStatusValues),
11560
+ fetchedVia: z17.enum(SerpPageFetchedViaValues).nullable(),
11561
+ httpStatus: z17.number().int().min(100).max(599).nullable(),
11562
+ contentType: z17.string().nullable(),
11563
+ title: z17.string().nullable(),
11142
11564
  canonicalUrl: SerpIntelligencePublicHttpUrlSchema.nullable(),
11143
- metaDescription: z16.string().nullable(),
11144
- headings: z16.array(z16.object({
11145
- level: z16.number().int().min(1).max(6),
11146
- text: z16.string()
11565
+ metaDescription: z17.string().nullable(),
11566
+ headings: z17.array(z17.object({
11567
+ level: z17.number().int().min(1).max(6),
11568
+ text: z17.string()
11147
11569
  }).strict()).default([]),
11148
- artifact: z16.object({
11149
- htmlBlobUrl: z16.string().url().nullable(),
11150
- textBlobUrl: z16.string().url().nullable(),
11151
- markdownBlobUrl: z16.string().url().nullable(),
11152
- screenshotBlobUrl: z16.string().url().nullable(),
11153
- contentSha256: z16.string().nullable(),
11154
- capturedAt: z16.string().nullable()
11570
+ artifact: z17.object({
11571
+ htmlBlobUrl: z17.string().url().nullable(),
11572
+ textBlobUrl: z17.string().url().nullable(),
11573
+ markdownBlobUrl: z17.string().url().nullable(),
11574
+ screenshotBlobUrl: z17.string().url().nullable(),
11575
+ contentSha256: z17.string().nullable(),
11576
+ capturedAt: z17.string().nullable()
11155
11577
  }).strict(),
11156
- error: z16.object({
11157
- code: z16.string(),
11158
- message: z16.string()
11578
+ error: z17.object({
11579
+ code: z17.string(),
11580
+ message: z17.string()
11159
11581
  }).strict().nullable()
11160
11582
  }).strict();
11161
- var SerpIntelligenceCaptureResponseSchema = z16.object({
11583
+ var SerpIntelligenceCaptureResponseSchema = z17.object({
11162
11584
  harvestResult: SerpIntelligenceHarvestResultSchema,
11163
- attempts: z16.array(SerpIntelligenceCaptureAttemptSchema),
11585
+ attempts: z17.array(SerpIntelligenceCaptureAttemptSchema),
11164
11586
  locationEvidence: SerpIntelligenceLocationEvidenceSchema.nullable(),
11165
- pageSnapshotArtifacts: z16.array(SerpPageSnapshotCaptureSchema),
11166
- billing: z16.object({
11167
- creditsUsed: z16.number().min(0).optional(),
11168
- requestId: z16.string().optional(),
11169
- jobId: z16.string().optional()
11587
+ pageSnapshotArtifacts: z17.array(SerpPageSnapshotCaptureSchema),
11588
+ billing: z17.object({
11589
+ creditsUsed: z17.number().min(0).optional(),
11590
+ requestId: z17.string().optional(),
11591
+ jobId: z17.string().optional()
11170
11592
  }).strict().optional()
11171
11593
  }).strict();
11172
- var SerpIntelligencePageSnapshotsResponseSchema = z16.object({
11173
- pageSnapshotArtifacts: z16.array(SerpPageSnapshotCaptureSchema),
11174
- attempts: z16.array(SerpIntelligenceCaptureAttemptSchema).default([])
11594
+ var SerpIntelligencePageSnapshotsResponseSchema = z17.object({
11595
+ pageSnapshotArtifacts: z17.array(SerpPageSnapshotCaptureSchema),
11596
+ attempts: z17.array(SerpIntelligenceCaptureAttemptSchema).default([])
11175
11597
  }).strict();
11176
11598
 
11177
11599
  // src/serp-intelligence/serp-capture-service.ts
@@ -11343,7 +11765,7 @@ var SERP_INTELLIGENCE_RATE_LIMIT = 60;
11343
11765
  var SERP_INTELLIGENCE_RATE_WINDOW_SECONDS = 60;
11344
11766
  var POST_CAPTURE_ROUTE_LABEL = "POST /capture";
11345
11767
  var POST_PAGE_SNAPSHOTS_ROUTE_LABEL = "POST /page-snapshots";
11346
- var serpIntelligenceApp = new Hono7();
11768
+ var serpIntelligenceApp = new Hono8();
11347
11769
  serpIntelligenceApp.use("*", createApiKeyAuth());
11348
11770
  function structuredError(input) {
11349
11771
  return {
@@ -11518,7 +11940,7 @@ serpIntelligenceApp.post("/page-snapshots", async (c) => {
11518
11940
  });
11519
11941
 
11520
11942
  // src/mcp/mcp-routes.ts
11521
- import { Hono as Hono8 } from "hono";
11943
+ import { Hono as Hono9 } from "hono";
11522
11944
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
11523
11945
  configureReportSaving(false);
11524
11946
  function mcpAuthError() {
@@ -11548,11 +11970,11 @@ async function requireMcpCallerKey(c) {
11548
11970
  if (!user) return mcpAuthError();
11549
11971
  return callerKey;
11550
11972
  }
11551
- var mcpApp = new Hono8();
11973
+ var mcpApp = new Hono9();
11552
11974
  function registerSerpIntelligenceCaptureTools(server, executor) {
11553
11975
  server.registerTool("capture_serp_snapshot", {
11554
11976
  title: "SERP Intelligence Snapshot",
11555
- description: "Capture a structured SERP Intelligence Google snapshot through POST /serp-intelligence/capture, the same product capture path used by Phoenix. Split query from location, infer gl/hl, use proxyMode location for localized US residential evidence; location mode creates fresh proxy IDs across retries and rejects wrong-location evidence before returning. Use configured only for the static residential proxy, and none only for direct-network debugging. Set debug true when investigating location evidence, proxy behavior, CAPTCHA, or capture reliability.",
11977
+ description: "Capture a structured SERP Intelligence Google snapshot through POST /serp-intelligence/capture, the same product capture path used by Phoenix. Split query from location, infer gl/hl, use proxyMode location for localized US residential evidence; location mode creates fresh proxy IDs across retries, briefly waits for browser-service automatic CAPTCHA/challenge solving when Google blocks a page, and rotates to a new proxy/session if the challenge does not clear or location evidence is wrong. Use configured only for the static residential proxy, and none only for direct-network debugging. Set debug true when investigating location evidence, proxy behavior, CAPTCHA, or capture reliability.",
11556
11978
  inputSchema: CaptureSerpSnapshotInputSchema,
11557
11979
  annotations: liveWebToolAnnotations("SERP Intelligence Snapshot")
11558
11980
  }, async (input) => executor.captureSerpSnapshot(input));
@@ -11584,7 +12006,7 @@ mcpApp.all("/", async (c) => {
11584
12006
  });
11585
12007
 
11586
12008
  // src/api/browser-agent-routes.ts
11587
- import { Hono as Hono9 } from "hono";
12009
+ import { Hono as Hono10 } from "hono";
11588
12010
 
11589
12011
  // src/api/browser-agent-db.ts
11590
12012
  import { randomUUID } from "crypto";
@@ -12105,7 +12527,7 @@ async function loadOpenSession(id, userId) {
12105
12527
  return row;
12106
12528
  }
12107
12529
  function buildBrowserAgentRoutes() {
12108
- const app2 = new Hono9();
12530
+ const app2 = new Hono10();
12109
12531
  app2.use("*", async (c, next) => {
12110
12532
  await migrateBrowserAgent();
12111
12533
  return next();
@@ -12620,9 +13042,9 @@ if (state.key) { refreshSessions(); if (state.current) selectSession(state.curre
12620
13042
 
12621
13043
  // src/api/stripe-routes.ts
12622
13044
  import Stripe from "stripe";
12623
- import { Hono as Hono10 } from "hono";
13045
+ import { Hono as Hono11 } from "hono";
12624
13046
  var stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
12625
- var stripeApp = new Hono10();
13047
+ var stripeApp = new Hono11();
12626
13048
  stripeApp.post("/webhooks", async (c) => {
12627
13049
  const sig = c.req.header("stripe-signature");
12628
13050
  const body = await c.req.text();
@@ -12717,27 +13139,27 @@ import { getCookie, setCookie, deleteCookie } from "hono/cookie";
12717
13139
  import Stripe2 from "stripe";
12718
13140
 
12719
13141
  // src/api/billing-schemas.ts
12720
- import { z as z17 } from "zod";
12721
- var BillingCheckoutBodySchema = z17.object({
12722
- priceId: z17.string().min(1)
12723
- });
12724
- var FreeCreditBreakdownSchema = z17.object({
12725
- signup_grant_mc: z17.number().int().nonnegative(),
12726
- monthly_refresh_mc: z17.number().int().nonnegative(),
12727
- total_free_mc: z17.number().int().nonnegative(),
12728
- signup_grant_credits: z17.number().nonnegative(),
12729
- monthly_refresh_credits: z17.number().nonnegative(),
12730
- total_free_credits: z17.number().nonnegative()
12731
- });
12732
- var BillingBalanceResponseSchema = z17.object({
12733
- balance_mc: z17.number().int().nonnegative(),
12734
- balance_credits: z17.number().nonnegative(),
13142
+ import { z as z18 } from "zod";
13143
+ var BillingCheckoutBodySchema = z18.object({
13144
+ priceId: z18.string().min(1)
13145
+ });
13146
+ var FreeCreditBreakdownSchema = z18.object({
13147
+ signup_grant_mc: z18.number().int().nonnegative(),
13148
+ monthly_refresh_mc: z18.number().int().nonnegative(),
13149
+ total_free_mc: z18.number().int().nonnegative(),
13150
+ signup_grant_credits: z18.number().nonnegative(),
13151
+ monthly_refresh_credits: z18.number().nonnegative(),
13152
+ total_free_credits: z18.number().nonnegative()
13153
+ });
13154
+ var BillingBalanceResponseSchema = z18.object({
13155
+ balance_mc: z18.number().int().nonnegative(),
13156
+ balance_credits: z18.number().nonnegative(),
12735
13157
  free_credits: FreeCreditBreakdownSchema,
12736
- ledger: z17.array(z17.any())
13158
+ ledger: z18.array(z18.any())
12737
13159
  });
12738
- var MonthlyRefreshSweepResultSchema = z17.object({
12739
- usersRefreshed: z17.number().int().nonnegative(),
12740
- totalMcGranted: z17.number().int().nonnegative()
13160
+ var MonthlyRefreshSweepResultSchema = z18.object({
13161
+ usersRefreshed: z18.number().int().nonnegative(),
13162
+ totalMcGranted: z18.number().int().nonnegative()
12741
13163
  });
12742
13164
 
12743
13165
  // src/api/credit-operations.ts
@@ -12838,7 +13260,7 @@ async function getFreeCreditBreakdown(userId) {
12838
13260
  }
12839
13261
 
12840
13262
  // src/api/session.ts
12841
- import { createHmac, timingSafeEqual } from "crypto";
13263
+ import { createHmac as createHmac2, timingSafeEqual } from "crypto";
12842
13264
  var isProduction = () => process.env.NODE_ENV === "production" || process.env.VERCEL === "1";
12843
13265
  function getSessionSecret() {
12844
13266
  const configured = process.env.SESSION_SECRET?.trim();
@@ -12857,7 +13279,7 @@ function safeEqualHex(a, b) {
12857
13279
  }
12858
13280
  function signSession(userId) {
12859
13281
  const payload = String(userId);
12860
- const sig = createHmac("sha256", secret()).update(payload).digest("hex");
13282
+ const sig = createHmac2("sha256", secret()).update(payload).digest("hex");
12861
13283
  return `${payload}.${sig}`;
12862
13284
  }
12863
13285
  function verifySession(token) {
@@ -12865,7 +13287,7 @@ function verifySession(token) {
12865
13287
  if (dot === -1) return null;
12866
13288
  const payload = token.slice(0, dot);
12867
13289
  const sig = token.slice(dot + 1);
12868
- const expected = createHmac("sha256", secret()).update(payload).digest("hex");
13290
+ const expected = createHmac2("sha256", secret()).update(payload).digest("hex");
12869
13291
  if (!safeEqualHex(sig, expected)) return null;
12870
13292
  const id = parseInt(payload);
12871
13293
  return isNaN(id) ? null : id;
@@ -12948,7 +13370,7 @@ var sessionAuth = createMiddleware3(async (c, next) => {
12948
13370
  c.set("sessionUser", { ...refreshed, balance_mc: balanceMc });
12949
13371
  return next();
12950
13372
  });
12951
- var app = new Hono11();
13373
+ var app = new Hono12();
12952
13374
  var STRIPE_API_VERSION = "2026-02-25.clover";
12953
13375
  function requireStripeSecret() {
12954
13376
  const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
@@ -13594,13 +14016,14 @@ app.get("/cron/tick", async (c) => {
13594
14016
  if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
13595
14017
  return c.json({ error: "Unauthorized" }, 401);
13596
14018
  }
13597
- const { drainQueue } = await import("./worker-NAKGTIF5.js");
14019
+ const { drainQueue } = await import("./worker-TDJQ6TH3.js");
13598
14020
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
14021
+ const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
13599
14022
  const [results, sweepResult] = await Promise.all([
13600
14023
  drainQueue(budget),
13601
14024
  runMonthlyRefreshSweep()
13602
14025
  ]);
13603
- return c.json({ drained: results.length, results, sweepResult });
14026
+ return c.json({ drained: results.length, results, sweepResult, workflowDispatch: workflowDispatchResult });
13604
14027
  });
13605
14028
  app.on(["GET", "POST", "PUT"], "/api/inngest", serveInngest({ client: inngest, functions: [siteAuditFn] }));
13606
14029
  app.route("/api/internal/site-architecture-auditor", siteAuditApp);
@@ -13609,6 +14032,7 @@ app.route("/screenshot", screenshotApp);
13609
14032
  app.route("/facebook", facebookAdApp);
13610
14033
  app.route("/maps", mapsApp);
13611
14034
  app.route("/directory", directoryApp);
14035
+ app.route("/workflows", workflowApp);
13612
14036
  app.route("/serp-intelligence", serpIntelligenceApp);
13613
14037
  app.route("/mcp", mcpApp);
13614
14038
  app.route("/agent", buildBrowserAgentRoutes());
@@ -13720,4 +14144,4 @@ app.get("/blog/:slug/", (c) => {
13720
14144
  export {
13721
14145
  app
13722
14146
  };
13723
- //# sourceMappingURL=server-RIKRBDOI.js.map
14147
+ //# sourceMappingURL=server-BKJ7LULQ.js.map