feat: basic generate and condense_text endpoints
This commit is contained in:
parent
bbc7b5bf9b
commit
d1aa5106f5
14 changed files with 3210 additions and 1868 deletions
1257
package-lock.json
generated
1257
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "cloudflare-workers-openapi",
|
||||
"name": "bluesky-alt-text-worker",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
"cf-typegen": "wrangler types"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.4.0",
|
||||
"chanfana": "^2.6.3",
|
||||
"hono": "^4.6.20",
|
||||
"zod": "^3.24.1"
|
||||
|
|
|
|||
108
src/endpoints/condense_text.ts
Normal file
108
src/endpoints/condense_text.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { Num, OpenAPIRoute } from "chanfana";
|
||||
import { z } from "zod";
|
||||
import { type AppContext } from "../types";
|
||||
|
||||
export class CondenseTextEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ["AltText"],
|
||||
summary: "Condense a given text based on a directive",
|
||||
security: [
|
||||
{
|
||||
apiKey: [],
|
||||
},
|
||||
],
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
text: z.string({
|
||||
description: "The text to be condensed.",
|
||||
required_error:
|
||||
"Text is required for condensation.",
|
||||
}).min(1, "Text cannot be empty."),
|
||||
directive: z.string({
|
||||
description:
|
||||
"Instructions for condensing the text (e.g., 'Summarize this article', 'Extract keywords').",
|
||||
required_error:
|
||||
"A condensation directive is required.",
|
||||
}).min(1, "Directive cannot be empty."),
|
||||
targetLength: Num({
|
||||
description:
|
||||
"The approximate target length for the condensed text (e.g., number of sentences, characters, or words).",
|
||||
default: 200,
|
||||
}).optional(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Returns the condensed text",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
success: z.boolean(),
|
||||
condensedText: z.string().nullable(),
|
||||
error: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
"500": {
|
||||
description:
|
||||
"Internal Server Error - Issue with Cloud Function or API call",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
success: z.boolean(),
|
||||
message: z.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: AppContext) {
|
||||
const data = await this.getValidatedData<typeof this.schema>();
|
||||
const { text, directive, targetLength } = data.body;
|
||||
|
||||
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",
|
||||
contents: [{
|
||||
parts: [
|
||||
{ text: directive },
|
||||
{ text: text },
|
||||
],
|
||||
}],
|
||||
config: {
|
||||
temperature: 0.2,
|
||||
maxOutputTokens: 1024,
|
||||
},
|
||||
});
|
||||
|
||||
const condensedText = res.candidates?.[0]?.content?.parts?.[0]
|
||||
?.text;
|
||||
if (!condensedText) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Failed to condense text.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
altText: condensedText,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
message: e,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
182
src/endpoints/generate.ts
Normal file
182
src/endpoints/generate.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import { Num, OpenAPIRoute } from "chanfana";
|
||||
import { z } from "zod";
|
||||
import { type AppContext } from "../types";
|
||||
|
||||
const systemInstructions =
|
||||
`You will be provided with visual media (either a still image or a video file). Your task is to generate alternative text (alt-text) that describes the media's content and context. This alt-text is intended for use with screen reader technology, assisting individuals who are blind or visually impaired to understand the visual information. Adhere to the following guidelines strictly:
|
||||
|
||||
1. **Media Type Identification:** * Begin by identifying the type of media. For images, note if it is a "photograph", "painting", "illustration", "diagram", "screenshot", "comic panel", etc. For videos, simply describe the content directly without prefacing with "Video describing...".
|
||||
|
||||
2. **Content and Purpose:**
|
||||
* Describe the visual content accurately and thoroughly. Explain the media in the context that it is presented.
|
||||
* Convey the media's purpose. Why is this included? What information is it trying to present? What is the core message?
|
||||
* Prioritize the most important information, placing it at the beginning of the alt-text.
|
||||
* If the image serves a specific function (e.g., a button or a link), describe the function. Example: "Search button" or "Link to the homepage".
|
||||
|
||||
3. **Video-Specific Instructions:**
|
||||
* For standard videos, describe the key visual elements, actions, scenes, and any text overlays that appear throughout the *duration* of the video playback. Focus on conveying the narrative or informational flow presented visually. Do *not* just describe a single frame or thumbnail.
|
||||
* **For short, looping animations (like animated GIFs or silent WebM files):** Describe the *complete action* or the *entire sequence* shown in the loop. Even if brief, explain what happens from the beginning to the end of the animation cycle. For example, instead of "A cat looking up", describe "Video showing a cat repeatedly looking up, raising its head, and then lowering it again in a loop."
|
||||
|
||||
4. **Sequential Art (Comics/Webcomics):**
|
||||
* For media containing sequential art like comic panels or webcomics, describe the narrative progression. Detail the actions, characters, settings, and dialogue/captions within each panel or across the sequence to tell the story visually represented.
|
||||
|
||||
5. **Text within the Media:**
|
||||
* If the media contains text (e.g., signs, labels, captions, text overlays in videos), transcribe the text *verbatim* within the alt-text. Indicate that this is a direct quote by using quotation marks. Example: 'A sign that reads, "Proceed with Caution".'
|
||||
* **Crucially**, if the media consists primarily of a large block of text (e.g., a screenshot of an article, a quote graphic, a presentation slide), you MUST transcribe the *entire* text content verbatim, up to a practical limit (e.g., 2000 characters). Accuracy and completeness of the text take precedence over brevity in these cases.
|
||||
* For screenshots containing User Interface (UI) elements, transcribe essential text (button labels, input field values, key menu items). Exercise judgment to omit minor or redundant UI text (tooltips, decorative labels) that doesn't significantly contribute to understanding the core function or state shown. Example: "Screenshot of a software settings window. The 'Notifications' tab is active, showing a checkbox labeled \"Enable desktop alerts\" which is checked."
|
||||
|
||||
6. **Brevity and Clarity:**
|
||||
* Keep descriptions concise *except* when transcribing significant amounts of text or describing sequential narratives (comics, videos), where clarity and completeness are more important. Aim for under 150 characters for simple images where possible.
|
||||
* Use clear, simple language. Avoid jargon unless it's part of transcribed text or essential to the meaning.
|
||||
* Use proper grammar, punctuation, and capitalization. End sentences with a period.
|
||||
|
||||
7. **Notable Individuals:**
|
||||
* If the media features recognizable people, identify them by name. If their role or title is relevant, include that too. Example: "Photograph of Dr. Jane Goodall observing chimpanzees."
|
||||
|
||||
8. **Inappropriate or Sensitive Content:**
|
||||
* If the media depicts potentially sensitive, offensive, or harmful content, maintain a professional, objective, and clinical tone.
|
||||
* Describe the factual visual content accurately but avoid graphic or sensationalized language. Aim for a descriptive level appropriate for a general audience (e.g., PG-13).
|
||||
|
||||
9. **Output Format:**
|
||||
* Provide *only* the descriptive alt-text. Do *not* include introductory phrases (e.g., "The image shows...", "Alt-text:"), conversational filler, or follow-up statements. Output *just* the description.
|
||||
|
||||
10. **Do Not's:**
|
||||
* Do not begin descriptions with generic phrases like "Image of...", "Video of...", etc., unless specifying the type as in Guideline 1.
|
||||
* Do not add external information, interpretations, or assumptions not directly represented in the visual media itself.
|
||||
|
||||
By consistently applying these guidelines, you will create alt-text that is informative, accurate, concise where appropriate, and genuinely helpful for users of assistive technology across different types of visual media.`;
|
||||
|
||||
export class GenerateEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ["AltText"],
|
||||
summary: "Generates alt text for a given image.",
|
||||
security: [
|
||||
{
|
||||
apiKey: [],
|
||||
},
|
||||
],
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
base64Data: z.string({
|
||||
description: "The base64 encoded image data.",
|
||||
required_error:
|
||||
"Image data (base64Data) is required.",
|
||||
}).min(1, "Image data cannot be empty."),
|
||||
mimeType: z.string({
|
||||
description:
|
||||
"The MIME type of the image (e.g., 'image/jpeg', 'image/png').",
|
||||
required_error: "MIME type is required.",
|
||||
}).regex(
|
||||
/^image\/(jpeg|png|gif|webp|bmp|svg\+xml)$/,
|
||||
"Invalid image MIME type.",
|
||||
),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Returns the generated alt text.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
success: z.boolean().default(true),
|
||||
altText: z.string().nullable(),
|
||||
error: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
"400": {
|
||||
description: "Bad Request - Invalid input or missing fields.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
success: z.boolean().default(false),
|
||||
error: z.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
"403": {
|
||||
description: "Forbidden - Origin not allowed.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
success: z.boolean().default(false),
|
||||
error: z.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
"500": {
|
||||
description:
|
||||
"Internal Server Error - Issue with Cloud Function or AI API call.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
success: z.boolean().default(false),
|
||||
error: z.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
"502": {
|
||||
description: "Bad Gateway - AI API failed.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
success: z.boolean().default(false),
|
||||
error: z.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: AppContext) {
|
||||
const data = await this.getValidatedData<typeof this.schema>();
|
||||
const { base64Data, mimeType } = data.body;
|
||||
|
||||
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",
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
const generatedText = res.candidates?.[0]?.content?.parts?.[0]
|
||||
?.text;
|
||||
if (!generatedText) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Failed to generate text.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
altText: generatedText,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
message: e,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import { Bool, OpenAPIRoute } from "chanfana";
|
||||
import { z } from "zod";
|
||||
import { type AppContext, Task } from "../types";
|
||||
|
||||
export class TaskCreate extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ["Tasks"],
|
||||
summary: "Create a new Task",
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Task,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Returns the created task",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
series: z.object({
|
||||
success: Bool(),
|
||||
result: z.object({
|
||||
task: Task,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: AppContext) {
|
||||
// Get validated data
|
||||
const data = await this.getValidatedData<typeof this.schema>();
|
||||
|
||||
// Retrieve the validated request body
|
||||
const taskToCreate = data.body;
|
||||
|
||||
// Implement your own object insertion here
|
||||
|
||||
// return the new task
|
||||
return {
|
||||
success: true,
|
||||
task: {
|
||||
name: taskToCreate.name,
|
||||
slug: taskToCreate.slug,
|
||||
description: taskToCreate.description,
|
||||
completed: taskToCreate.completed,
|
||||
due_date: taskToCreate.due_date,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
import { Bool, OpenAPIRoute, Str } from "chanfana";
|
||||
import { z } from "zod";
|
||||
import { type AppContext, Task } from "../types";
|
||||
|
||||
export class TaskDelete extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ["Tasks"],
|
||||
summary: "Delete a Task",
|
||||
request: {
|
||||
params: z.object({
|
||||
taskSlug: Str({ description: "Task slug" }),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Returns if the task was deleted successfully",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
series: z.object({
|
||||
success: Bool(),
|
||||
result: z.object({
|
||||
task: Task,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: AppContext) {
|
||||
// Get validated data
|
||||
const data = await this.getValidatedData<typeof this.schema>();
|
||||
|
||||
// Retrieve the validated slug
|
||||
const { taskSlug } = data.params;
|
||||
|
||||
// Implement your own object deletion here
|
||||
|
||||
// Return the deleted task for confirmation
|
||||
return {
|
||||
result: {
|
||||
task: {
|
||||
name: "Build something awesome with Cloudflare Workers",
|
||||
slug: taskSlug,
|
||||
description: "Lorem Ipsum",
|
||||
completed: true,
|
||||
due_date: "2022-12-24",
|
||||
},
|
||||
},
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
import { Bool, OpenAPIRoute, Str } from "chanfana";
|
||||
import { z } from "zod";
|
||||
import { type AppContext, Task } from "../types";
|
||||
|
||||
export class TaskFetch extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ["Tasks"],
|
||||
summary: "Get a single Task by slug",
|
||||
request: {
|
||||
params: z.object({
|
||||
taskSlug: Str({ description: "Task slug" }),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Returns a single task if found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
series: z.object({
|
||||
success: Bool(),
|
||||
result: z.object({
|
||||
task: Task,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
"404": {
|
||||
description: "Task not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
series: z.object({
|
||||
success: Bool(),
|
||||
error: Str(),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: AppContext) {
|
||||
// Get validated data
|
||||
const data = await this.getValidatedData<typeof this.schema>();
|
||||
|
||||
// Retrieve the validated slug
|
||||
const { taskSlug } = data.params;
|
||||
|
||||
// Implement your own object fetch here
|
||||
|
||||
const exists = true;
|
||||
|
||||
// @ts-ignore: check if the object exists
|
||||
if (exists === false) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Object not found",
|
||||
},
|
||||
{
|
||||
status: 404,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
task: {
|
||||
name: "my task",
|
||||
slug: taskSlug,
|
||||
description: "this needs to be done",
|
||||
completed: false,
|
||||
due_date: new Date().toISOString().slice(0, 10),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
import { Bool, Num, OpenAPIRoute } from "chanfana";
|
||||
import { z } from "zod";
|
||||
import { type AppContext, Task } from "../types";
|
||||
|
||||
export class TaskList extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ["Tasks"],
|
||||
summary: "List Tasks",
|
||||
request: {
|
||||
query: z.object({
|
||||
page: Num({
|
||||
description: "Page number",
|
||||
default: 0,
|
||||
}),
|
||||
isCompleted: Bool({
|
||||
description: "Filter by completed flag",
|
||||
required: false,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Returns a list of tasks",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
series: z.object({
|
||||
success: Bool(),
|
||||
result: z.object({
|
||||
tasks: Task.array(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: AppContext) {
|
||||
// Get validated data
|
||||
const data = await this.getValidatedData<typeof this.schema>();
|
||||
|
||||
// Retrieve the validated parameters
|
||||
const { page, isCompleted } = data.query;
|
||||
|
||||
// Implement your own object list here
|
||||
|
||||
return {
|
||||
success: true,
|
||||
tasks: [
|
||||
{
|
||||
name: "Clean my room",
|
||||
slug: "clean-room",
|
||||
description: null,
|
||||
completed: false,
|
||||
due_date: "2025-01-05",
|
||||
},
|
||||
{
|
||||
name: "Build something awesome with Cloudflare Workers",
|
||||
slug: "cloudflare-workers",
|
||||
description: "Lorem Ipsum",
|
||||
completed: true,
|
||||
due_date: "2022-12-24",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
76
src/index.ts
76
src/index.ts
|
|
@ -1,26 +1,70 @@
|
|||
import { fromHono } from "chanfana";
|
||||
import { cors } from "hono/cors";
|
||||
import { Hono } from "hono";
|
||||
import { TaskCreate } from "./endpoints/taskCreate";
|
||||
import { TaskDelete } from "./endpoints/taskDelete";
|
||||
import { TaskFetch } from "./endpoints/taskFetch";
|
||||
import { TaskList } from "./endpoints/taskList";
|
||||
|
||||
// Start a Hono app
|
||||
const app = new Hono<{ Bindings: Env }>();
|
||||
import { CondenseTextEndpoint } from "./endpoints/condense_text";
|
||||
import { GenerateEndpoint } from "./endpoints/generate";
|
||||
import { geminiMiddleware } from "./middleware/gemini";
|
||||
import { authMiddleware } from "./middleware/auth";
|
||||
import { Env, Variables } from "./types";
|
||||
|
||||
const app = new Hono<{ Bindings: Env; Variables: Variables }>();
|
||||
const root = "/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;
|
||||
},
|
||||
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allowHeaders: ["*"],
|
||||
maxAge: 600,
|
||||
credentials: true,
|
||||
}),
|
||||
);
|
||||
|
||||
app.use(root + "/generate", authMiddleware);
|
||||
app.use(root + "/condense_text", authMiddleware);
|
||||
app.use("*", geminiMiddleware);
|
||||
|
||||
// Setup OpenAPI registry
|
||||
const openapi = fromHono(app, {
|
||||
docs_url: "/",
|
||||
schema: {
|
||||
info: {
|
||||
title: "Alt Text Generator",
|
||||
description:
|
||||
"Endpoints for my Bluesky alt text generator browser extension.",
|
||||
version: "1.0",
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: "https://indexx.dev/api/altText",
|
||||
},
|
||||
],
|
||||
},
|
||||
docs_url: root + "/",
|
||||
openapi_url: root + "/openapi.json",
|
||||
});
|
||||
|
||||
// Register OpenAPI endpoints
|
||||
openapi.get("/api/tasks", TaskList);
|
||||
openapi.post("/api/tasks", TaskCreate);
|
||||
openapi.get("/api/tasks/:taskSlug", TaskFetch);
|
||||
openapi.delete("/api/tasks/:taskSlug", TaskDelete);
|
||||
openapi.registry.registerComponent("securitySchemes", "apiKey", {
|
||||
type: "apiKey",
|
||||
name: "Authorization",
|
||||
in: "header",
|
||||
});
|
||||
|
||||
// You may also register routes for non OpenAPI directly on Hono
|
||||
// app.get('/test', (c) => c.text('Hono!'))
|
||||
openapi.post(root + "/generate", GenerateEndpoint);
|
||||
openapi.post(root + "/condense_text", CondenseTextEndpoint);
|
||||
|
||||
// Export the Hono app
|
||||
export default app;
|
||||
|
|
|
|||
27
src/middleware/auth.ts
Normal file
27
src/middleware/auth.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { Context, Next } from "hono";
|
||||
import { Env } from "../types";
|
||||
|
||||
export async function authMiddleware(
|
||||
c: Context<{ Bindings: Env }>,
|
||||
next: Next,
|
||||
) {
|
||||
const authToken = c.req.header("Authorization");
|
||||
|
||||
if (!authToken) {
|
||||
c.status(401);
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "No authentication token provided.",
|
||||
});
|
||||
}
|
||||
|
||||
if (authToken !== c.env.AUTH_TOKEN) {
|
||||
c.status(403);
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "Authentication token provided is invalid.",
|
||||
});
|
||||
}
|
||||
|
||||
await next();
|
||||
}
|
||||
22
src/middleware/gemini.ts
Normal file
22
src/middleware/gemini.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { GoogleGenAI } from "@google/genai";
|
||||
import { Context, Next } from "hono";
|
||||
import { Env, Variables } from "../types";
|
||||
|
||||
export async function geminiMiddleware(
|
||||
c: Context<{ Bindings: Env; Variables: Variables }>,
|
||||
next: Next,
|
||||
) {
|
||||
if (!c.env || !c.env.GEMINI_API_KEY) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "Gemini API key is not specified in worker secrets.",
|
||||
}, 500);
|
||||
}
|
||||
|
||||
const genAI = new GoogleGenAI({
|
||||
apiKey: c.env.GEMINI_API_KEY,
|
||||
});
|
||||
c.set("gemini", genAI);
|
||||
|
||||
await next();
|
||||
}
|
||||
12
src/types.ts
12
src/types.ts
|
|
@ -1,8 +1,18 @@
|
|||
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 }>;
|
||||
export type AppContext = Context<{ Bindings: Env; Variables: Variables }>;
|
||||
|
||||
export interface Env {
|
||||
AUTH_TOKEN: string;
|
||||
GEMINI_API_KEY: string;
|
||||
}
|
||||
|
||||
export type Variables = {
|
||||
gemini: GoogleGenAI;
|
||||
};
|
||||
|
||||
export const Task = z.object({
|
||||
name: Str({ example: "lorem" }),
|
||||
|
|
|
|||
4
worker-configuration.d.ts
vendored
4
worker-configuration.d.ts
vendored
|
|
@ -1,8 +1,10 @@
|
|||
/* eslint-disable */
|
||||
// Generated by Wrangler by running `wrangler types` (hash: 869ac3b4ce0f52ba3b2e0bc70c49089e)
|
||||
// Generated by Wrangler by running `wrangler types` (hash: adb357211f6292feb98b54ff11ac8fca)
|
||||
// Runtime types generated with workerd@1.20250525.0 2025-06-07
|
||||
declare namespace Cloudflare {
|
||||
interface Env {
|
||||
AUTH_TOKEN: string;
|
||||
GEMINI_API_KEY: string;
|
||||
}
|
||||
}
|
||||
interface Env extends Cloudflare.Env {}
|
||||
|
|
|
|||
|
|
@ -1,47 +1,12 @@
|
|||
/**
|
||||
* For more details on how to configure Wrangler, refer to:
|
||||
* https://developers.cloudflare.com/workers/wrangler/configuration/
|
||||
*/
|
||||
{
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "bluesky-alt-text-worker",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-06-07",
|
||||
"observability": {
|
||||
"enabled": true
|
||||
"enabled": true,
|
||||
"logs": {
|
||||
"invocation_logs": true
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Smart Placement
|
||||
* Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
|
||||
*/
|
||||
// "placement": { "mode": "smart" },
|
||||
|
||||
/**
|
||||
* Bindings
|
||||
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
|
||||
* databases, object storage, AI inference, real-time communication and more.
|
||||
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
|
||||
*/
|
||||
|
||||
/**
|
||||
* Environment Variables
|
||||
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
|
||||
*/
|
||||
// "vars": { "MY_VARIABLE": "production_value" },
|
||||
/**
|
||||
* Note: Use secrets to store sensitive data.
|
||||
* https://developers.cloudflare.com/workers/configuration/secrets/
|
||||
*/
|
||||
|
||||
/**
|
||||
* Static Assets
|
||||
* https://developers.cloudflare.com/workers/static-assets/binding/
|
||||
*/
|
||||
// "assets": { "directory": "./public/", "binding": "ASSETS" },
|
||||
|
||||
/**
|
||||
* Service Bindings (communicate between multiple Workers)
|
||||
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
|
||||
*/
|
||||
// "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue