@percepta/create 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -0
- package/dist/chunk-GEVZERMP.js +108 -0
- package/dist/chunk-R4FWPE4A.js +49 -0
- package/dist/chunk-WMJT7CB5.js +57 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +974 -0
- package/dist/init-Z4VGBHAK.js +96 -0
- package/dist/status-MITGDLTT.js +76 -0
- package/dist/sync-J4SFZHDX.js +136 -0
- package/dist/upstream-AQI7P4EU.js +144 -0
- package/package.json +58 -0
- package/template-versions.json +4 -0
- package/templates/library/README.md +30 -0
- package/templates/library/eslint.config.js +10 -0
- package/templates/library/gitignore.template +18 -0
- package/templates/library/package.json.template +29 -0
- package/templates/library/src/index.ts +9 -0
- package/templates/library/tsconfig.json +19 -0
- package/templates/monorepo/README.md +41 -0
- package/templates/monorepo/eslint.config.js +10 -0
- package/templates/monorepo/gitignore.template +31 -0
- package/templates/monorepo/npmrc.template +4 -0
- package/templates/monorepo/package.json.template +25 -0
- package/templates/monorepo/packages/.gitkeep +0 -0
- package/templates/monorepo/pnpm-workspace.yaml +2 -0
- package/templates/monorepo/tsconfig.json +16 -0
- package/templates/webapp/.claude/commands/sync.md +19 -0
- package/templates/webapp/.claude/commands/upstream.md +17 -0
- package/templates/webapp/.dockerignore +59 -0
- package/templates/webapp/.gitattributes +1 -0
- package/templates/webapp/.github/workflows/__APP_NAME__-ryvn-release.yaml +114 -0
- package/templates/webapp/.github/workflows/__APP_NAME__-terraform.yml +28 -0
- package/templates/webapp/.github/workflows/ci.yml +149 -0
- package/templates/webapp/.node-version +2 -0
- package/templates/webapp/.prettierrc.mjs +5 -0
- package/templates/webapp/AGENTS.md +240 -0
- package/templates/webapp/Dockerfile +64 -0
- package/templates/webapp/README.md +200 -0
- package/templates/webapp/agent-skills/database.md +140 -0
- package/templates/webapp/agent-skills/deploy.md +94 -0
- package/templates/webapp/agent-skills/inngest.md +147 -0
- package/templates/webapp/agent-skills/langfuse.md +117 -0
- package/templates/webapp/agent-skills/oneshot.md +216 -0
- package/templates/webapp/agent-skills/ryvn.md +25 -0
- package/templates/webapp/deploy/README.md +39 -0
- package/templates/webapp/deploy/ryvn/__APP_NAME__.service.yaml +11 -0
- package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml +121 -0
- package/templates/webapp/docker-compose.yml +19 -0
- package/templates/webapp/drizzle.config.ts +30 -0
- package/templates/webapp/env.example.template +44 -0
- package/templates/webapp/eslint.config.mjs +52 -0
- package/templates/webapp/gitignore.template +53 -0
- package/templates/webapp/next.config.ts +8 -0
- package/templates/webapp/npmrc.template +4 -0
- package/templates/webapp/package.json.template +122 -0
- package/templates/webapp/postcss.config.mjs +5 -0
- package/templates/webapp/scripts/create-user.ts +47 -0
- package/templates/webapp/scripts/migrate.ts +18 -0
- package/templates/webapp/scripts/seed.ts +62 -0
- package/templates/webapp/scripts/setup-database.ts +57 -0
- package/templates/webapp/scripts/setup-readonly-user.ts +193 -0
- package/templates/webapp/scripts/start.sh +52 -0
- package/templates/webapp/src/app/(app)/layout.tsx +21 -0
- package/templates/webapp/src/app/(app)/page.tsx +30 -0
- package/templates/webapp/src/app/(auth)/auth/signin/CredentialsSignInForm.tsx +103 -0
- package/templates/webapp/src/app/(auth)/auth/signin/page.tsx +30 -0
- package/templates/webapp/src/app/(auth)/layout.tsx +15 -0
- package/templates/webapp/src/app/api/auth/[...all]/route.ts +4 -0
- package/templates/webapp/src/app/api/healthz/route.ts +10 -0
- package/templates/webapp/src/app/api/inngest/route.ts +31 -0
- package/templates/webapp/src/app/api/readyz/route.ts +31 -0
- package/templates/webapp/src/app/api/trpc/[trpc]/route.ts +21 -0
- package/templates/webapp/src/app/favicon.ico +0 -0
- package/templates/webapp/src/app/global-error.tsx +27 -0
- package/templates/webapp/src/app/layout.tsx +18 -0
- package/templates/webapp/src/components/FaroProvider.tsx +37 -0
- package/templates/webapp/src/components/Header.tsx +70 -0
- package/templates/webapp/src/components/Providers.tsx +45 -0
- package/templates/webapp/src/components/form/FormItem.tsx +82 -0
- package/templates/webapp/src/config/clientEnvConfig.ts +11 -0
- package/templates/webapp/src/config/getEnvConfig.ts +62 -0
- package/templates/webapp/src/config/isDev.ts +7 -0
- package/templates/webapp/src/drizzle/db.ts +28 -0
- package/templates/webapp/src/drizzle/migrations/0000_eager_grandmaster.sql +57 -0
- package/templates/webapp/src/drizzle/migrations/meta/0000_snapshot.json +376 -0
- package/templates/webapp/src/drizzle/migrations/meta/_journal.json +13 -0
- package/templates/webapp/src/drizzle/schema/auth/accounts.ts +33 -0
- package/templates/webapp/src/drizzle/schema/auth/sessions.ts +25 -0
- package/templates/webapp/src/drizzle/schema/auth/users.ts +38 -0
- package/templates/webapp/src/drizzle/schema/auth/verifications.ts +19 -0
- package/templates/webapp/src/drizzle/schema/index.ts +4 -0
- package/templates/webapp/src/drizzle/schema/utils/jsonbFromZod.ts +25 -0
- package/templates/webapp/src/instrumentation.ts +35 -0
- package/templates/webapp/src/lib/auth/index.ts +85 -0
- package/templates/webapp/src/lib/auth-client.ts +6 -0
- package/templates/webapp/src/lib/trpc.ts +15 -0
- package/templates/webapp/src/server/api/root.ts +5 -0
- package/templates/webapp/src/server/trpc.ts +61 -0
- package/templates/webapp/src/services/AuthContextService.ts +63 -0
- package/templates/webapp/src/services/DatabaseService.ts +54 -0
- package/templates/webapp/src/services/inngest/InngestFunctionCollection.ts +5 -0
- package/templates/webapp/src/services/inngest/InngestService.ts +71 -0
- package/templates/webapp/src/services/inngest/events/AppEvents.ts +34 -0
- package/templates/webapp/src/services/inngest/events/payloads/ExampleEventPayload.ts +14 -0
- package/templates/webapp/src/services/langfuse/LangfuseService.ts +80 -0
- package/templates/webapp/src/services/logger/AppLogger.ts +61 -0
- package/templates/webapp/src/services/logger/withRequestContext.ts +27 -0
- package/templates/webapp/src/services/observability/initFaro.ts +22 -0
- package/templates/webapp/src/startup-checks.ts +32 -0
- package/templates/webapp/src/styles/globals.css +27 -0
- package/templates/webapp/src/utils/__tests__/cn.test.ts +20 -0
- package/templates/webapp/src/utils/cn.ts +6 -0
- package/templates/webapp/src/utils/syncInngestApp.ts +62 -0
- package/templates/webapp/terraform/README.md +147 -0
- package/templates/webapp/terraform/deploy.sh +97 -0
- package/templates/webapp/terraform/main.tf +101 -0
- package/templates/webapp/terraform/modules/cloudtrail/main.tf +27 -0
- package/templates/webapp/terraform/modules/cloudtrail/outputs.tf +10 -0
- package/templates/webapp/terraform/modules/cloudtrail/variables.tf +15 -0
- package/templates/webapp/terraform/modules/networking/main.tf +118 -0
- package/templates/webapp/terraform/modules/networking/outputs.tf +38 -0
- package/templates/webapp/terraform/modules/networking/variables.tf +24 -0
- package/templates/webapp/terraform/modules/rds/main.tf +227 -0
- package/templates/webapp/terraform/modules/rds/outputs.tf +73 -0
- package/templates/webapp/terraform/modules/rds/variables.tf +61 -0
- package/templates/webapp/terraform/modules/s3-logging/main.tf +148 -0
- package/templates/webapp/terraform/modules/s3-logging/outputs.tf +10 -0
- package/templates/webapp/terraform/modules/s3-logging/variables.tf +16 -0
- package/templates/webapp/terraform/modules/secrets/main.tf +39 -0
- package/templates/webapp/terraform/modules/secrets/outputs.tf +9 -0
- package/templates/webapp/terraform/modules/secrets/variables.tf +51 -0
- package/templates/webapp/terraform/outputs.tf +102 -0
- package/templates/webapp/terraform/providers.tf +32 -0
- package/templates/webapp/terraform/terraform.tfvars.example +65 -0
- package/templates/webapp/terraform/variables.tf +129 -0
- package/templates/webapp/tsconfig.json +14 -0
- package/templates/webapp/vitest.config.ts +9 -0
- package/templates/webapp/vitest.setup.ts +5 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { TRPCError, initTRPC } from "@trpc/server";
|
|
2
|
+
import { headers } from "next/headers";
|
|
3
|
+
import superjson from "superjson";
|
|
4
|
+
import { type BetterAuthSession, auth } from "../lib/auth";
|
|
5
|
+
import { AuthContextService } from "../services/AuthContextService";
|
|
6
|
+
import { getTracer } from "../services/logger/AppLogger";
|
|
7
|
+
|
|
8
|
+
export interface Context {
|
|
9
|
+
session: BetterAuthSession | null;
|
|
10
|
+
services: {
|
|
11
|
+
authContext: AuthContextService;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ProtectedContext extends Context {
|
|
16
|
+
session: BetterAuthSession;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function createContext(): Promise<Context> {
|
|
20
|
+
const session = await auth.api.getSession({
|
|
21
|
+
headers: await headers(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const services: Context["services"] = {
|
|
25
|
+
authContext: AuthContextService.create(),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return { session, services };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const { router, procedure } = initTRPC.context<Context>().create({
|
|
32
|
+
transformer: superjson,
|
|
33
|
+
errorFormatter({ shape }) {
|
|
34
|
+
const tracer = getTracer();
|
|
35
|
+
return {
|
|
36
|
+
...shape,
|
|
37
|
+
data: {
|
|
38
|
+
...(tracer != null && { traceId: tracer.traceId }),
|
|
39
|
+
...shape.data,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const protectedProcedure = procedure.use(
|
|
46
|
+
({ ctx, next }): ReturnType<typeof next<ProtectedContext>> => {
|
|
47
|
+
const {
|
|
48
|
+
session,
|
|
49
|
+
services: { authContext },
|
|
50
|
+
} = ctx;
|
|
51
|
+
if (session?.user == null) {
|
|
52
|
+
throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return authContext.withSession(session, () =>
|
|
56
|
+
next({
|
|
57
|
+
ctx,
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
},
|
|
61
|
+
);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { trace } from "@opentelemetry/api";
|
|
2
|
+
import { TRPCError } from "@trpc/server";
|
|
3
|
+
import type { BetterAuthSession } from "../lib/auth";
|
|
4
|
+
|
|
5
|
+
export type AuthSession = BetterAuthSession;
|
|
6
|
+
|
|
7
|
+
export class AuthContextService {
|
|
8
|
+
private static SINGLETON: AuthContextService | undefined;
|
|
9
|
+
public static create(): AuthContextService {
|
|
10
|
+
if (AuthContextService.SINGLETON == null) {
|
|
11
|
+
AuthContextService.SINGLETON = new AuthContextService();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return AuthContextService.SINGLETON;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private authAsyncLocalStorage = new AsyncLocalStorage<LocalStorage>();
|
|
18
|
+
|
|
19
|
+
private constructor() {}
|
|
20
|
+
|
|
21
|
+
public withSession<TReturn>(
|
|
22
|
+
session: AuthSession,
|
|
23
|
+
callback: () => TReturn,
|
|
24
|
+
): TReturn {
|
|
25
|
+
// Set user email on the active OpenTelemetry span
|
|
26
|
+
const activeSpan = trace.getActiveSpan();
|
|
27
|
+
if (
|
|
28
|
+
activeSpan != null &&
|
|
29
|
+
session.user.email != null &&
|
|
30
|
+
session.user.id != null
|
|
31
|
+
) {
|
|
32
|
+
activeSpan.setAttribute("user.email", session.user.email);
|
|
33
|
+
activeSpan.setAttribute("user.id", session.user.id);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return this.authAsyncLocalStorage.run<TReturn>({ session }, () =>
|
|
37
|
+
callback(),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public getSession(): AuthSession | undefined {
|
|
42
|
+
const context = this.authAsyncLocalStorage.getStore();
|
|
43
|
+
if (context == null) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { session } = context;
|
|
48
|
+
return session;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public getSessionOrThrow(): AuthSession {
|
|
52
|
+
const session = this.getSession();
|
|
53
|
+
if (session == null) {
|
|
54
|
+
throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return session;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface LocalStorage {
|
|
62
|
+
session: AuthSession;
|
|
63
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { type NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
3
|
+
import { db } from "../drizzle/db";
|
|
4
|
+
import type * as schema from "../drizzle/schema";
|
|
5
|
+
|
|
6
|
+
export class DatabaseService {
|
|
7
|
+
private static SINGLETON: DatabaseService | undefined;
|
|
8
|
+
public static create(): DatabaseService {
|
|
9
|
+
if (DatabaseService.SINGLETON == null) {
|
|
10
|
+
DatabaseService.SINGLETON = new DatabaseService(db);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return DatabaseService.SINGLETON;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private transactionAsyncLocalStorage = new AsyncLocalStorage<LocalStorage>();
|
|
17
|
+
|
|
18
|
+
private constructor(private database: NodePgDatabase<typeof schema>) {}
|
|
19
|
+
|
|
20
|
+
public async createTransaction<TReturn>(
|
|
21
|
+
callback: (txn: NodePgDatabase<typeof schema>) => Promise<TReturn>,
|
|
22
|
+
): Promise<TReturn> {
|
|
23
|
+
const currentContext = this.transactionAsyncLocalStorage.getStore();
|
|
24
|
+
if (currentContext != null) {
|
|
25
|
+
const { txn } = currentContext;
|
|
26
|
+
|
|
27
|
+
// Already in a transaction.
|
|
28
|
+
return callback(txn);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return this.database.transaction((txn) => {
|
|
32
|
+
return this.transactionAsyncLocalStorage.run<Promise<TReturn>>(
|
|
33
|
+
{ txn },
|
|
34
|
+
() => callback(txn),
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public getDatabase(): Database {
|
|
40
|
+
const context = this.transactionAsyncLocalStorage.getStore();
|
|
41
|
+
if (context == null) {
|
|
42
|
+
return this.database;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { txn } = context;
|
|
46
|
+
return txn;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type Database = Omit<NodePgDatabase<typeof schema>, "transaction">;
|
|
51
|
+
|
|
52
|
+
interface LocalStorage {
|
|
53
|
+
txn: NodePgDatabase<typeof schema>;
|
|
54
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { validationMiddleware } from "@inngest/middleware-validation";
|
|
2
|
+
import { EventSchemas, Inngest } from "inngest";
|
|
3
|
+
import { getEnvConfig } from "../../config/getEnvConfig";
|
|
4
|
+
import { AppEvents } from "./events/AppEvents";
|
|
5
|
+
|
|
6
|
+
export interface PollOpts {
|
|
7
|
+
maxAttempts: number;
|
|
8
|
+
delayMs: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AppInngest extends AppInngestInternal {
|
|
12
|
+
// Externally, we should never call createFunction on the client interface.
|
|
13
|
+
createFunction: never;
|
|
14
|
+
}
|
|
15
|
+
type AppInngestInternal = Inngest<{
|
|
16
|
+
id: string;
|
|
17
|
+
schemas: typeof EVENT_SCHEMAS;
|
|
18
|
+
}>;
|
|
19
|
+
|
|
20
|
+
export class InngestService {
|
|
21
|
+
private static SINGLETON: InngestService | undefined;
|
|
22
|
+
public static create(): InngestService {
|
|
23
|
+
if (InngestService.SINGLETON == null) {
|
|
24
|
+
InngestService.SINGLETON = new InngestService();
|
|
25
|
+
}
|
|
26
|
+
return InngestService.SINGLETON;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private _signingKey: string | undefined;
|
|
30
|
+
private inngest: AppInngestInternal;
|
|
31
|
+
|
|
32
|
+
private constructor() {
|
|
33
|
+
const {
|
|
34
|
+
INNGEST_BASE_URL: baseUrl,
|
|
35
|
+
INNGEST_SIGNING_KEY: signingKey,
|
|
36
|
+
INNGEST_EVENT_KEY: eventKey,
|
|
37
|
+
} = getEnvConfig();
|
|
38
|
+
|
|
39
|
+
if (baseUrl == null) {
|
|
40
|
+
throw new Error("INNGEST_BASE_URL is not set.");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this._signingKey = signingKey;
|
|
44
|
+
this.inngest = new Inngest({
|
|
45
|
+
id: "__APP_NAME__",
|
|
46
|
+
eventKey,
|
|
47
|
+
baseUrl,
|
|
48
|
+
schemas: EVENT_SCHEMAS,
|
|
49
|
+
middleware: [validationMiddleware()],
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public get client(): AppInngest {
|
|
54
|
+
return this.inngest as never;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public get signingKey(): string | undefined {
|
|
58
|
+
return this._signingKey;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public createFunction: Inngest.CreateFunction<AppInngest> = (
|
|
62
|
+
options,
|
|
63
|
+
trigger,
|
|
64
|
+
handler,
|
|
65
|
+
) => {
|
|
66
|
+
// TODO: Add context plumbing here.
|
|
67
|
+
return this.inngest.createFunction(options, trigger, handler) as never;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const EVENT_SCHEMAS = new EventSchemas().fromSchema(AppEvents);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import { ExampleEventPayload } from "./payloads/ExampleEventPayload";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Define all Inngest events for the application here.
|
|
6
|
+
*
|
|
7
|
+
* Each event should have:
|
|
8
|
+
* - A unique name (conventionally "app/event.name")
|
|
9
|
+
* - A Zod schema for the event data
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* export const AppEvents = {
|
|
14
|
+
* "app/user.created": z.object({
|
|
15
|
+
* data: z.object({
|
|
16
|
+
* userId: z.string(),
|
|
17
|
+
* email: z.string(),
|
|
18
|
+
* }),
|
|
19
|
+
* }),
|
|
20
|
+
* "app/order.completed": z.object({
|
|
21
|
+
* data: z.object({
|
|
22
|
+
* orderId: z.string(),
|
|
23
|
+
* total: z.number(),
|
|
24
|
+
* }),
|
|
25
|
+
* }),
|
|
26
|
+
* };
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export const AppEvents = {
|
|
30
|
+
// Example event - replace with your actual events
|
|
31
|
+
"app/example.event": z.object({
|
|
32
|
+
data: ExampleEventPayload.SCHEMA,
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Example event payload schema.
|
|
5
|
+
* Replace this with your actual event payloads.
|
|
6
|
+
*/
|
|
7
|
+
export type ExampleEventPayload = z.infer<typeof ExampleEventPayload.SCHEMA>;
|
|
8
|
+
export namespace ExampleEventPayload {
|
|
9
|
+
export const SCHEMA = z.object({
|
|
10
|
+
// Add your event properties here
|
|
11
|
+
// exampleId: z.string(),
|
|
12
|
+
// data: z.record(z.unknown()),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Langfuse } from "langfuse";
|
|
2
|
+
import { getEnvConfig } from "../../config/getEnvConfig";
|
|
3
|
+
import { getLogger } from "../logger/AppLogger";
|
|
4
|
+
|
|
5
|
+
export class LangfuseService {
|
|
6
|
+
private static SINGLETON: LangfuseService | undefined;
|
|
7
|
+
|
|
8
|
+
public static create(): LangfuseService {
|
|
9
|
+
if (LangfuseService.SINGLETON == null) {
|
|
10
|
+
LangfuseService.SINGLETON = new LangfuseService();
|
|
11
|
+
}
|
|
12
|
+
return LangfuseService.SINGLETON;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private readonly client: Langfuse | undefined;
|
|
16
|
+
|
|
17
|
+
private constructor() {
|
|
18
|
+
const config = getEnvConfig();
|
|
19
|
+
const baseUrl = config.LANGFUSE_BASE_URL;
|
|
20
|
+
const publicKey = config.LANGFUSE_PUBLIC_KEY;
|
|
21
|
+
const secretKey = config.LANGFUSE_SECRET_KEY;
|
|
22
|
+
|
|
23
|
+
if (baseUrl && publicKey && secretKey) {
|
|
24
|
+
this.client = new Langfuse({
|
|
25
|
+
baseUrl,
|
|
26
|
+
publicKey,
|
|
27
|
+
secretKey,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if Langfuse is configured.
|
|
34
|
+
*/
|
|
35
|
+
public isConfigured(): boolean {
|
|
36
|
+
return this.client != null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Update a Langfuse trace with metadata.
|
|
41
|
+
* Uses trace.update() which merges with existing metadata.
|
|
42
|
+
*
|
|
43
|
+
* @param traceId - The trace ID (same as documentPipelineRuns.id)
|
|
44
|
+
* @param metadata - The metadata to save (must be JSON-serializable)
|
|
45
|
+
*/
|
|
46
|
+
public async updateTraceMetadata<T extends object>(
|
|
47
|
+
traceId: string,
|
|
48
|
+
metadata: T,
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
if (!this.isConfigured()) {
|
|
51
|
+
getLogger().warn(
|
|
52
|
+
{ safe: { traceId } },
|
|
53
|
+
"[LangfuseService] Langfuse not configured, skipping trace update",
|
|
54
|
+
);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Capture client after isConfigured() guard for type narrowing
|
|
59
|
+
const client = this.client!;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Attach to existing trace by ID and update metadata
|
|
63
|
+
const trace = client.trace({ id: traceId });
|
|
64
|
+
trace.update({ metadata });
|
|
65
|
+
|
|
66
|
+
// Flush to ensure the update is sent
|
|
67
|
+
await client.flushAsync();
|
|
68
|
+
|
|
69
|
+
getLogger().info(
|
|
70
|
+
{ safe: { traceId } },
|
|
71
|
+
"[LangfuseService] Updated trace with metadata",
|
|
72
|
+
);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
getLogger().error(
|
|
75
|
+
{ safe: { traceId }, unsafe: { error: String(error) } },
|
|
76
|
+
"[LangfuseService] Failed to update trace metadata",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type MosaicLogger,
|
|
3
|
+
createLogFactory,
|
|
4
|
+
createTracerFactory,
|
|
5
|
+
} from "@percepta/logger";
|
|
6
|
+
import { assertNever } from "@percepta/utils";
|
|
7
|
+
import pino, { type Logger } from "pino";
|
|
8
|
+
import pretty from "pino-pretty";
|
|
9
|
+
import { getEnvConfig } from "../../config/getEnvConfig";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The main application logger.
|
|
13
|
+
* Uses Pino under the hood with AsyncLocalStorage for request context.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* getLogger().info({ safe: { key: value } }, "message");
|
|
17
|
+
* getLogger().error({ unsafe: { pii: data } }, "error message", error);
|
|
18
|
+
*
|
|
19
|
+
* All logs automatically include request context (reqId, method, path, host)
|
|
20
|
+
* when called within a request scope established by withLogContext().
|
|
21
|
+
*/
|
|
22
|
+
export const { getLogger, withLogContext } = createLogFactory(
|
|
23
|
+
createBasePinoInstance(),
|
|
24
|
+
() => new AsyncLocalStorage<MosaicLogger>(),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Tracer for request context propagation.
|
|
29
|
+
* Uses AsyncLocalStorage to pass trace IDs through async boundaries.
|
|
30
|
+
*/
|
|
31
|
+
export const { getTracer, withTracer } = createTracerFactory(
|
|
32
|
+
() => new AsyncLocalStorage(),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// TODO(@dzhao): Generalize this and move to createPinoInstance.ts.
|
|
36
|
+
function createBasePinoInstance(): Logger {
|
|
37
|
+
const { NODE_ENV: nodeEnv, LOG_LEVEL: level } = getEnvConfig();
|
|
38
|
+
|
|
39
|
+
switch (nodeEnv) {
|
|
40
|
+
case "production":
|
|
41
|
+
return pino({
|
|
42
|
+
level,
|
|
43
|
+
formatters: {
|
|
44
|
+
level: (label) => ({ level: label }),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
case "development":
|
|
48
|
+
case "test":
|
|
49
|
+
// Development: use pino-pretty synchronously (avoids worker thread issues with Next.js/Turbopack)
|
|
50
|
+
return pino(
|
|
51
|
+
{ level },
|
|
52
|
+
pretty({
|
|
53
|
+
colorize: true,
|
|
54
|
+
translateTime: "SYS:standard",
|
|
55
|
+
ignore: "pid,hostname",
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
default:
|
|
59
|
+
return assertNever(nodeEnv);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type RequestContext,
|
|
3
|
+
createRequestContextFromNextApiRequest,
|
|
4
|
+
createRequestContextFromRequest,
|
|
5
|
+
createRequestContextMiddleware,
|
|
6
|
+
getRequestId,
|
|
7
|
+
} from "@percepta/next-utils";
|
|
8
|
+
import { withLogContext, withTracer } from "./AppLogger";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Request context middleware with injected dependencies.
|
|
12
|
+
* Uses the local withLogContext and withTracer implementations.
|
|
13
|
+
*/
|
|
14
|
+
const { withRequestContext, withAppRouterRequestContext } =
|
|
15
|
+
createRequestContextMiddleware({
|
|
16
|
+
withLogContext,
|
|
17
|
+
withTracer,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
withRequestContext,
|
|
22
|
+
withAppRouterRequestContext,
|
|
23
|
+
createRequestContextFromNextApiRequest,
|
|
24
|
+
createRequestContextFromRequest,
|
|
25
|
+
getRequestId,
|
|
26
|
+
type RequestContext,
|
|
27
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createFaroInstance } from "@percepta/next-utils/faro";
|
|
4
|
+
import { TracingInstrumentation } from "@grafana/faro-web-tracing";
|
|
5
|
+
import { getClientEnvConfig } from "../../config/clientEnvConfig";
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
FARO_COLLECTOR_URL,
|
|
9
|
+
FARO_APP_NAME,
|
|
10
|
+
FARO_APP_VERSION,
|
|
11
|
+
FARO_APP_ENVIRONMENT,
|
|
12
|
+
} = getClientEnvConfig();
|
|
13
|
+
|
|
14
|
+
export const faroInstance = createFaroInstance({
|
|
15
|
+
collectorUrl: FARO_COLLECTOR_URL,
|
|
16
|
+
app: {
|
|
17
|
+
name: FARO_APP_NAME,
|
|
18
|
+
version: FARO_APP_VERSION,
|
|
19
|
+
environment: FARO_APP_ENVIRONMENT,
|
|
20
|
+
},
|
|
21
|
+
extraInstrumentations: [new TracingInstrumentation()],
|
|
22
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getEnvConfig } from "./config/getEnvConfig";
|
|
2
|
+
import { client } from "./drizzle/db";
|
|
3
|
+
import { getLogger } from "./services/logger/AppLogger";
|
|
4
|
+
|
|
5
|
+
export async function checkStartup(): Promise<boolean> {
|
|
6
|
+
return validateEnvironment() && (await validateDatabaseConnection());
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function validateEnvironment(): boolean {
|
|
10
|
+
try {
|
|
11
|
+
getEnvConfig();
|
|
12
|
+
} catch {
|
|
13
|
+
getLogger().warn(undefined, "Environment config validation failed");
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function validateDatabaseConnection(): Promise<boolean> {
|
|
20
|
+
try {
|
|
21
|
+
const dbCheckPromise = client.query("SELECT 1");
|
|
22
|
+
|
|
23
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
24
|
+
setTimeout(() => reject(new Error("Database check timeout")), 5000),
|
|
25
|
+
);
|
|
26
|
+
await Promise.race([dbCheckPromise, timeoutPromise]);
|
|
27
|
+
} catch {
|
|
28
|
+
getLogger().warn(undefined, "Database connection failed");
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "@percepta/design/theme";
|
|
3
|
+
|
|
4
|
+
:root {
|
|
5
|
+
--background: #ffffff;
|
|
6
|
+
--foreground: #171717;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@theme inline {
|
|
10
|
+
--color-background: var(--background);
|
|
11
|
+
--color-foreground: var(--foreground);
|
|
12
|
+
--font-sans: var(--font-geist-sans);
|
|
13
|
+
--font-mono: var(--font-geist-mono);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@media (prefers-color-scheme: dark) {
|
|
17
|
+
:root {
|
|
18
|
+
--background: #0a0a0a;
|
|
19
|
+
--foreground: #ededed;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
body {
|
|
24
|
+
background: var(--background);
|
|
25
|
+
color: var(--foreground);
|
|
26
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
27
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { cn } from "../cn";
|
|
3
|
+
|
|
4
|
+
describe("cn", () => {
|
|
5
|
+
it("merges class names", () => {
|
|
6
|
+
expect(cn("foo", "bar")).toBe("foo bar");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("handles falsy values", () => {
|
|
10
|
+
expect(cn("base", undefined, "visible")).toBe("base visible");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("deduplicates conflicting Tailwind classes", () => {
|
|
14
|
+
expect(cn("px-2", "px-4")).toBe("px-4");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns an empty string when given no arguments", () => {
|
|
18
|
+
expect(cn()).toBe("");
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { getEnvConfig } from "../config/getEnvConfig";
|
|
2
|
+
import { getLogger } from "../services/logger/AppLogger";
|
|
3
|
+
|
|
4
|
+
export async function syncInngestApp(): Promise<void> {
|
|
5
|
+
const {
|
|
6
|
+
SKIP_INNGEST_SYNC: skipSync,
|
|
7
|
+
INNGEST_BASE_URL: inngestServerUrl,
|
|
8
|
+
APP_BASE_URL: appBaseUrl,
|
|
9
|
+
INNGEST_APP_URL,
|
|
10
|
+
} = getEnvConfig();
|
|
11
|
+
|
|
12
|
+
// Cascade: explicit INNGEST_APP_URL > derived from APP_BASE_URL > localhost fallback
|
|
13
|
+
const appUrl =
|
|
14
|
+
INNGEST_APP_URL ??
|
|
15
|
+
(appBaseUrl
|
|
16
|
+
? `${appBaseUrl}/api/inngest`
|
|
17
|
+
: "http://localhost:3000/api/inngest");
|
|
18
|
+
|
|
19
|
+
if (skipSync) {
|
|
20
|
+
getLogger().info(undefined, "SKIP_INNGEST_SYNC=true, skipping auto-sync");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// INNGEST_BASE_URL is required
|
|
25
|
+
if (!inngestServerUrl) {
|
|
26
|
+
getLogger().error(
|
|
27
|
+
undefined,
|
|
28
|
+
"INNGEST_BASE_URL environment variable is not set, skipping sync",
|
|
29
|
+
);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const gqlEndpoint = `${inngestServerUrl}/v0/gql`;
|
|
34
|
+
|
|
35
|
+
getLogger().info(
|
|
36
|
+
{ safe: { gqlEndpoint, appUrl } },
|
|
37
|
+
"Syncing with Inngest Server",
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const payload = {
|
|
41
|
+
operationName: "CreateApp",
|
|
42
|
+
query:
|
|
43
|
+
"mutation CreateApp($input: CreateAppInput!) {\n createApp(input: $input) {\n url\n }\n}\n",
|
|
44
|
+
variables: {
|
|
45
|
+
input: {
|
|
46
|
+
url: appUrl,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const response = await fetch(gqlEndpoint, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: {
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify(payload),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (response.ok) {
|
|
60
|
+
getLogger().info(undefined, "Successfully synced with Inngest Server");
|
|
61
|
+
}
|
|
62
|
+
}
|