diff --git a/README.md b/README.md index 2d063cb..00b7a3b 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,5 @@ A Cloudflare Worker which works in conjunction with https://github.com/indexxing/Bluesky-Alt-Text. All endpoints are based off the original cloud function made by [symmetricalboy](https://github.com/symmetricalboy) [here](https://github.com/symmetricalboy/gen-alt-text/blob/main/functions/index.js), just in Typescript and prepackaged into a Cloudflare Worker environment. Documentation is served at the root of the worker deployment. There is a root path variable specified in the entrypoint file because my setup involves using a worker route wildcard on my custom domain. + +**Note:** This does not support video captioning, or uploading larger media for processing yet like the source cloud function. diff --git a/src/endpoints/condense_text.ts b/src/endpoints/condense_text.ts index 56399af..6269e59 100644 --- a/src/endpoints/condense_text.ts +++ b/src/endpoints/condense_text.ts @@ -8,7 +8,7 @@ export class CondenseTextEndpoint extends OpenAPIRoute { summary: "Condense a given text based on a directive", security: [ { - apiKey: [], + bearerAuth: [], }, ], request: { @@ -67,8 +67,7 @@ export class CondenseTextEndpoint extends OpenAPIRoute { try { const res = await c.var.gemini.models.generateContent({ - // * Original cloud function used "gemini-2.0-flash", but I think the lite version should work fine too. - model: "gemini-2.0-flash-lite", + model: c.env.GEMINI_MODEL, contents: [{ parts: [ { @@ -79,14 +78,16 @@ export class CondenseTextEndpoint extends OpenAPIRoute { ], }], config: { - temperature: 0.2, - maxOutputTokens: 1024, + temperature: c.env.GEMINI_CONDENSE_TEMPERATURE, + maxOutputTokens: c.env.GEMINI_CONDENSE_MAX_OUTPUT_TOKENS, }, }); const condensedText = res.candidates?.[0]?.content?.parts?.[0] ?.text; if (!condensedText) { + c.status(502); // Bad response from upstream API resulting in "Bad Gateway" status + return { success: false, error: "Failed to condense text.", @@ -95,12 +96,15 @@ export class CondenseTextEndpoint extends OpenAPIRoute { return { success: true, - altText: condensedText, + text: condensedText, + tokens: res.usageMetadata.totalTokenCount ?? 0, }; } catch (e) { + c.status(500); + return { success: false, - message: e, + error: e, }; } } diff --git a/src/endpoints/generate.ts b/src/endpoints/generate.ts index a767e43..58625a9 100644 --- a/src/endpoints/generate.ts +++ b/src/endpoints/generate.ts @@ -52,7 +52,7 @@ export class GenerateEndpoint extends OpenAPIRoute { summary: "Generates alt text for a given image.", security: [ { - apiKey: [], + bearerAuth: [], }, ], request: { @@ -145,17 +145,16 @@ export class GenerateEndpoint extends OpenAPIRoute { try { const res = await c.var.gemini.models.generateContent({ - // * Original cloud function used "gemini-2.0-flash", but I think the lite version should work fine too. - model: "gemini-2.0-flash-lite", + model: c.env.GEMINI_MODEL, contents: [ { text: systemInstructions }, { inlineData: { mimeType: mimeType, data: base64Data } }, ], config: { - temperature: 0.2, // Lower temperature for more deterministic output - maxOutputTokens: 2048, // Allow for longer descriptions if needed - topP: 0.95, - topK: 64, + temperature: c.env.GEMINI_GENERATE_TEMPERATURE, + maxOutputTokens: c.env.GEMINI_GENERATE_MAX_OUTPUT_TOKENS, + topP: c.env.GEMINI_GENERATE_TOP_P, + topK: c.env.GEMINI_GENERATE_TOP_K, }, }); @@ -170,12 +169,13 @@ export class GenerateEndpoint extends OpenAPIRoute { return { success: true, - altText: generatedText, + text: generatedText, + tokens: res.usageMetadata.totalTokenCount ?? 0, }; } catch (e) { return { success: false, - message: e, + error: e, }; } } diff --git a/src/index.ts b/src/index.ts index f97f2bd..29136d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,21 +16,12 @@ const rootPath = "/api/altText/"; app.use( "*", cors({ - origin: (origin) => { - const allowedOrigins = [ - "https://indexx.dev", - "moz-extension://", - "chrome-extension://", - ]; - - if ( - origin && - allowedOrigins.some((allowed) => origin.startsWith(allowed)) - ) { - return origin; - } - return null; - }, + origin: [ + "https://indexx.dev", + "chrome-extension://", + "safari-web-extension://", + "moz-extension://", + ], allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], allowHeaders: ["*"], maxAge: 600, @@ -51,10 +42,10 @@ const openapi = fromHono(app, { openapi_url: rootPath + "openapi.json", }); -openapi.registry.registerComponent("securitySchemes", "apiKey", { - type: "apiKey", - name: "Authorization", - in: "header", +openapi.registry.registerComponent("securitySchemes", "bearerAuth", { + type: "http", + scheme: "bearer", + bearerFormat: "API key", }); // Define Middlewares diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index 8977aae..d59a9dc 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -5,22 +5,35 @@ export async function authMiddleware( c: Context<{ Bindings: Env; Variables: Variables }>, next: Next, ) { - const authToken = c.req.header("Authorization"); - - if (!authToken) { - c.status(401); + if (!c.env.AUTH_TOKEN) { return c.json({ success: false, - error: "No authentication token provided.", - }); + error: "Authentication token is not specified in worker secrets.", + }, 500); } - if (authToken !== c.env.AUTH_TOKEN) { - c.status(403); + const authToken = c.req.header("Authorization"); + + if (!authToken || !authToken.startsWith("Bearer ")) { + return c.json( + { + success: false, + error: + "Authentication token missing or malformed. Expected 'Bearer '.", + }, + 401, + { + "WWW-Authenticate": "Bearer", + }, + ); + } + + const token = authToken.split(" ")[1]; + if (token !== c.env.AUTH_TOKEN) { return c.json({ success: false, error: "Authentication token provided is invalid.", - }); + }, 401); } await next(); diff --git a/src/types.ts b/src/types.ts index 2407b79..0634900 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,4 @@ -import { DateTime, Str } from "chanfana"; import type { Context } from "hono"; -import { z } from "zod"; import type { GoogleGenAI } from "@google/genai"; export type AppContext = Context<{ Bindings: Env; Variables: Variables }>; @@ -11,6 +9,15 @@ export interface Env { ABSOLUTE_MAX_LENGTH: number; MAX_DIRECT_BLOB_SIZE: number; + GEMINI_MODEL: string; + GEMINI_GENERATE_TEMPERATURE: number; + GEMINI_GENERATE_MAX_OUTPUT_TOKENS: number; + GEMINI_GENERATE_TOP_P: number; + GEMINI_GENERATE_TOP_K: number; + + GEMINI_CONDENSE_TEMPERATURE: number; + GEMINI_CONDENSE_MAX_OUTPUT_TOKENS: number; + // Secrets AUTH_TOKEN: string; GEMINI_API_KEY: string; @@ -19,11 +26,3 @@ export interface Env { export type Variables = { gemini: GoogleGenAI; }; - -export const Task = z.object({ - name: Str({ example: "lorem" }), - slug: Str(), - description: Str({ required: false }), - completed: z.boolean().default(false), - due_date: DateTime(), -}); diff --git a/wrangler.jsonc b/wrangler.jsonc index 111404f..dfa068d 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -12,6 +12,15 @@ "vars": { "MAX_ALT_TEXT_LENGTH": 2000, "ABSOLUTE_MAX_LENGTH": 5000, - "MAX_DIRECT_BLOB_SIZE": 5242880 + "MAX_DIRECT_BLOB_SIZE": 5242880, + + "GEMINI_MODEL": "gemini-2.0-flash-lite", + "GEMINI_GENERATE_TEMPERATURE": 0.2, + "GEMINI_GENERATE_MAX_OUTPUT_TOKENS": 2048, + "GEMINI_GENERATE_TOP_P": 0.95, + "GEMINI_GENERATE_TOP_K": 64, + + "GEMINI_CONDENSE_TEMPERATURE": 0.2, + "GEMINI_CONDENSE_MAX_OUTPUT_TOKENS": 1024 } }