Skip to content
Merged
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
13 changes: 6 additions & 7 deletions src/commands/delete.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { Command } from "commander";

import { getBitteUrls } from "../config/constants";
import { validateAndParseOpenApiSpec } from "../services/openapi-service";
import { deletePlugin } from "../services/plugin-service";
import { getAuthentication } from "../services/signer-service";
import { PluginService } from "../services/plugin";
import { deployedUrl } from "../utils/deployed-url";
import { validateAndParseOpenApiSpec } from "../utils/openapi";
import { getSpecUrl, getHostname } from "../utils/url-utils";

export const deleteCommand = new Command()
Expand Down Expand Up @@ -32,15 +30,16 @@ export const deleteCommand = new Command()
console.error("Failed to parse account ID from OpenAPI specification.");
return;
}

const authentication = await getAuthentication(accountId);
const pluginService = new PluginService();
const authentication =
await pluginService.auth.getAuthentication(accountId);
if (!authentication) {
console.error("Authentication failed. Unable to delete the plugin.");
return;
}

try {
await deletePlugin(pluginId, getBitteUrls().BASE_URL);
await pluginService.delete(pluginId);
console.log(`Plugin ${pluginId} deleted successfully.`);
} catch (error) {
console.error("Failed to delete the plugin:", error);
Expand Down
12 changes: 5 additions & 7 deletions src/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Command } from "commander";

import { getBitteUrls } from "../config/constants";
import { validateAndParseOpenApiSpec } from "../services/openapi-service";
import { registerPlugin, updatePlugin } from "../services/plugin-service";
import { PluginService } from "../services/plugin";
import { deployedUrl } from "../utils/deployed-url";
import { validateAndParseOpenApiSpec } from "../utils/openapi";
import { getSpecUrl, getHostname } from "../utils/url-utils";

export const deployCommand = new Command()
Expand Down Expand Up @@ -33,16 +32,15 @@ export const deployCommand = new Command()
console.error("Failed to parse account ID from OpenAPI specification.");
return;
}
const bitteUrls = getBitteUrls();
const pluginService = new PluginService();
try {
await updatePlugin(id, accountId, bitteUrls.BASE_URL);
await pluginService.update(id, accountId);
console.log(`Plugin ${id} updated successfully.`);
} catch (error) {
console.log("Plugin not found. Attempting to register...");
const result = await registerPlugin({
const result = await pluginService.register({
pluginId: id,
accountId,
bitteUrls,
});
if (result) {
console.log(`Plugin ${id} registered successfully.`);
Expand Down
9 changes: 7 additions & 2 deletions src/commands/dev.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Command } from "commander";

import { startLocalTunnelAndRegister } from "../services/tunnel-service";
import { TunnelService } from "../services/tunnel";
import { detectPort } from "../utils/port-detector";

export const devCommand = new Command()
Expand All @@ -21,5 +21,10 @@ export const devCommand = new Command()
}
console.log(`Detected port: ${port}`);
}
await startLocalTunnelAndRegister(port, options.serveo, options.testnet);
const tunnelService = new TunnelService({
port,
useServeo: options.serveo,
useTestnet: options.testnet,
});
await tunnelService.start();
});
8 changes: 3 additions & 5 deletions src/commands/register.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Command } from "commander";

import { getBitteUrls } from "../config/constants";
import { validateAndParseOpenApiSpec } from "../services/openapi-service";
import { registerPlugin } from "../services/plugin-service";
import { PluginService } from "../services/plugin";
import { deployedUrl } from "../utils/deployed-url";
import { validateAndParseOpenApiSpec } from "../utils/openapi";
import { getHostname, getSpecUrl } from "../utils/url-utils";

export const registerCommand = new Command()
Expand Down Expand Up @@ -32,10 +31,9 @@ export const registerCommand = new Command()
return;
}

