Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
2 changes: 2 additions & 0 deletions apps/backend/.env
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,5 @@ STACK_OPENAI_API_KEY=# enter your openai api key
STACK_FEATUREBASE_API_KEY=# enter your featurebase api key
STACK_STRIPE_SECRET_KEY=# enter your stripe api key
STACK_STRIPE_WEBHOOK_SECRET=# enter your stripe webhook secret
STACK_TELEGRAM_BOT_TOKEN= # enter you telegram bot token
STACK_TELEGRAM_CHAT_ID=# enter your telegram chat id
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { adaptSchema, yupArray, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
import { InferType } from "yup";

const TELEGRAM_HOSTNAME = "api.telegram.org";
const TELEGRAM_ENDPOINT_PATH = "/sendMessage";
const STACK_TRACE_MAX_LENGTH = 4000;
const MESSAGE_PREFIX = "_".repeat(50);


const completionPayloadSchema = yupObject({
success: yupBoolean().defined(),
distinctId: yupString().optional(),
options: adaptSchema.defined(),
args: yupArray(yupString().defined()).defined(),
isNonInteractive: yupBoolean().defined(),
timestamp: yupString().defined(),
projectPath: yupString().optional(),
error: yupObject({
name: yupString().optional(),
message: yupString().defined(),
stack: yupString().optional(),
}).optional(),
}).defined();

export const POST = createSmartRouteHandler({
request: yupObject({
auth: yupObject({
type: adaptSchema,
user: adaptSchema,
project: adaptSchema,
}).nullable(),
body: completionPayloadSchema,
method: yupString().oneOf(["POST"]).defined(),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({
success: yupBoolean().oneOf([true]).defined(),
}).defined(),
}),
async handler({ body }) {
const botToken = getEnvVariable("STACK_TELEGRAM_BOT_TOKEN", "");
const chatId = getEnvVariable("STACK_TELEGRAM_CHAT_ID", "");

if (!botToken || !chatId) {
throw new StackAssertionError("Telegram integration is not configured.");
}

const message = buildMessage(body);
await postToTelegram({ botToken, chatId, message });

return {
statusCode: 200,
bodyType: "json",
body: {
success: true,
},
};
},
});

function buildMessage(payload: InferType<typeof completionPayloadSchema>): string {
const { success, distinctId, options, args, isNonInteractive, timestamp, projectPath, error } = payload;
const status = success ? "[SUCCESS]" : "[FAILURE]";
const optionSummary = safeJson(options);
const argSummary = args.length ? safeJson(args) : "[]";
const errorSummary = error?.message ? `${error.name ? `${error.name}: ` : ""}${error.message}` : "none";

const lines = [
`Stack init completed ${status}`,
`Timestamp: ${timestamp}`,
distinctId ? `DistinctId: ${distinctId}` : undefined,
`NonInteractiveEnv: ${isNonInteractive}`,
projectPath ? `ProjectPath: ${projectPath}` : undefined,
`Options: ${optionSummary}`,
`Args: ${argSummary}`,
`Error: ${errorSummary}`,
].filter((line): line is string => Boolean(line));

if (error?.stack) {
lines.push(`Stack: ${truncate(error.stack, STACK_TRACE_MAX_LENGTH)}`);
}

return `${MESSAGE_PREFIX}\n\n${lines.join("\n")}`;
}

async function postToTelegram({ botToken, chatId, message }: { botToken: string, chatId: string, message: string }): Promise<void> {
const response = await fetch(`https://${TELEGRAM_HOSTNAME}/bot${botToken}${TELEGRAM_ENDPOINT_PATH}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
chat_id: chatId,
text: message,
}),
});

if (!response.ok) {
const body = await safeReadBody(response);
throw new StackAssertionError("Failed to send Telegram notification.", {
status: response.status,
body,
});
}
}

async function safeReadBody(response: Response): Promise<string | undefined> {
try {
return await response.text();
} catch {
return undefined;
}
}

function safeJson(value: unknown): string {
try {
return JSON.stringify(value, null, 2);
} catch {
return "[unserializable]";
}
}

function truncate(value: string, maxLength: number): string {
if (value.length <= maxLength) {
return value;
}
return `${value.slice(0, maxLength - 3)}...`;
}
2 changes: 1 addition & 1 deletion packages/init-stack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"lint": "eslint --ext .tsx,.ts .",
"typecheck": "tsc --noEmit",
"init-stack": "node dist/index.js",
"init-stack:local": "STACK_NEXT_INSTALL_PACKAGE_NAME_OVERRIDE=../../stack STACK_JS_INSTALL_PACKAGE_NAME_OVERRIDE=../../js STACK_REACT_INSTALL_PACKAGE_NAME_OVERRIDE=../../react node dist/index.js",
"init-stack:local": "STACK_INIT_API_BASE_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02 STACK_NEXT_INSTALL_PACKAGE_NAME_OVERRIDE=../../stack STACK_JS_INSTALL_PACKAGE_NAME_OVERRIDE=../../js STACK_REACT_INSTALL_PACKAGE_NAME_OVERRIDE=../../react node dist/index.js",
"test-run": "pnpm run build && pnpm run test-run-js && pnpm run test-run-node && pnpm run test-run-next && pnpm run test-run-neon && pnpm run test-run-no-browser",
"test-run:manual": "pnpm run build && pnpm run test-run-js:manual && pnpm run test-run-node:manual && pnpm run test-run-next:manual && pnpm run test-run-neon:manual",
"ensure-neon": "grep -q '\"@neondatabase/serverless\"' ./test-run-output/package.json && echo 'Initialized Neon successfully!'",
Expand Down
34 changes: 34 additions & 0 deletions packages/init-stack/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as os from 'os';
import * as path from "path";
import { PostHog } from 'posthog-node';
import packageJson from '../package.json';
import { invokeCallback } from "./telegram";
import { scheduleMcpConfiguration } from "./mcp";
import { Colorize, configureVerboseLogging, logVerbose, templateIdentity } from "./util";

Expand Down Expand Up @@ -420,6 +421,16 @@ async function main(): Promise<void> {
commandsExecuted,
});

await invokeCallback({
success: true,
distinctId,
options,
args: program.args,
isNonInteractive: isNonInteractiveEnv(),
timestamp: new Date().toISOString(),
projectPath,
});

// Success!
console.log(`
${colorize.green`===============================================`}
Expand Down Expand Up @@ -474,6 +485,29 @@ main().catch(async (err) => {
console.error(`Error message: ${err.message}`);
}
console.error();
const fallbackErrorMessage = (() => {
if (err instanceof Error) return err.message;
if (typeof err === "string") return err;
try {
return JSON.stringify(err);
} catch {
return "Unknown error";
}
})();
await invokeCallback({
success: false,
distinctId,
options,
args: program.args,
isNonInteractive: isNonInteractiveEnv(),
timestamp: new Date().toISOString(),
projectPath: savedProjectPath,
error: {
name: err instanceof Error ? err.name : undefined,
message: fallbackErrorMessage,
stack: err instanceof Error ? err.stack : undefined,
},
});
await ph_client.shutdown();
process.exit(1);
});
Expand Down
31 changes: 31 additions & 0 deletions packages/init-stack/src/telegram.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
type TelegramErrorInfo = {
name?: string,
message: string,
stack?: string,
};

export type TelegramCompletionPayload = {
success: boolean,
distinctId?: string,
options: Record<string, unknown>,
args: string[],
isNonInteractive: boolean,
timestamp: string,
projectPath?: string,
error?: TelegramErrorInfo,
};

const API_BASE_ENV = "STACK_INIT_API_BASE_URL";
const DEFAULT_API_BASE_URL = "https://api.stack-auth.com";
const CALLBACK_ENDPOINT = "/api/latest/internal/init-script-callback";

export async function invokeCallback(payload: TelegramCompletionPayload): Promise<void> {
const baseUrl = process.env[API_BASE_ENV] ?? DEFAULT_API_BASE_URL;
await fetch(`${baseUrl}${CALLBACK_ENDPOINT}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
}