Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8ff3ff4
Update BridgeDeploy.tsx
kubo6472 Nov 5, 2024
cb4a488
force yarn v1 with corepack
kubo6472 Nov 5, 2024
859c228
Create dependabot.yml
kubo6472 Nov 5, 2024
d7791fe
Bump the npm_and_yarn group across 1 directory with 5 updates
dependabot[bot] Nov 5, 2024
7122746
Merge pull request #1 from kubo6472/dependabot/npm_and_yarn/npm_and_y…
kubo6472 Nov 5, 2024
5f1b3fd
Bump nanoid in the npm_and_yarn group across 1 directory
dependabot[bot] Dec 14, 2024
938843c
Update Welcome.tsx
kubo6472 Dec 15, 2024
da278c6
Merge pull request #2 from kubo6472/dependabot/npm_and_yarn/npm_and_y…
kubo6472 Feb 16, 2025
6ea8142
Bump the npm_and_yarn group across 1 directory with 2 updates
dependabot[bot] Feb 16, 2025
519d041
Merge pull request #3 from kubo6472/dependabot/npm_and_yarn/npm_and_y…
kubo6472 Feb 20, 2025
c3022e0
Update BeeperLogin.tsx
kubo6472 Jun 23, 2025
6a5051a
Update FlyLogin.tsx
kubo6472 Jun 23, 2025
ffa48d2
Update FlyLogin.tsx
kubo6472 Jun 23, 2025
31c4074
Update route.ts
kubo6472 Jun 24, 2025
d3c7a1c
Update BridgeInstance.tsx
kubo6472 Jun 24, 2025
3163d32
Bump next in the npm_and_yarn group across 1 directory
dependabot[bot] Dec 6, 2025
d493906
Merge pull request #4 from kubo6472/dependabot/npm_and_yarn/npm_and_y…
kubo6472 Dec 6, 2025
0d8e084
Bump the npm_and_yarn group across 1 directory with 3 updates
dependabot[bot] Dec 6, 2025
afc23b4
Merge pull request #5 from kubo6472/dependabot/npm_and_yarn/npm_and_y…
kubo6472 Jan 28, 2026
c052de6
Bump next in the npm_and_yarn group across 1 directory
dependabot[bot] Jan 28, 2026
5821c32
Merge pull request #6 from kubo6472/dependabot/npm_and_yarn/npm_and_y…
kubo6472 Jan 28, 2026
bf3dcc2
feat-redeploy
kubo6472 Jan 28, 2026
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"
169 changes: 122 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 @@ -90,9 +133,35 @@ export async function POST(req: Request) {
if (!secrets_request_data?.setSecrets?.hasOwnProperty('release')) {
return NextResponse.json({ error: JSON.stringify(secrets_request_data) }, { status: 500 })
}
const res_create_volume = await fetch(
`https://api.machines.dev/v1/apps/${app_name}/volumes`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${flyToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "bridge_data",
region: region,
size_gb: 1
})
}
)

if (res_create_volume.status !== 201) {
const volume_data = await res_create_volume.json()
return NextResponse.json(
{ error: JSON.stringify(volume_data) },
{ status: 500 }
)
}
const volume = await res_create_volume.json()
const volumeId = volume.id

// Create machine
await new Promise((r) => setTimeout(r, 500))

// Create machine
const res_create_machine = await fetch(`https://api.machines.dev/v1/apps/${app_name}/machines`, {
method: 'POST',
headers: {
Expand All @@ -106,25 +175,31 @@ export async function POST(req: Request) {
"env": {
"APP_ENV": "production"
},
"mounts": [
{
volume: volumeId,
path: "/data"
}
],
"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 +222,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 })
}
59 changes: 42 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,59 @@ 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"
/>
<button
className="p-2 mt-4 rounded-md w-full bg-purple-600 border-0 text-white hover:bg-purple-500"
type="submit"
>
Login
</button>
</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"
/>
<button
className="p-2 mt-4 rounded-md w-full bg-purple-600 border-0 text-white hover:bg-purple-500"
type="submit"
>
Send Login Code
</button>
</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