Spaces:
Sleeping
Sleeping
| import { TRPCError } from "@trpc/server"; | |
| import { ENV } from "./env"; | |
| export type NotificationPayload = { | |
| title: string; | |
| content: string; | |
| }; | |
| const TITLE_MAX_LENGTH = 1200; | |
| const CONTENT_MAX_LENGTH = 20000; | |
| const trimValue = (value: string): string => value.trim(); | |
| const isNonEmptyString = (value: unknown): value is string => | |
| typeof value === "string" && value.trim().length > 0; | |
| const buildEndpointUrl = (baseUrl: string): string => { | |
| const normalizedBase = baseUrl.endsWith("/") | |
| ? baseUrl | |
| : `${baseUrl}/`; | |
| return new URL( | |
| "webdevtoken.v1.WebDevService/SendNotification", | |
| normalizedBase | |
| ).toString(); | |
| }; | |
| const validatePayload = (input: NotificationPayload): NotificationPayload => { | |
| if (!isNonEmptyString(input.title)) { | |
| throw new TRPCError({ | |
| code: "BAD_REQUEST", | |
| message: "Notification title is required.", | |
| }); | |
| } | |
| if (!isNonEmptyString(input.content)) { | |
| throw new TRPCError({ | |
| code: "BAD_REQUEST", | |
| message: "Notification content is required.", | |
| }); | |
| } | |
| const title = trimValue(input.title); | |
| const content = trimValue(input.content); | |
| if (title.length > TITLE_MAX_LENGTH) { | |
| throw new TRPCError({ | |
| code: "BAD_REQUEST", | |
| message: `Notification title must be at most ${TITLE_MAX_LENGTH} characters.`, | |
| }); | |
| } | |
| if (content.length > CONTENT_MAX_LENGTH) { | |
| throw new TRPCError({ | |
| code: "BAD_REQUEST", | |
| message: `Notification content must be at most ${CONTENT_MAX_LENGTH} characters.`, | |
| }); | |
| } | |
| return { title, content }; | |
| }; | |
| /** | |
| * Dispatches a project-owner notification through the Manus Notification Service. | |
| * Returns `true` if the request was accepted, `false` when the upstream service | |
| * cannot be reached (callers can fall back to email/slack). Validation errors | |
| * bubble up as TRPC errors so callers can fix the payload. | |
| */ | |
| export async function notifyOwner( | |
| payload: NotificationPayload | |
| ): Promise<boolean> { | |
| const { title, content } = validatePayload(payload); | |
| if (!ENV.forgeApiUrl) { | |
| throw new TRPCError({ | |
| code: "INTERNAL_SERVER_ERROR", | |
| message: "Notification service URL is not configured.", | |
| }); | |
| } | |
| if (!ENV.forgeApiKey) { | |
| throw new TRPCError({ | |
| code: "INTERNAL_SERVER_ERROR", | |
| message: "Notification service API key is not configured.", | |
| }); | |
| } | |
| const endpoint = buildEndpointUrl(ENV.forgeApiUrl); | |
| try { | |
| const response = await fetch(endpoint, { | |
| method: "POST", | |
| headers: { | |
| accept: "application/json", | |
| authorization: `Bearer ${ENV.forgeApiKey}`, | |
| "content-type": "application/json", | |
| "connect-protocol-version": "1", | |
| }, | |
| body: JSON.stringify({ title, content }), | |
| }); | |
| if (!response.ok) { | |
| const detail = await response.text().catch(() => ""); | |
| console.warn( | |
| `[Notification] Failed to notify owner (${response.status} ${response.statusText})${ | |
| detail ? `: ${detail}` : "" | |
| }` | |
| ); | |
| return false; | |
| } | |
| return true; | |
| } catch (error) { | |
| console.warn("[Notification] Error calling notification service:", error); | |
| return false; | |
| } | |
| } | |