-
Notifications
You must be signed in to change notification settings - Fork 38
feat(api): AllTrails OG preview endpoint and MCP tool #2310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1743028
a702b3a
953a903
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,84 @@ | ||||||||||||||||
| import { Elysia, status } from 'elysia'; | ||||||||||||||||
| import { z } from 'zod'; | ||||||||||||||||
|
|
||||||||||||||||
| const ALLTRAILS_HOSTNAME_RE = /^(?:[a-z0-9-]+\.)?alltrails\.com$/; | ||||||||||||||||
| const UA = 'Mozilla/5.0 (compatible; PackRat/1.0; +https://packrat.world)'; | ||||||||||||||||
|
|
||||||||||||||||
| function extractOgTag(html: string, property: string): string | null { | ||||||||||||||||
| const match = | ||||||||||||||||
| html.match( | ||||||||||||||||
| new RegExp(`<meta[^>]+property=["']${property}["'][^>]+content=["']([^"']+)["']`, 'i'), | ||||||||||||||||
| ) ?? | ||||||||||||||||
| html.match( | ||||||||||||||||
| new RegExp(`<meta[^>]+content=["']([^"']+)["'][^>]+property=["']${property}["']`, 'i'), | ||||||||||||||||
| ); | ||||||||||||||||
| return match?.[1] ?? null; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| export const alltrailsRoutes = new Elysia({ prefix: '/alltrails' }).post( | ||||||||||||||||
| '/preview', | ||||||||||||||||
| async ({ body }) => { | ||||||||||||||||
| const { url } = body; | ||||||||||||||||
|
|
||||||||||||||||
|
Comment on lines
+18
to
+22
|
||||||||||||||||
| let parsed: URL; | ||||||||||||||||
| try { | ||||||||||||||||
| parsed = new URL(url); | ||||||||||||||||
| } catch { | ||||||||||||||||
| return status(400, { error: 'Invalid URL' }); | ||||||||||||||||
| } | ||||||||||||||||
|
Comment on lines
+23
to
+28
|
||||||||||||||||
| let parsed: URL; | |
| try { | |
| parsed = new URL(url); | |
| } catch { | |
| return status(400, { error: 'Invalid URL' }); | |
| } | |
| const parsed = new URL(url); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Error message understates what's accepted.
The host regex permits any single-level subdomain (www., es., …) but the message implies only the bare apex. A user posting https://es.alltrails.com/... that fails parsing will see a misleading error. Minor wording fix:
- return status(400, { error: 'URL must be an https://alltrails.com URL' });
+ return status(400, { error: 'URL must be an https://(*.)alltrails.com URL' });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (parsed.protocol !== 'https:' || !ALLTRAILS_HOSTNAME_RE.test(parsed.hostname)) { | |
| return status(400, { error: 'URL must be an https://alltrails.com URL' }); | |
| } | |
| if (parsed.protocol !== 'https:' || !ALLTRAILS_HOSTNAME_RE.test(parsed.hostname)) { | |
| return status(400, { error: 'URL must be an https://(*.)alltrails.com URL' }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/api/src/routes/alltrails.ts` around lines 30 - 32, Update the 400
error message in the alltrails route to accurately reflect allowed hosts: when
checking parsed.protocol and ALLTRAILS_HOSTNAME_RE, change the returned message
from "URL must be an https://alltrails.com URL" to something that mentions https
and allows alltrails.com and its single-level subdomains (e.g., "URL must be an
https://alltrails.com or subdomain URL"); locate the check that uses parsed and
ALLTRAILS_HOSTNAME_RE and update the status(400, { error: ... }) string
accordingly.
Copilot
AI
Apr 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The redirect check happens after fetch() completes, but fetch() will follow redirects by default. That means a crafted alltrails.com URL could still cause the worker to make an outbound request to a non-AllTrails host before you return the 400, which defeats the intent of the redirect guard. Consider using redirect: 'manual' and explicitly validating/following 3xx Location targets only when they remain within *.alltrails.com (or disable redirects entirely).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
Does Elysia 1.4 support Zod schemas directly for the body validator via Standard Schema, or is a plugin/adapter required?
💡 Result:
Yes, Elysia 1.4 supports Zod schemas directly for the body validator via Standard Schema out of the box. No plugin or adapter is required.
Citations:
- 1: https://elysiajs.com/blog/elysia-14.html
- 2: https://elysiajs.com/blog/elysia-14
- 3: https://elysiajs.com/essential/validation
🏁 Script executed:
head -100 packages/api/src/routes/alltrails.tsRepository: PackRat-AI/PackRat
Length of output: 2588
Use OpenAPIHono with createRoute per API guidelines instead of Elysia. Elysia 1.4 validates Zod schemas directly via Standard Schema out of the box, so the original concern about unvalidated bodies is not applicable—the schema will be honored. However, this file violates the mandatory requirement that API routes in packages/api/src/**/*.ts must use OpenAPIHono with createRoute and Zod schemas for type-safe, documented endpoints.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/api/src/routes/alltrails.ts` around lines 73 - 83, This route
currently uses Elysia-style route metadata and must be rewritten to use
OpenAPIHono's createRoute with the existing Zod body schema; replace the current
Elysia route definition in alltrails.ts with a createRoute call that declares
method (POST), path, and the Zod body schema (z.object({ url: z.string().url()
})) as the request schema, move the tags/summary/description into the OpenAPI
metadata for createRoute, and ensure the handler function signature matches
OpenAPIHono expectations and returns the same OG preview response; import
createRoute from OpenAPIHono and keep the Zod schema identifier so type-safe
validation and generated docs work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ALLTRAILS_HOSTNAME_REonly allows at most one subdomain level ((?:...\.)?). That rejects valid multi-level subdomains likefoo.bar.alltrails.comeven though the PR description says subdomains are supported. Consider changing this to allow any number of subdomain labels (e.g.(?:[a-z0-9-]+\.)*alltrails\.com).