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
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
- package-ecosystem: "yarn" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
136 changes: 89 additions & 47 deletions app/api/deploy/route.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,71 @@
import {NextResponse} from 'next/server'
import {gql, GraphQLClient} from 'graphql-request'
import { NextResponse } from 'next/server'
import { gql, GraphQLClient } from 'graphql-request'

export async function POST(req: Request) {
const { beeperToken, flyToken, bridge, region, appName, redeploy } = await req.json()

// If redeploy flag is passed, update existing machine to latest image
if (redeploy) {
const res_list = await fetch(`https://api.machines.dev/v1/apps/${appName}/machines`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${flyToken}`,
'Content-Type': 'application/json',
}
})

if (res_list.status !== 200) {
const list_data = await res_list.json()
return NextResponse.json({ error: JSON.stringify(list_data) }, { status: 500 })
}

const machines = await res_list.json()
if (!machines || machines.length === 0) {
return NextResponse.json({ error: `No machines found for app ${appName}` }, { status: 404 })
}

const machine = machines[0]

const update_res = await fetch(`https://api.machines.dev/v1/apps/${appName}/machines/${machine.id}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${flyToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
config: {
...machine.config,
image: "ghcr.io/beeper/bridge-manager"
}
})
})

if (update_res.status !== 200) {
const update_data = await update_res.json()
return NextResponse.json({ error: JSON.stringify(update_data) }, { status: 500 })
}

return NextResponse.json({ success: true })
}
Comment on lines +7 to +49
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Redeploy logic needs improvements for robustness.

The redeploy implementation has several concerns:

  1. Line 27: Always uses the first machine without validation - if multiple machines exist, is this intentional?

  2. Line 38: The hardcoded image "ghcr.io/beeper/bridge-manager" has no version tag:

    • Docker may use cached images instead of pulling the latest
    • Consider using a specific tag or :latest explicitly
    • No guarantee the image will actually update
  3. No deployment verification: Unlike the main deploy flow (lines 182-200), the redeploy returns immediately without confirming the machine restarted or is running the new image

  4. Line 37: Spreading ...machine.config could be risky if the config schema has changed between bridge-manager versions

Consider these improvements:

 // If redeploy flag is passed, update existing machine to latest image
 if (redeploy) {
     const res_list = await fetch(`https://api.machines.dev/v1/apps/${appName}/machines`, {
         method: 'GET',
         headers: {
             'Authorization': `Bearer ${flyToken}`,
             'Content-Type': 'application/json',
         }
     })

     if (res_list.status !== 200) {
         const list_data = await res_list.json()
         return NextResponse.json({ error: JSON.stringify(list_data) }, { status: 500 })
     }

     const machines = await res_list.json()
     if (!machines || machines.length === 0) {
         return NextResponse.json({ error: `No machines found for app ${appName}` }, { status: 404 })
     }

     const machine = machines[0]

     const update_res = await fetch(`https://api.machines.dev/v1/apps/${appName}/machines/${machine.id}`, {
         method: 'POST',
         headers: {
             'Authorization': `Bearer ${flyToken}`,
             'Content-Type': 'application/json',
         },
         body: JSON.stringify({
             config: {
                 ...machine.config,
-                image: "ghcr.io/beeper/bridge-manager"
+                image: "ghcr.io/beeper/bridge-manager:latest"
             }
         })
     })

     if (update_res.status !== 200) {
         const update_data = await update_res.json()
         return NextResponse.json({ error: JSON.stringify(update_data) }, { status: 500 })
     }
+
+    // TODO: Consider adding verification that the machine restarted successfully

     return NextResponse.json({ success: true })
 }
📝 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.

