A Cloudflare Worker which works in conjunction with https://github.com/indexxing/bsky-alt-text

feat: cleanup, status codes, Bearer auth, env vars

Index d8b18700 0c09378d

Changed files
+73 -55
src
+2
README.md
···
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.
+11 -7
src/endpoints/condense_text.ts
···
summary: "Condense a given text based on a directive",
security: [
{
-
apiKey: [],
+
bearerAuth: [],
},
],
request: {
···
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: [
{
···
],
}],
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.",
···
return {
success: true,
-
altText: condensedText,
+
text: condensedText,
+
tokens: res.usageMetadata.totalTokenCount ?? 0,
};
} catch (e) {
+
c.status(500);
+
return {
success: false,
-
message: e,
+
error: e,
};
}
}
+9 -9
src/endpoints/generate.ts
···
summary: "Generates alt text for a given image.",
security: [
{
-
apiKey: [],
+
bearerAuth: [],
},
],
request: {
···
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,
},
});
···
return {
success: true,
-
altText: generatedText,
+
text: generatedText,
+
tokens: res.usageMetadata.totalTokenCount ?? 0,
};
} catch (e) {
return {
success: false,
-
message: e,
+
error: e,
};
}
}
+10 -19
src/index.ts
···
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,
···
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
+22 -9
src/middleware/auth.ts
···
c: Context<{ Bindings: Env; Variables: Variables }>,
next: Next,
) {
+
if (!c.env.AUTH_TOKEN) {
+
return c.json({
+
success: false,
+
error: "Authentication token is not specified in worker secrets.",
+
}, 500);
+
}
+
const authToken = c.req.header("Authorization");
-
if (!authToken) {
-
c.status(401);
-
return c.json({
-
success: false,
-
error: "No authentication token provided.",
-
});
+
if (!authToken || !authToken.startsWith("Bearer ")) {
+
return c.json(
+
{
+
success: false,
+
error:
+
"Authentication token missing or malformed. Expected 'Bearer <token>'.",
+
},
+
401,
+
{
+
"WWW-Authenticate": "Bearer",
+
},
+
);
}
-
if (authToken !== c.env.AUTH_TOKEN) {
-
c.status(403);
+
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();
+9 -10
src/types.ts
···
-
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 }>;
···
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;
···
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(),
-
});
+10 -1
wrangler.jsonc
···
"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
}
}