Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
frontend:
name: Frontend (typecheck + build + test)
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: frontend/package-lock.json

- name: Install dependencies
run: npm ci

- name: Type check
run: npx tsc --noEmit

- name: Build
run: npm run build

- name: Test
run: npm test

rust:
name: Rust (check + clippy + test)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@stable
with:
components: clippy

- uses: Swatinem/rust-cache@v2

- name: Check
run: cargo check

- name: Clippy
run: cargo clippy -- -D warnings

- name: Test
run: cargo test

worker:
name: Worker (typecheck)
runs-on: ubuntu-latest
defaults:
run:
working-directory: alerts
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: alerts/package-lock.json

- name: Install dependencies
run: npm ci

- name: Type check
run: npx tsc --noEmit

lint:
name: Lint + Format (Biome)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20

- name: Install Biome
run: npm install --global @biomejs/biome

- name: Lint
run: biome lint --no-errors-on-unmatched frontend/src alerts/src

- name: Format check
run: biome format --no-errors-on-unmatched frontend/src alerts/src
13 changes: 9 additions & 4 deletions alerts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@
"deploy": "wrangler deploy",
"build": "wrangler deploy --dry-run",
"db:create": "wrangler d1 create turbolong-alerts",
"db:migrate": "wrangler d1 execute turbolong-alerts --file=src/schema.sql",
"lint": "biome lint .",
"format": "biome format . --write",
"format:check": "biome format ."
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"wrangler": "^3.99.0",
"typescript": "^5.7.3"
"db:migrate": "wrangler d1 execute turbolong-alerts --local --file=src/schema.sql",
"db:migrate:remote": "wrangler d1 execute turbolong-alerts --remote --file=src/schema.sql",
"vapid:generate": "npx @pushforge/builder vapid",
"setup:remote": "powershell -ExecutionPolicy Bypass -File scripts/deploy-remote.ps1"
},
"devDependencies": {
"typescript": "^5.7.3",
"wrangler": "^3.99.0"
},
"dependencies": {
"@pushforge/builder": "^2.0.5"
}
Expand Down
13 changes: 12 additions & 1 deletion alerts/src/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ interface SendResult {
error?: string;
}

async function sendEmail(env: Env, to: string, subject: string, html: string): Promise<SendResult> {
async function sendEmail(
env: Env,
to: string,
subject: string,
html: string,
extraHeaders?: Record<string, string>,
): Promise<SendResult> {
const res = await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
Expand All @@ -24,6 +30,7 @@ async function sendEmail(env: Env, to: string, subject: string, html: string): P
to: [to],
subject,
html,
...(extraHeaders ? { headers: extraHeaders } : {}),
}),
});

Expand Down Expand Up @@ -98,5 +105,9 @@ export async function sendApyAlert(
to,
`\u26A0 Negative APY: ${assetSymbol} at ${leverage}x on ${poolName}`,
html,
{
"List-Unsubscribe": `<${unsubscribeUrl}>`,
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
},
);
}
15 changes: 14 additions & 1 deletion alerts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ interface Env {
RESEND_API_KEY: string;
RESEND_FROM: string;
FRONTEND_ORIGIN: string;
/** Optional Bearer token required to access /metrics. If unset, endpoint is public. */
METRICS_TOKEN?: string;
VAPID_PUBLIC_KEY: string;
VAPID_PRIVATE_KEY: string;
VAPID_SUBJECT?: string;
Expand Down Expand Up @@ -175,12 +177,20 @@ async function handleUnsubscribe(request: Request, env: Env): Promise<Response>
const url = new URL(request.url);
const token = url.searchParams.get("token");

if (!token) return htmlResponse("<h2>Missing token.</h2>", 400);
if (!token) {
if (request.method === "POST") return new Response("Missing token", { status: 400 });
return htmlResponse("<h2>Missing token.</h2>", 400);
}

const result = await env.DB.prepare(
"DELETE FROM subscriptions WHERE unsub_token = ?1"
).bind(token).run();

// RFC 8058 one-click: mail client POSTs List-Unsubscribe=One-Click; respond 200 with no body
if (request.method === "POST") {
return new Response(null, { status: result.meta.changes ? 200 : 404 });
}

if (!result.meta.changes) {
return htmlResponse("<h2>Subscription not found or already removed.</h2>", 404);
}
Expand Down Expand Up @@ -432,6 +442,9 @@ export default {
return handleVerify(request, env);

case "/unsubscribe":
if (request.method !== "GET" && request.method !== "POST") {
return jsonResponse({ error: "Method not allowed" }, 405, env);
}
return handleUnsubscribe(request, env);

case "/vapid-public-key":
Expand Down
39 changes: 39 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"organizeImports": {
"enabled": false
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny": "off"
},
"style": {
"noParameterAssign": "off"
}
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"trailingCommas": "all",
"semicolons": "always"
}
},
"files": {
"ignore": [
"**/node_modules",
"**/dist",
"**/.wrangler",
"**/target"
]
}
}
Loading