Suggested change
// If redeploy flag is passed, update existing machine to latest image
if (redeploy) {
const res_list = await fetch(`https://api.machines.dev/v1/apps/${appName}/machines`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${flyToken}`,
'Content-Type': 'application/json',
}
})
if (res_list.status !== 200) {
const list_data = await res_list.json()
return NextResponse.json({ error: JSON.stringify(list_data) }, { status: 500 })
}
const machines = await res_list.json()
if (!machines || machines.length === 0) {
return NextResponse.json({ error: `No machines found for app ${appName}` }, { status: 404 })
}
const machine = machines[0]
const update_res = await fetch(`https://api.machines.dev/v1/apps/${appName}/machines/${machine.id}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${flyToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
config: {
...machine.config,
image: "ghcr.io/beeper/bridge-manager"
}
})
})
if (update_res.status !== 200) {
const update_data = await update_res.json()
return NextResponse.json({ error: JSON.stringify(update_data) }, { status: 500 })
}
return NextResponse.json({ success: true })
}
// If redeploy flag is passed, update existing machine to latest image
if (redeploy) {
const res_list = await fetch(`https://api.machines.dev/v1/apps/${appName}/machines`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${flyToken}`,
'Content-Type': 'application/json',
}
})
if (res_list.status !== 200) {
const list_data = await res_list.json()
return NextResponse.json({ error: JSON.stringify(list_data) }, { status: 500 })
}
const machines = await res_list.json()
if (!machines || machines.length === 0) {
return NextResponse.json({ error: `No machines found for app ${appName}` }, { status: 404 })
}
const machine = machines[0]
const update_res = await fetch(`https://api.machines.dev/v1/apps/${appName}/machines/${machine.id}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${flyToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
config: {
...machine.config,
image: "ghcr.io/beeper/bridge-manager:latest"
}
})
})
if (update_res.status !== 200) {
const update_data = await update_res.json()
return NextResponse.json({ error: JSON.stringify(update_data) }, { status: 500 })
}
// TODO: Consider adding verification that the machine restarted successfully
return NextResponse.json({ success: true })
}
🤖 Prompt for AI Agents
In app/api/deploy/route.ts around lines 7 to 49, the redeploy branch is fragile:
it unconditionally picks the first machine, injects a hardcoded untagged image,
blindly spreads machine.config, and returns before verifying the update. Fix by
selecting the correct machine (validate if multiple exist and choose by
role/name or surface an error if ambiguous), require or append an explicit image
tag (or accept a tag param) instead of a bare registry string, build the new
config by copying only known-safe fields rather than blind ...machine.config
spreading, perform the machine update call and then poll the machine status
endpoint (with a short retry/backoff and a timeout) to verify the machine
restarted and is running the new image, and propagate detailed error responses
if selection, update, or verification fails.


const {beeperToken, flyToken, bridge, region} = await req.json()
const app_name = `sh-${bridge}-${Date.now()}`

// Create the app

const res_create_app = await fetch('https://api.machines.dev/v1/apps', {
method: 'POST',
headers: {
'Authorization': `Bearer ${flyToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({app_name: app_name, org_slug: 'personal'})
body: JSON.stringify({ app_name: app_name, org_slug: 'personal' })
})

if (res_create_app.status != 201) {
const create_app_data = await res_create_app.json();
const create_app_data = await res_create_app.json()
return NextResponse.json({ error: JSON.stringify(create_app_data) }, { status: 500 })
}

// Allocate shared IPv4

const graphQLClient = new GraphQLClient('https://api.fly.io/graphql', {
headers: {
authorization: `Bearer ${flyToken}`,
Expand All @@ -47,40 +89,41 @@ export async function POST(req: Request) {
"region": region
}
}

const ip_request_data: any = await graphQLClient.request(ip_query, ip_variables)

if (!ip_request_data.allocateIpAddress?.app?.sharedIpAddress) {
return NextResponse.json({ error: JSON.stringify(ip_request_data) }, { status: 500 })
}

// Set secrets

const secrets_query = gql`
mutation($input: SetSecretsInput!) {
setSecrets(input: $input) {
release {
id
version
reason
description
user {
setSecrets(input: $input) {
release {
id
email
name
version
reason
description
user {
id
email
name
}
evaluationId
createdAt
}
evaluationId
createdAt
}
}
}`
`

const secrets_variables = {
"input": {
"appId": app_name,
"secrets": [
{"key": "MATRIX_ACCESS_TOKEN", "value": beeperToken},
{"key": "BRIDGE_NAME", "value": app_name},
{"key": "DB_DIR", "value": "/data"}
{ "key": "MATRIX_ACCESS_TOKEN", "value": beeperToken },
{ "key": "BRIDGE_NAME", "value": app_name },
{ "key": "DB_DIR", "value": "/data" }
]
}
}
Expand All @@ -92,7 +135,6 @@ export async function POST(req: Request) {
}

// Create machine

const res_create_machine = await fetch(`https://api.machines.dev/v1/apps/${app_name}/machines`, {
method: 'POST',
headers: {
Expand All @@ -108,23 +150,23 @@ export async function POST(req: Request) {
},
"services": [
{
"ports": [
{
"port": 443,
"handlers": [
"tls",
"http"
]
},
{
"port": 80,
"handlers": [
"http"
]
}
],
"protocol": "tcp",
"internal_port": 8080
"ports": [
{
"port": 443,
"handlers": [
"tls",
"http"
]
},
{
"port": 80,
"handlers": [
"http"
]
}
],
"protocol": "tcp",
"internal_port": 8080
}
]
}
Expand All @@ -147,15 +189,15 @@ export async function POST(req: Request) {
})

if (beeper_whoami.status != 200) {
const beeper_bridge_data = await beeper_whoami.json();
const beeper_bridge_data = await beeper_whoami.json()
return NextResponse.json({ error: JSON.stringify(beeper_bridge_data) }, { status: 500 })
}

const beeper_bridge_response = await beeper_whoami.json();
beeper_bridges = Object.keys(beeper_bridge_response.user.bridges);
const beeper_bridge_response = await beeper_whoami.json()
beeper_bridges = Object.keys(beeper_bridge_response.user.bridges)

await new Promise(r => setTimeout(r, 1000));
await new Promise(r => setTimeout(r, 1000))
}

return NextResponse.json({"appName": app_name})
}
return NextResponse.json({ "appName": app_name })
}
47 changes: 30 additions & 17 deletions app/components/BeeperLogin.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {useState} from "react";
import { useState } from "react";

export default function BeeperLogin({ setBeeperToken }: any) {

const [sentCode, setSentCode] = useState(false)
const [loginIdentifier, setLoginIdentifier] = useState("")
const [sentCode, setSentCode] = useState(false);
const [loginIdentifier, setLoginIdentifier] = useState("");

async function sendLoginEmail(event: any) {
event.preventDefault();
Expand All @@ -16,25 +15,26 @@ export default function BeeperLogin({ setBeeperToken }: any) {
Authorization: "Bearer BEEPER-PRIVATE-API-PLEASE-DONT-USE",
},
});
const {request} = await loginResponse.json();
const { request } = await loginResponse.json();

await fetch("https://api.beeper.com/user/login/email", {
method: "POST",
headers: {
Authorization: "Bearer BEEPER-PRIVATE-API-PLEASE-DONT-USE",
"Content-Type": "application/json",
},
body: JSON.stringify({request, email}),
body: JSON.stringify({ request, email }),
});

setSentCode(true);
setLoginIdentifier(request);
}

async function getToken(event: any) {
event.preventDefault()
event.preventDefault();

const code = event.target[0].value;
let code = event.target[0].value;
code = code.replace(/\s+/g, ""); // strip all whitespace

const loginChallengeResponse = await fetch(
"https://api.beeper.com/user/login/response",
Expand Down Expand Up @@ -62,34 +62,47 @@ export default function BeeperLogin({ setBeeperToken }: any) {
token: token
})
}
)
);

