Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 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
2ec7a70
Bump eslint in the npm_and_yarn group across 1 directory (#7)
dependabot[bot] Apr 27, 2026
537eaf4
Merge pull request #8 from tojemoc/cursor/beeper-proper-logout-c258
kubo6472 Apr 27, 2026
a521bbd
Bump the npm_and_yarn group across 1 directory with 11 updates (#9)
dependabot[bot] Apr 27, 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"
33 changes: 33 additions & 0 deletions app/api/beeper/logout/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { NextResponse } from "next/server";

export async function POST(req: Request) {
const { beeperToken } = await req.json();

if (!beeperToken) {
return NextResponse.json({ error: "Missing beeperToken" }, { status: 400 });
}

const response = await fetch("https://matrix.beeper.com/_matrix/client/v3/logout", {
method: "POST",
headers: {
Authorization: `Bearer ${beeperToken}`,
"Content-Type": "application/json",
},
});

if (!response.ok) {
let details = "";
try {
details = JSON.stringify(await response.json());
} catch {
details = await response.text();
}

return NextResponse.json(
{ error: "Failed to logout from Beeper API", details },
{ status: response.status }
);
}
Comment on lines +18 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fallback path is broken: response body already consumed.

response.json() reads and locks the body stream. If parsing fails (e.g. upstream returned HTML or plain text), the catch block's await response.text() will throw TypeError: Body is unusable / already read, which propagates as an unhandled 500 and discards both the upstream status and any diagnostic info you intended to forward.

Read the body once as text, then try to parse it as JSON:

🐛 Proposed fix
     if (!response.ok) {
-        let details = "";
-        try {
-            details = JSON.stringify(await response.json());
-        } catch {
-            details = await response.text();
-        }
+        const raw = await response.text();
+        let details: unknown = raw;
+        try {
+            details = JSON.parse(raw);
+        } catch {
+            // keep raw text
+        }

         return NextResponse.json(
             { error: "Failed to logout from Beeper API", details },
             { status: response.status }
         );
     }
📝 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 (!response.ok) {
let details = "";
try {
details = JSON.stringify(await response.json());
} catch {
details = await response.text();
}
return NextResponse.json(
{ error: "Failed to logout from Beeper API", details },
{ status: response.status }
);
}
if (!response.ok) {
const raw = await response.text();
let details: unknown = raw;
try {
details = JSON.parse(raw);
} catch {
// keep raw text
}
return NextResponse.json(
{ error: "Failed to logout from Beeper API", details },
{ status: response.status }
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/beeper/logout/route.ts` around lines 18 - 30, The failure handling
for the Beeper logout uses response.json() then response.text(), which consumes
the body twice and can throw "Body is unusable"; instead read the body once as
text (use await response.text()), then attempt JSON.parse on that text inside a
try/catch to produce structured details, falling back to the raw text if parse
fails, and finally return NextResponse.json({ error: "Failed to logout from
Beeper API", details }, { status: response.status }); update the error branch in
route.ts (the block using the response variable and NextResponse.json) to
implement this single-read approach.


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

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 })
}
Loading