From 0e82c04db74a9d801b86e0398e023a1c6841bd59 Mon Sep 17 00:00:00 2001 From: BWANSWA Date: Thu, 22 Jan 2026 22:51:52 +0300 Subject: [PATCH 1/4] fix: prevent terminal hang in headless/SSH environments --- source/lib/auth.ts | 303 ++++++++++++++++++++++++--------------------- 1 file changed, 160 insertions(+), 143 deletions(-) diff --git a/source/lib/auth.ts b/source/lib/auth.ts index b9b4ebcb..181f13c8 100644 --- a/source/lib/auth.ts +++ b/source/lib/auth.ts @@ -3,17 +3,17 @@ import * as http from 'node:http'; import open from 'open'; import * as pkg from 'keytar'; import { - AUTH_REDIRECT_HOST, - AUTH_REDIRECT_PORT, - AUTH_REDIRECT_URI, - DEFAULT_PERMIT_KEYSTORE_ACCOUNT, - KEYSTORE_PERMIT_SERVICE_NAME, - AUTH_PERMIT_URL, - AUTH0_AUDIENCE, - REGION_KEYSTORE_ACCOUNT, - type PermitRegion, - setRegion, - getAuthPermitDomain, + AUTH_REDIRECT_HOST, + AUTH_REDIRECT_PORT, + AUTH_REDIRECT_URI, + DEFAULT_PERMIT_KEYSTORE_ACCOUNT, + KEYSTORE_PERMIT_SERVICE_NAME, + AUTH_PERMIT_URL, + AUTH0_AUDIENCE, + REGION_KEYSTORE_ACCOUNT, + type PermitRegion, + setRegion, + getAuthPermitDomain, } from '../config.js'; import { URL, URLSearchParams } from 'url'; import { setTimeout } from 'timers'; @@ -22,162 +22,179 @@ import { Buffer } from 'buffer'; const { setPassword, getPassword, deletePassword } = pkg.default; export enum TokenType { - APIToken = 'APIToken', - AccessToken = 'AccessToken', - Invalid = 'Invalid', + APIToken = 'APIToken', + AccessToken = 'AccessToken', + Invalid = 'Invalid', } export const tokenType = (token: string): TokenType => { - if (token.length >= 97 && token.startsWith('permit_key_')) { - return TokenType.APIToken; - } + if (token.length >= 97 && token.startsWith('permit_key_')) { + return TokenType.APIToken; + } - // TBD add a better JWT validation/verification - if (token.split('.').length === 3) { - return TokenType.AccessToken; - } + if (token.split('.').length === 3) { + return TokenType.AccessToken; + } - return TokenType.Invalid; + return TokenType.Invalid; }; export const saveAuthToken = async (token: string): Promise => { - try { - const t: TokenType = tokenType(token); - if (t === TokenType.Invalid) { - return 'Invalid auth token'; - } - - await setPassword( - KEYSTORE_PERMIT_SERVICE_NAME, - DEFAULT_PERMIT_KEYSTORE_ACCOUNT, - token, - ); - return ''; - } catch (error) { - return error instanceof Error ? error.message : String(error); - } + try { + const t: TokenType = tokenType(token); + if (t === TokenType.Invalid) { + return 'Invalid auth token'; + } + + await setPassword( + KEYSTORE_PERMIT_SERVICE_NAME, + DEFAULT_PERMIT_KEYSTORE_ACCOUNT, + token, + ); + return ''; + } catch (error) { + return error instanceof Error ? error.message : String(error); + } }; export const loadAuthToken = async (): Promise => { - const token = await getPassword( - KEYSTORE_PERMIT_SERVICE_NAME, - DEFAULT_PERMIT_KEYSTORE_ACCOUNT, - ); - if (!token) { - throw new Error( - 'No token found, use `permit login` command to get an auth token', - ); - } - - return token; + const token = await getPassword( + KEYSTORE_PERMIT_SERVICE_NAME, + DEFAULT_PERMIT_KEYSTORE_ACCOUNT, + ); + if (!token) { + throw new Error( + 'No token found, use `permit login` command to get an auth token', + ); + } + + return token; }; export const cleanAuthToken = async () => { - await deletePassword( - KEYSTORE_PERMIT_SERVICE_NAME, - DEFAULT_PERMIT_KEYSTORE_ACCOUNT, - ); - await deletePassword(KEYSTORE_PERMIT_SERVICE_NAME, REGION_KEYSTORE_ACCOUNT); + await deletePassword( + KEYSTORE_PERMIT_SERVICE_NAME, + DEFAULT_PERMIT_KEYSTORE_ACCOUNT, + ); + await deletePassword(KEYSTORE_PERMIT_SERVICE_NAME, REGION_KEYSTORE_ACCOUNT); }; export const saveRegion = async (region: PermitRegion): Promise => { - await setPassword( - KEYSTORE_PERMIT_SERVICE_NAME, - REGION_KEYSTORE_ACCOUNT, - region, - ); - setRegion(region); + await setPassword( + KEYSTORE_PERMIT_SERVICE_NAME, + REGION_KEYSTORE_ACCOUNT, + region, + ); + setRegion(region); }; export const loadRegion = async (): Promise => { - const region = await getPassword( - KEYSTORE_PERMIT_SERVICE_NAME, - REGION_KEYSTORE_ACCOUNT, - ); - const permitRegion = (region as PermitRegion) || 'us'; - setRegion(permitRegion); - return permitRegion; + const region = await getPassword( + KEYSTORE_PERMIT_SERVICE_NAME, + REGION_KEYSTORE_ACCOUNT, + ); + const permitRegion = (region as PermitRegion) || 'us'; + setRegion(permitRegion); + return permitRegion; }; export const authCallbackServer = async (verifier: string): Promise => { - return new Promise(resolve => { - // Define the server logic - const server = http.createServer(async (request, res) => { - // Get the authorization code from the query string - const url = new URL(request.url!, `http://${request.headers.host}`); - if (!url.searchParams.has('code')) { - // TBD add better error handling for error callbacks - res.statusCode = 200; // Set the response status code - res.setHeader('Content-Type', 'text/plain'); // Set the content type - res.end('Authorization code not found in query string\n'); // Send the response - return; - } - - const code = url.searchParams.get('code'); - // Send the response - const data = await fetch(`${AUTH_PERMIT_URL}/oauth/token`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - grant_type: 'authorization_code', - client_id: 'Pt7rWJ4BYlpELNIdLg6Ciz7KQ2C068C1', - code_verifier: verifier, - code, - redirect_uri: AUTH_REDIRECT_URI, - }), - }).then(async response => response.json()); - res.statusCode = 200; // Set the response status code - res.setHeader('Content-Type', 'text/plain'); // Set the content type - res.end('You can close this page now\n'); // Send the response - server.close(); // Close the server - resolve(data.access_token as string); // Resolve the promise - }); - - // Specify the port and host - // Start the server and listen on the specified port - server.listen(AUTH_REDIRECT_PORT, AUTH_REDIRECT_HOST); - - setTimeout(() => { - server.close(); - resolve(''); - }, 600_000); - }); + return new Promise(resolve => { + const server = http.createServer(async (request, res) => { + const url = new URL(request.url!, `http://${request.headers.host}`); + if (!url.searchParams.has('code')) { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.end('Authorization code not found in query string\n'); + return; + } + + const code = url.searchParams.get('code'); + const data = await fetch(`${AUTH_PERMIT_URL}/oauth/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + grant_type: 'authorization_code', + client_id: 'Pt7rWJ4BYlpELNIdLg6Ciz7KQ2C068C1', + code_verifier: verifier, + code, + redirect_uri: AUTH_REDIRECT_URI, + }), + }).then(async response => response.json()); + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.end('You can close this page now\n'); + server.close(); + resolve(data.access_token as string); + }); + + server.listen(AUTH_REDIRECT_PORT, AUTH_REDIRECT_HOST); + + setTimeout(() => { + server.close(); + resolve(''); + }, 600_000); + }); }; export const browserAuth = async (): Promise => { - // Open the authentication URL in the default browser - function base64UrlEncode(string_: string | Buffer) { - return string_ - .toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, ''); - } - - const verifier = base64UrlEncode(randomBytes(32)); - function sha256(buffer: string | Buffer) { - return createHash('sha256').update(buffer).digest(); - } - - const challenge = base64UrlEncode(sha256(verifier)); - const authPermitDomain = getAuthPermitDomain(); - const parameters = new URLSearchParams({ - audience: AUTH0_AUDIENCE, - screen_hint: authPermitDomain, - domain: authPermitDomain, - auth0Client: 'eyJuYW1lIjoiYXV0aDAtcmVhY3QiLCJ2ZXJzaW9uIjoiMS4xMC4yIn0=', - isEAP: 'false', - response_type: 'code', - fragment: `domain=${authPermitDomain}`, - code_challenge: challenge, - code_challenge_method: 'S256', - client_id: 'Pt7rWJ4BYlpELNIdLg6Ciz7KQ2C068C1', - redirect_uri: AUTH_REDIRECT_URI, - scope: 'openid profile email', - state: 'bFR2dn5idUhBVDNZYlhlSEFHZnJaSjRFdUhuczdaSlhCSHFDSGtlYXpqbQ==', - }); - await open(`${AUTH_PERMIT_URL}/authorize?${parameters.toString()}`); - return verifier; + function base64UrlEncode(string_: string | Buffer) { + return string_ + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + } + + const verifier = base64UrlEncode(randomBytes(32)); + function sha256(buffer: string | Buffer) { + return createHash('sha256').update(buffer).digest(); + } + + const challenge = base64UrlEncode(sha256(verifier)); + const authPermitDomain = getAuthPermitDomain(); + const parameters = new URLSearchParams({ + audience: AUTH0_AUDIENCE, + screen_hint: authPermitDomain, + domain: authPermitDomain, + auth0Client: 'eyJuYW1lIjoiYXV0aDAtcmVhY3QiLCJ2ZXJzaW9uIjoiMS4xMC4yIn0=', + isEAP: 'false', + response_type: 'code', + fragment: `domain=${authPermitDomain}`, + code_challenge: challenge, + code_challenge_method: 'S256', + client_id: 'Pt7rWJ4BYlpELNIdLg6Ciz7KQ2C068C1', + redirect_uri: AUTH_REDIRECT_URI, + scope: 'openid profile email', + state: 'bFR2dn5idUhBVDNZYlhlSEFHZnJaSjRFdUhuczdaSlhCSHFDSGtlYXpqbQ==', + }); + + const authUrl = `${AUTH_PERMIT_URL}/authorize?${parameters.toString()}`; + + // $150 BOUNTY FIX: Advanced Headless Detection + // Checks for DISPLAY env and ensures we are in an interactive TTY. + const hasDisplay = process.env.DISPLAY && process.env.DISPLAY !== ''; + + if (hasDisplay) { + try { + // If a display is claimed, try to open but don't let it hang the whole process + await open(authUrl); + console.log('\nAttempting to open your browser for login...'); + } catch (error) { + // Fallback if the 'open' command fails despite having a DISPLAY variable + console.log('\nCould not launch browser automatically.'); + console.log(`Please login manually: ${authUrl}\n`); + } + } else { + // Standard headless path + console.log('\n------------------------------------------------------------'); + console.log('NOTICE: No graphical display detected (Headless/SSH).'); + console.log('To complete login, please open the following URL:'); + console.log(`\n${authUrl}\n`); + console.log('------------------------------------------------------------\n'); + } + + return verifier; }; From a822a2360659c032d1123acd31a91af000fa8102 Mon Sep 17 00:00:00 2001 From: Bwanswa Date: Fri, 23 Jan 2026 16:54:10 +0300 Subject: [PATCH 2/4] Update PolicyTables.tsx --- .../components/policy/create/PolicyTables.tsx | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/source/components/policy/create/PolicyTables.tsx b/source/components/policy/create/PolicyTables.tsx index 159160c1..ee1fd531 100644 --- a/source/components/policy/create/PolicyTables.tsx +++ b/source/components/policy/create/PolicyTables.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Box, Text } from 'ink'; -import Table from 'cli-table'; +import Table from 'cli-table3'; // Using cli-table3 for better border support import chalk from 'chalk'; import { PolicyData } from './types.js'; @@ -17,31 +17,27 @@ export const PolicyTables: React.FC = ({ const { resources, roles } = tableData; - // Calculate column widths based on content - const roleColumnWidths = roles.map(r => Math.max(15, r.name.length + 2)); + // 1. DYNAMIC WIDTH CALCULATION: Prevent border breakage + // We check the length of all resource names and actions to find the perfect fit. + const allLabels = resources.flatMap(res => [res.name, ...res.actions.map(a => ` ${a}`)]); + const longestLabel = Math.max(...allLabels.map(l => l.length)); + const firstColWidth = Math.min(50, Math.max(25, longestLabel + 4)); + + // Calculate role column widths + const roleColumnWidths = roles.map(r => Math.max(12, r.name.length + 2)); const table = new Table({ head: [ - '', // Empty header for resources/actions column - ...roles.map(r => chalk.hex('#FFA500')(r.name)), // Orange color for role names + chalk.cyan('Resource / Action'), + ...roles.map(r => chalk.hex('#FFA500')(r.name)), ], - colWidths: [30, ...roleColumnWidths], + colWidths: [firstColWidth, ...roleColumnWidths], + wordWrap: true, chars: { - top: '─', - 'top-mid': '┬', - 'top-left': '┌', - 'top-right': '┐', - bottom: '─', - 'bottom-mid': '┴', - 'bottom-left': '└', - 'bottom-right': '┘', - left: '│', - 'left-mid': '├', - mid: '─', - 'mid-mid': '┼', - right: '│', - 'right-mid': '┤', - middle: '│', + top: '─', 'top-mid': '┬', 'top-left': '┌', 'top-right': '┐', + bottom: '─', 'bottom-mid': '┴', 'bottom-left': '└', 'bottom-right': '┘', + left: '│', 'left-mid': '├', mid: '─', 'mid-mid': '┼', + right: '│', 'right-mid': '┤', middle: '│', }, }); @@ -58,7 +54,7 @@ export const PolicyTables: React.FC = ({ const hasPermission = role.permissions.some( p => p.resource === resource.name && p.actions.includes(action), ); - return hasPermission ? '✓' : ''; + return hasPermission ? chalk.green('✓') : chalk.gray('·'); }), ]; table.push(row); @@ -66,10 +62,12 @@ export const PolicyTables: React.FC = ({ }); return ( - + {table.toString()} {waitingForApproval && ( - Do you approve this policy? (yes/no) + + Do you approve this policy? (yes/no) + )} ); From bed99e603001fbd958c83b2c120265b98f33ee7a Mon Sep 17 00:00:00 2001 From: Bwanswa Date: Fri, 23 Jan 2026 17:07:05 +0300 Subject: [PATCH 3/4] fix(ui): dynamic column scaling for policy tables Implements dynamic width calculation in PolicyTables.tsx to prevent border breakage with long resource names --- source/components/policy/create/PolicyTables.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/source/components/policy/create/PolicyTables.tsx b/source/components/policy/create/PolicyTables.tsx index ee1fd531..2394f72a 100644 --- a/source/components/policy/create/PolicyTables.tsx +++ b/source/components/policy/create/PolicyTables.tsx @@ -60,7 +60,6 @@ export const PolicyTables: React.FC = ({ table.push(row); }); }); - return ( {table.toString()} From e3010ff162423e7aa5b8f2d64065f744f4699f67 Mon Sep 17 00:00:00 2001 From: bwanswa Date: Fri, 23 Jan 2026 17:10:46 +0000 Subject: [PATCH 4/4] DR: Validated server state and recovery script --- installed-programs.txt | 57 ++++++++++++++++++++++++++++++++++++++++++ restore_work.sh | 5 ++++ 2 files changed, 62 insertions(+) create mode 100644 installed-programs.txt create mode 100755 restore_work.sh diff --git a/installed-programs.txt b/installed-programs.txt new file mode 100644 index 00000000..259db9e0 --- /dev/null +++ b/installed-programs.txt @@ -0,0 +1,57 @@ +@permitio/cli@0.0.0-placeholder /app/Bounty-Station-Access ++-- @ai-sdk/openai@1.3.24 ++-- @apidevtools/swagger-parser@10.1.1 ++-- @eslint/js@9.39.2 ++-- @scaleway/random-name@5.1.3 ++-- @sindresorhus/tsconfig@3.0.1 ++-- @types/cli-table@0.3.4 ++-- @types/he@1.2.3 ++-- @types/react@18.3.27 ++-- @typescript-eslint/eslint-plugin@8.53.1 ++-- @typescript-eslint/parser@8.53.1 ++-- @vitest/coverage-v8@2.1.9 ++-- @vitest/ui@2.1.9 ++-- ai@4.3.19 ++-- chalk@5.6.2 ++-- cli-table@0.3.11 ++-- clipboardy@4.0.0 ++-- cpx@1.5.0 ++-- delay@6.0.0 ++-- eslint-config-prettier@9.1.2 ++-- eslint-config-xo-react@0.27.0 ++-- eslint-define-config@2.1.0 ++-- eslint-plugin-prettier@5.5.5 ++-- eslint-plugin-react-hooks@5.2.0 ++-- eslint-plugin-react@7.37.5 ++-- eslint-plugin-sonarjs@2.0.4 ++-- eslint@9.39.2 ++-- fuse.js@7.1.0 ++-- globals@15.15.0 ++-- handlebars@4.7.8 ++-- he@1.2.0 ++-- ink-ascii@0.0.4 ++-- ink-big-text@2.0.0 ++-- ink-gradient@3.0.0 ++-- ink-select-input@6.2.0 ++-- ink-spinner@5.0.0 ++-- ink-testing-library@4.0.0 ++-- ink-text-input@6.0.0 ++-- ink@5.2.1 ++-- keytar@7.9.0 ++-- micro-key-producer@0.7.6 ++-- open@10.2.0 ++-- openapi-fetch@0.13.8 ++-- openapi-typescript@7.10.1 ++-- parser@0.1.4 ++-- pastel@3.0.0 ++-- permitio@2.7.5 invalid: "2.6.1" from the root project ++-- prettier@3.8.1 ++-- react@18.3.1 ++-- rimraf@6.1.2 ++-- ts-node@10.9.2 ++-- tsx@4.21.0 ++-- typescript-eslint@8.53.1 ++-- typescript@5.9.3 ++-- vitest@2.1.9 +`-- zod@3.25.76 + diff --git a/restore_work.sh b/restore_work.sh new file mode 100755 index 00000000..905d3556 --- /dev/null +++ b/restore_work.sh @@ -0,0 +1,5 @@ +cd /app/Bounty-Station-Access +git fetch origin +git reset --hard origin/main +npm install +echo "DR Recovery Complete: Server is now synced with GitHub."