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
15 changes: 14 additions & 1 deletion apps/bridge/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,18 @@ BASE_URL=http://localhost:3000
# AWS Configuration
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
AWS_REGION=
AWS_REGION=us-east-1
S3_BUCKET_NAME=

# Chat Configuration
# Comma-separated list of authorized URL origins or domains for chat.
CHAT_AUTHORIZED_URLS=

# Bedrock Agent Configuration
# The unique ID of your Bedrock Agent (found in the Bedrock Console > Agents)
BEDROCK_AGENT_ID=

# The Alias ID of your Agent.
# Use 'TSTALIASID' for the Working Draft (Draft Version).
# Use a specific Alias ID (e.g., ABC123DEFG) for a published version.
BEDROCK_AGENT_ALIAS_ID=TSTALIASID
1 change: 1 addition & 0 deletions apps/bridge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"@aws-sdk/client-bedrock-agent-runtime": "^3.1027.0",
"@aws-sdk/client-s3": "^3.954.0",
"@aws-sdk/s3-request-presigner": "^3.954.0",
"@fastify/autoload": "^6.0.0",
Expand Down
3 changes: 3 additions & 0 deletions apps/bridge/src/modules/chat/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Copyright (C) 2024 BPS-Consulting - Licensed under AGPLv3
export { default } from "./routes.js";
export * from "./service.js";
82 changes: 82 additions & 0 deletions apps/bridge/src/modules/chat/origin-validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (C) 2024 BPS-Consulting - Licensed under AGPLv3

/**
* Validates if an origin is authorized for chat based on CHAT_AUTHORIZED_URLS.
* Supports exact matches and wildcard subdomains (e.g., https://*.example.com).
*/

/**
* Parses the CHAT_AUTHORIZED_URLS environment variable into a list of patterns.
*/
export function getAuthorizedPatterns(): string[] {
const envValue = process.env.CHAT_AUTHORIZED_URLS || "";
if (!envValue.trim()) {
return [];
}
return envValue
.split(",")
.map((url) => url.trim())
.filter((url) => url.length > 0);
}

/**
* Checks if a given origin matches an authorized pattern.
* Supports wildcard subdomains: https://*.example.com matches https://foo.example.com
*/
export function matchesPattern(origin: string, pattern: string): boolean {
// Handle wildcard patterns like https://*.example.com
if (pattern.includes("*.")) {
try {
const patternUrl = new URL(pattern.replace("*.", "placeholder."));
const originUrl = new URL(origin);

// Protocol must match exactly
if (patternUrl.protocol !== originUrl.protocol) {
return false;
}

// Port must match (or both be absent)
if (patternUrl.port !== originUrl.port) {
return false;
}

// Extract the base domain from pattern (e.g., example.com from *.example.com)
const baseDomain = patternUrl.hostname.replace("placeholder.", "");

// Origin hostname must end with .baseDomain or be exactly baseDomain
const originHostname = originUrl.hostname;
if (
originHostname === baseDomain ||
originHostname.endsWith("." + baseDomain)
) {
return true;
}

return false;
} catch {
return false;
}
}

// Exact match (case-sensitive)
return origin === pattern;
}

/**
* Validates if an origin is authorized for chat.
* @param origin - The Origin header value from the request
* @returns true if the origin is authorized, false otherwise
*/
export function isOriginAuthorized(origin: string | undefined): boolean {
if (!origin) {
return false;
}

const patterns = getAuthorizedPatterns();
if (patterns.length === 0) {
// No authorized URLs configured = no origins allowed
return false;
}

return patterns.some((pattern) => matchesPattern(origin, pattern));
}
91 changes: 91 additions & 0 deletions apps/bridge/src/modules/chat/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (C) 2024 BPS-Consulting - Licensed under AGPLv3
import { FastifyPluginAsync } from "fastify";
import { isOriginAuthorized } from "./origin-validator.js";
import { ChatService } from "./service.js";

interface ChatBody {
message: string;
}

const chatRoute: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
const chatService = new ChatService();

fastify.post<{ Body: ChatBody }>(
"/",
{
schema: {
tags: ["Chat"],
summary: "Send a chat message",
description:
"Sends a message to the AI chat service and returns a response. Only authorized origins can use this endpoint.",
body: {
type: "object",
required: ["message"],
properties: {
message: {
type: "string",
description: "The user's message to send to the chat service",
},
},
},
response: {
200: {
type: "object",
properties: {
reply: {
type: "string",
description: "The AI's response",
},
},
},
403: {
type: "object",
properties: {
error: {
type: "string",
description: "Error message for unauthorized access",
},
},
},
},
},
},
async (request, reply) => {
// Check origin authorization
const origin = request.headers.origin;

if (!isOriginAuthorized(origin)) {
request.log.warn({ origin }, "Unauthorized chat origin");
return reply.code(403).send({
error: "not authorized",
});
}

try {
const { message } = request.body;

if (!message || typeof message !== "string" || message.trim() === "") {
return reply.code(400).send({
error: "Message is required",
});
}

const chatReply = await chatService.chat(message.trim());

return reply.send({
reply: chatReply,
});
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
request.log.error({ error: errorMessage }, "Chat request failed");

return reply.code(500).send({
error: "Chat service unavailable",
});
}
},
);
};

export default chatRoute;
70 changes: 70 additions & 0 deletions apps/bridge/src/modules/chat/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (C) 2024 BPS-Consulting - Licensed under AGPLv3
import {
BedrockAgentRuntimeClient,
InvokeAgentCommand,
} from "@aws-sdk/client-bedrock-agent-runtime";

export interface ChatRequest {
message: string;
}

export interface ChatResponse {
reply: string;
}

/**
* Service for interacting with Amazon Bedrock Agents.
*/
export class ChatService {
private client: BedrockAgentRuntimeClient;
private agentId: string;
private agentAliasId: string;

constructor() {
this.client = new BedrockAgentRuntimeClient({
region: process.env.AWS_REGION || "us-east-1",
});
// These IDs are found in the AWS Console under Bedrock > Agents
this.agentId = process.env.BEDROCK_AGENT_ID || "";
this.agentAliasId = process.env.BEDROCK_AGENT_ALIAS_ID || "TSTALIASID"; // "TSTALIASID" is default for draft
}

/**
* Sends a message to a Bedrock Agent and returns the aggregated response.
* @param message - The user's message
* @param sessionId - Optional session ID to maintain conversation context
* @returns The Agent's reply
*/
async chat(
message: string,
sessionId: string = "default-session",
): Promise<string> {
const command = new InvokeAgentCommand({
agentId: this.agentId,
agentAliasId: this.agentAliasId,
sessionId: sessionId,
inputText: message,
});

try {
const response = await this.client.send(command);
let fullResponse = "";

// Bedrock Agents return a stream of events
if (response.completion) {
for await (const chunk of response.completion) {
if (chunk.chunk && chunk.chunk.bytes) {
// Decode the binary chunk into text
const text = new TextDecoder("utf-8").decode(chunk.chunk.bytes);
fullResponse += text;
}
}
}

return fullResponse || "No response generated";
} catch (error) {
console.error("Error invoking Bedrock Agent:", error);
throw new Error("Failed to get response from AI Agent");
}
}
}
20 changes: 0 additions & 20 deletions apps/bridge/src/shared/plugins/support.ts

This file was deleted.

1 change: 1 addition & 0 deletions apps/bridge/src/shared/schemas/wafir-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const fieldSchema = {
"markdown",
"rating",
"date",
"chat",
],
description:
"Field input type. Matches GitHub Form Schema types plus Wafir extensions.",
Expand Down
Loading
Loading