const { access_token } = await accessTokenResponse.json();

setBeeperToken(access_token);
window.localStorage.setItem("beeperToken", access_token)
window.localStorage.setItem("beeperToken", access_token);
}

return (
<div className="m-20">
<p className="text-center text-4xl font-bold">Sign in to Beeper</p>
<p className="text-center mt-5">This will be used to connect your self-hosted bridge to your Beeper account. Your credentials will be passed directly to Fly.</p>
{ sentCode ? (
<p className="text-center mt-5">
This will be used to connect your self-hosted bridge to your Beeper account. Your credentials will be passed directly to Fly.
</p>

{sentCode ? (
<div className="mx-auto w-72 mt-16">
<p>{"We've emailed you a login code."}</p>
<form className="mt-2" onSubmit={getToken}>
<p>Enter it here:</p>
<input className="p-2 border-2 rounded-md w-full" name="code" type="number" />
<input
className="p-2 border-2 rounded-md w-full"
name="code"
type="text"
inputMode="numeric"
placeholder="123 456"
/>
</form>
</div>
) : (
) : (
<div className="mx-auto w-72 mt-16">
<form onSubmit={sendLoginEmail}>
<p>Email:</p>
<input className="p-2 border-2 rounded-md w-full" name="email" type="email" />
<input
className="p-2 border-2 rounded-md w-full"
name="email"
type="email"
/>
</form>
</div>
)}
</div>
)
}
);
}
7 changes: 4 additions & 3 deletions app/components/BridgeDeploy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,12 @@ export default function BridgeDeploy({beeperToken, flyToken, onCreate}: any) {
const bridges: Record<string, string> = {
whatsapp: "WhatsApp",
gmessages: "Google Messages",
instagram: "Instagram",
instagramgo: "Instagram",
facebookgo: "Facebook",
signal: "Signal",
discord: "Discord",
slack: "Slack",
telegram: "Telegram",
twitter: "Twitter"
}


Expand Down Expand Up @@ -100,4 +101,4 @@ export default function BridgeDeploy({beeperToken, flyToken, onCreate}: any) {
</div>
)

}
}
Loading