const result = await registerPlugin({
const result = await new PluginService().register({
pluginId,
accountId,
bitteUrls: getBitteUrls(),
});
if (result) {
console.log(`Plugin ${pluginId} registered successfully.`);
Expand Down
15 changes: 7 additions & 8 deletions src/commands/update.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { Command } from "commander";

import { getBitteUrls } from "../config/constants";
import { validateAndParseOpenApiSpec } from "../services/openapi-service";
import { updatePlugin } from "../services/plugin-service";
import { getAuthentication } from "../services/signer-service";
import { PluginService } from "../services/plugin";
import { deployedUrl } from "../utils/deployed-url";
import { validateAndParseOpenApiSpec } from "../utils/openapi";
import { getSpecUrl, getHostname } from "../utils/url-utils";

export const updateCommand = new Command()
Expand Down Expand Up @@ -32,15 +30,16 @@ export const updateCommand = new Command()
console.error("Failed to parse account ID from OpenAPI specification.");
return;
}

const authentication = await getAuthentication(accountId);
const pluginService = new PluginService();
const authentication =
await pluginService.auth.getAuthentication(accountId);
if (!authentication) {
console.error("Authentication failed. Unable to update the plugin.");
return;
}
const bitteUrls = getBitteUrls();

try {
await updatePlugin(pluginId, accountId, bitteUrls.BASE_URL);
await pluginService.update(pluginId, accountId);
console.log(`Plugin ${pluginId} updated successfully.`);
} catch (error) {
console.error("Failed to update the plugin:", error);
Expand Down
170 changes: 170 additions & 0 deletions src/services/authentication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import crypto from "crypto";
import dotenv from "dotenv";
import { createServer, IncomingMessage, ServerResponse } from "http";
import open from "open";

import type { BitteUrls } from "../config/constants";
import {
BITTE_KEY_ENV_KEY,
SIGN_MESSAGE,
SIGN_MESSAGE_PORT,
} from "../config/constants";
import { appendToEnv } from "../utils/file-utils";
import {
verifyMessage,
type KeySignMessageParams,
} from "../utils/verify-msg-utils";

dotenv.config();
dotenv.config({ path: ".env.local", override: true });

export class AuthenticationService {
private readonly bitteUrls: BitteUrls;

constructor(bitteUrls: BitteUrls) {
this.bitteUrls = bitteUrls;
}

async getAuthentication(accountId?: string): Promise<string | null> {
const bitteKeyString = process.env.BITTE_KEY;
if (!bitteKeyString) return null;

const parsedKey = JSON.parse(bitteKeyString) as KeySignMessageParams;
if (
(accountId &&
(await verifyMessage({
params: parsedKey,
accountIdToVerify: accountId,
}))) ||
!accountId
) {
return bitteKeyString;
}

return null;
}

async authenticateOrCreateKey(): Promise<string | null> {
const authentication = await this.getAuthentication();
if (authentication) {
console.log("Already authenticated.");
return authentication;
}

console.log(
"Not authenticated. Redirecting to Bitte wallet for signing...",
);
const newKey = await this.createAndStoreKey();
if (newKey) {
console.log("New key created and stored successfully.");
return JSON.stringify(newKey);
} else {
console.log("Failed to create and store new key.");
return null;
}
}

async getSignedMessage(): Promise<KeySignMessageParams> {
return new Promise((resolve, reject) => {
const server = createServer((req, res) =>
this.handleRequest(req, res, resolve, reject, server),
);

server.listen(SIGN_MESSAGE_PORT, () => {
const postEndpoint = `http://localhost:${SIGN_MESSAGE_PORT}`;
const nonce = crypto.randomBytes(16).toString("hex");
const signUrl = `${this.bitteUrls.SIGN_MESSAGE_URL}?message=${encodeURIComponent(
SIGN_MESSAGE,
)}&callbackUrl=${encodeURIComponent(
this.bitteUrls.SIGN_MESSAGE_SUCCESS_URL,
)}&nonce=${encodeURIComponent(nonce)}&postEndpoint=${encodeURIComponent(
postEndpoint,
)}`;
open(signUrl).catch((error) => {
console.error("Failed to open the browser:", error);
server.close();
reject(error);
});
});
});
}

private async createAndStoreKey(): Promise<KeySignMessageParams | null> {
try {
const signedMessage = await this.getSignedMessage();
if (!signedMessage) {
console.error("Failed to get signed message");
return null;
}

const isVerified = await verifyMessage({ params: signedMessage });
if (!isVerified) {
console.warn("Message verification failed");
}

await appendToEnv(BITTE_KEY_ENV_KEY, JSON.stringify(signedMessage));
return signedMessage;
} catch (error) {
console.error("Error creating and storing key:", error);
return null;
}
}

private handleRequest(
req: IncomingMessage,
res: ServerResponse,
resolve: (value: KeySignMessageParams) => void,
reject: (reason: Error) => void,
server: ReturnType<typeof createServer>,
): void {
this.setCORSHeaders(res);

if (req.method === "OPTIONS") {
this.handlePreflight(res);
return;
}

if (req.method === "POST") {
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", () => {
try {
const jsonBody = JSON.parse(body);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ message: "Signed message received" }));
resolve(jsonBody);
server.close(() => console.log("Temporary server closed"));
} catch (error) {
console.error("Error parsing JSON:", error);
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Invalid JSON" }));
reject(error as Error);
}
});
} else {
this.handleInvalidMethod(res, reject);
}
}

private setCORSHeaders(res: ServerResponse): void {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
}

private handlePreflight(res: ServerResponse): void {
res.writeHead(204);
res.end();
}

private handleInvalidMethod(
res: ServerResponse,
reject: (reason: Error) => void,
): void {
res.writeHead(405, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Method Not Allowed" }));
reject(new Error("Method Not Allowed"));
}
}
Loading