@forgeailab/create-spark 0.1.1 → 0.1.3
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/.claude/skills/architecture-cutline/SKILL.md +96 -0
- package/.claude/skills/board-review/SKILL.md +77 -0
- package/.claude/skills/code-review/SKILL.md +76 -0
- package/.claude/skills/execute-task/SKILL.md +80 -0
- package/.claude/skills/idea-sharpen/SKILL.md +65 -0
- package/.claude/skills/implementation-brief/SKILL.md +87 -0
- package/.claude/skills/mvp-board/SKILL.md +95 -0
- package/.claude/skills/mvp-grill/SKILL.md +60 -0
- package/.claude/skills/mvp-spec/SKILL.md +78 -0
- package/.claude/skills/new-pack/SKILL.md +156 -0
- package/.claude/skills/next-task/SKILL.md +65 -0
- package/.claude/skills/pack-add/SKILL.md +64 -0
- package/.claude/skills/pack-resolve/SKILL.md +67 -0
- package/.claude/skills/parallel-execution/SKILL.md +68 -0
- package/.claude/skills/qa-verify/SKILL.md +77 -0
- package/.claude/skills/risk-check/SKILL.md +88 -0
- package/.claude/skills/sync-board/SKILL.md +76 -0
- package/.claude/skills/ux-theme/SKILL.md +93 -0
- package/.codex/skills/architecture-cutline/SKILL.md +94 -0
- package/.codex/skills/board-review/SKILL.md +75 -0
- package/.codex/skills/code-review/SKILL.md +73 -0
- package/.codex/skills/execute-task/SKILL.md +76 -0
- package/.codex/skills/idea-sharpen/SKILL.md +63 -0
- package/.codex/skills/implementation-brief/SKILL.md +85 -0
- package/.codex/skills/mvp-board/SKILL.md +93 -0
- package/.codex/skills/mvp-grill/SKILL.md +58 -0
- package/.codex/skills/mvp-spec/SKILL.md +76 -0
- package/.codex/skills/new-pack/SKILL.md +153 -0
- package/.codex/skills/next-task/SKILL.md +64 -0
- package/.codex/skills/pack-add/SKILL.md +62 -0
- package/.codex/skills/pack-resolve/SKILL.md +65 -0
- package/.codex/skills/parallel-execution/SKILL.md +66 -0
- package/.codex/skills/qa-verify/SKILL.md +74 -0
- package/.codex/skills/risk-check/SKILL.md +86 -0
- package/.codex/skills/sync-board/SKILL.md +72 -0
- package/.codex/skills/ux-theme/SKILL.md +91 -0
- package/package.json +10 -4
- package/packs/README.md +132 -0
- package/packs/ai-anthropic/files/app/api/ai/route.ts +57 -0
- package/packs/ai-anthropic/files/lib/anthropic.ts +15 -0
- package/packs/ai-anthropic/pack.toml +32 -0
- package/packs/ai-anthropic/skills/ai-feature-patterns/SKILL.md +87 -0
- package/packs/ai-anthropic/tasks.yaml +9 -0
- package/packs/ai-openai/files/app/api/ai-openai/route.ts +55 -0
- package/packs/ai-openai/files/lib/openai.ts +21 -0
- package/packs/ai-openai/pack.toml +30 -0
- package/packs/ai-openai/tasks.yaml +9 -0
- package/packs/analytics-posthog/files/components/PostHogProvider.tsx +19 -0
- package/packs/analytics-posthog/files/lib/posthog/client.ts +20 -0
- package/packs/analytics-posthog/files/lib/posthog/server.ts +24 -0
- package/packs/analytics-posthog/pack.toml +35 -0
- package/packs/analytics-posthog/tasks.yaml +15 -0
- package/packs/auth-better-auth/files/app/(auth)/login/page.tsx +58 -0
- package/packs/auth-better-auth/files/app/api/auth/[...all]/route.ts +4 -0
- package/packs/auth-better-auth/files/lib/auth.ts +21 -0
- package/packs/auth-better-auth/pack.toml +32 -0
- package/packs/auth-better-auth/tasks.yaml +10 -0
- package/packs/auth-better-auth-pg/files/app/api/auth/[...all]/route.ts +4 -0
- package/packs/auth-better-auth-pg/files/lib/auth.ts +86 -0
- package/packs/auth-better-auth-pg/pack.toml +32 -0
- package/packs/auth-better-auth-pg/tasks.yaml +17 -0
- package/packs/auth-supabase/files/app/(auth)/login/page.tsx +64 -0
- package/packs/auth-supabase/files/app/auth/callback/route.ts +15 -0
- package/packs/auth-supabase/files/middleware.ts +41 -0
- package/packs/auth-supabase/pack.toml +34 -0
- package/packs/auth-supabase/tasks.yaml +10 -0
- package/packs/db-postgres/files/compose/postgres.yml +28 -0
- package/packs/db-postgres/files/docker-compose.include.yml +1 -0
- package/packs/db-postgres/files/docker-compose.yml +6 -0
- package/packs/db-postgres/files/drizzle.config.ts +10 -0
- package/packs/db-postgres/files/lib/db/index.ts +10 -0
- package/packs/db-postgres/files/lib/db/schema.ts +11 -0
- package/packs/db-postgres/pack.toml +53 -0
- package/packs/db-postgres/tasks.yaml +11 -0
- package/packs/db-sqlite/files/drizzle.config.ts +10 -0
- package/packs/db-sqlite/files/lib/db.ts +8 -0
- package/packs/db-sqlite/files/lib/schema.ts +13 -0
- package/packs/db-sqlite/pack.toml +34 -0
- package/packs/db-sqlite/tasks.yaml +6 -0
- package/packs/db-supabase/files/lib/supabase/client.ts +8 -0
- package/packs/db-supabase/files/lib/supabase/server.ts +27 -0
- package/packs/db-supabase/pack.toml +32 -0
- package/packs/db-supabase/skills/supabase-patterns/SKILL.md +82 -0
- package/packs/db-supabase/tasks.yaml +6 -0
- package/packs/deploy-vercel/files/docs/deploy.md +21 -0
- package/packs/deploy-vercel/files/vercel.json +4 -0
- package/packs/deploy-vercel/pack.toml +30 -0
- package/packs/deploy-vercel/tasks.yaml +14 -0
- package/packs/docker-compose-dev/files/.env.docker.example +2 -0
- package/packs/docker-compose-dev/files/compose/redis.yml +17 -0
- package/packs/docker-compose-dev/files/docker-compose.include.yml +1 -0
- package/packs/docker-compose-dev/files/docker-compose.yml +6 -0
- package/packs/docker-compose-dev/pack.toml +38 -0
- package/packs/docker-compose-dev/tasks.yaml +9 -0
- package/packs/email-resend/files/app/api/email/test/route.ts +38 -0
- package/packs/email-resend/files/emails/welcome.tsx +66 -0
- package/packs/email-resend/files/lib/email.ts +40 -0
- package/packs/email-resend/pack.toml +34 -0
- package/packs/email-resend/tasks.yaml +9 -0
- package/packs/example/pack.toml +69 -0
- package/packs/payments-stripe/files/app/api/billing-portal/route.ts +24 -0
- package/packs/payments-stripe/files/app/api/checkout/route.ts +58 -0
- package/packs/payments-stripe/files/app/api/webhooks/stripe/route.ts +84 -0
- package/packs/payments-stripe/files/lib/stripe.ts +60 -0
- package/packs/payments-stripe/pack.toml +49 -0
- package/packs/payments-stripe/skills/stripe-patterns/SKILL.md +93 -0
- package/packs/payments-stripe/tasks.yaml +16 -0
- package/packs/sync-zero/files/components/ZeroProvider.tsx +3 -0
- package/packs/sync-zero/files/compose/zero-cache.yml +26 -0
- package/packs/sync-zero/files/docker-compose.include.yml +1 -0
- package/packs/sync-zero/files/docker-compose.yml +6 -0
- package/packs/sync-zero/files/lib/zero/client.ts +18 -0
- package/packs/sync-zero/files/lib/zero/schema.ts +17 -0
- package/packs/sync-zero/files/zero.config.ts +26 -0
- package/packs/sync-zero/pack.toml +61 -0
- package/packs/sync-zero/skills/zero-patterns/SKILL.md +69 -0
- package/packs/sync-zero/tasks.yaml +16 -0
- package/packs/testing-playwright/files/e2e/example.spec.ts +7 -0
- package/packs/testing-playwright/files/playwright.config.ts +33 -0
- package/packs/testing-playwright/pack.toml +25 -0
- package/packs/testing-playwright/tasks.yaml +9 -0
- package/packs/ui-shadcn/files/app/globals.css +56 -0
- package/packs/ui-shadcn/files/components/ui/button.tsx +47 -0
- package/packs/ui-shadcn/files/components/ui/card.tsx +33 -0
- package/packs/ui-shadcn/files/lib/utils.ts +6 -0
- package/packs/ui-shadcn/files/postcss.config.mjs +7 -0
- package/packs/ui-shadcn/files/tailwind.config.ts +57 -0
- package/packs/ui-shadcn/pack.toml +44 -0
- package/packs/ui-shadcn/skills/shadcn-dashboard-patterns/SKILL.md +85 -0
- package/packs/ui-shadcn/tasks.yaml +6 -0
- package/presets/docs-site.toml +4 -0
- package/presets/internal-tool.toml +4 -0
- package/presets/lean-saas.toml +4 -0
- package/presets/local-ai-mvp.toml +4 -0
- package/presets/saas-classic.toml +4 -0
- package/scripts/sync-skills.ts +223 -0
- package/src/paths.ts +22 -4
- package/templates/README.md +43 -0
- package/templates/astro/README.md +3 -0
- package/templates/astro/template.toml +4 -0
- package/templates/astro-starlight/README.md +3 -0
- package/templates/astro-starlight/template.toml +4 -0
- package/templates/nextjs/.ai/architecture.md +13 -0
- package/templates/nextjs/.ai/board.md +7 -0
- package/templates/nextjs/.ai/product-spec.md +11 -0
- package/templates/nextjs/.claude/skills/.gitkeep +0 -0
- package/templates/nextjs/.codex/skills/.gitkeep +0 -0
- package/templates/nextjs/AGENTS.md +95 -0
- package/templates/nextjs/CLAUDE.md +3 -0
- package/templates/nextjs/README.md +20 -0
- package/templates/nextjs/app/(app)/home/page.tsx +43 -0
- package/templates/nextjs/app/(app)/home/posts-panel.tsx +83 -0
- package/templates/nextjs/app/(app)/layout.tsx +12 -0
- package/templates/nextjs/app/(auth)/login/page.tsx +97 -0
- package/templates/nextjs/app/globals.css +23 -0
- package/templates/nextjs/app/layout.tsx +20 -0
- package/templates/nextjs/app/page.tsx +39 -0
- package/templates/nextjs/lib/auth-placeholder.ts +21 -0
- package/templates/nextjs/lib/posts-placeholder.ts +30 -0
- package/templates/nextjs/next.config.ts +5 -0
- package/templates/nextjs/package.json +26 -0
- package/templates/nextjs/postcss.config.mjs +7 -0
- package/templates/nextjs/spark.config.json +4 -0
- package/templates/nextjs/template.toml +4 -0
- package/templates/nextjs/tsconfig.json +27 -0
- package/templates/nextjs/types/post.ts +13 -0
- package/templates/one/README.md +5 -0
- package/templates/one/template.toml +4 -0
- package/templates/vite-react/README.md +3 -0
- package/templates/vite-react/template.toml +4 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { getOpenAIClient, OPENAI_CHAT_MODEL, type OpenAIChatMessage } from "../../../lib/openai";
|
|
2
|
+
|
|
3
|
+
export const runtime = "nodejs";
|
|
4
|
+
|
|
5
|
+
type ChatRequest = {
|
|
6
|
+
messages?: OpenAIChatMessage[];
|
|
7
|
+
prompt?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function normalizeMessages(body: ChatRequest): OpenAIChatMessage[] {
|
|
11
|
+
if (Array.isArray(body.messages) && body.messages.length > 0) {
|
|
12
|
+
return body.messages;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (body.prompt) {
|
|
16
|
+
return [{ role: "user", content: body.prompt }];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
throw new Error("Request body must include messages or prompt.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function POST(request: Request): Promise<Response> {
|
|
23
|
+
const body = (await request.json()) as ChatRequest;
|
|
24
|
+
const messages = normalizeMessages(body);
|
|
25
|
+
|
|
26
|
+
const stream = await getOpenAIClient().chat.completions.create({
|
|
27
|
+
model: OPENAI_CHAT_MODEL,
|
|
28
|
+
messages,
|
|
29
|
+
stream: true,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const encoder = new TextEncoder();
|
|
33
|
+
const readable = new ReadableStream<Uint8Array>({
|
|
34
|
+
async start(controller) {
|
|
35
|
+
try {
|
|
36
|
+
for await (const chunk of stream) {
|
|
37
|
+
const content = chunk.choices[0]?.delta?.content;
|
|
38
|
+
if (content) {
|
|
39
|
+
controller.enqueue(encoder.encode(content));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
controller.close();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
controller.error(error);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return new Response(readable, {
|
|
50
|
+
headers: {
|
|
51
|
+
"Cache-Control": "no-cache",
|
|
52
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
|
|
3
|
+
let client: OpenAI | undefined;
|
|
4
|
+
|
|
5
|
+
export type OpenAIChatMessage = {
|
|
6
|
+
role: "developer" | "system" | "user" | "assistant";
|
|
7
|
+
content: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const OPENAI_CHAT_MODEL = "gpt-5.2";
|
|
11
|
+
|
|
12
|
+
export function getOpenAIClient(): OpenAI {
|
|
13
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
14
|
+
|
|
15
|
+
if (!apiKey) {
|
|
16
|
+
throw new Error("OPENAI_API_KEY is required to use the OpenAI client.");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
client ??= new OpenAI({ apiKey });
|
|
20
|
+
return client;
|
|
21
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name = "ai-openai"
|
|
2
|
+
version = "1.0.0"
|
|
3
|
+
category = "ai"
|
|
4
|
+
description = "OpenAI SDK client wrapper and streaming chat endpoint."
|
|
5
|
+
provides = ["ai-sdk"]
|
|
6
|
+
requires = []
|
|
7
|
+
conflicts = []
|
|
8
|
+
requires_runtime = ["server"]
|
|
9
|
+
compatible_scaffolds = []
|
|
10
|
+
|
|
11
|
+
[dependencies]
|
|
12
|
+
runtime = ["openai"]
|
|
13
|
+
dev = []
|
|
14
|
+
|
|
15
|
+
[env]
|
|
16
|
+
required = ["OPENAI_API_KEY"]
|
|
17
|
+
optional = []
|
|
18
|
+
|
|
19
|
+
[[files]]
|
|
20
|
+
mode = "create"
|
|
21
|
+
from = "lib/openai.ts"
|
|
22
|
+
to = "lib/openai.ts"
|
|
23
|
+
|
|
24
|
+
[[files]]
|
|
25
|
+
mode = "create"
|
|
26
|
+
from = "app/api/ai-openai/route.ts"
|
|
27
|
+
to = "app/api/ai-openai/route.ts"
|
|
28
|
+
|
|
29
|
+
[tasks]
|
|
30
|
+
file = "tasks.yaml"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
epic: AI
|
|
2
|
+
tasks:
|
|
3
|
+
- id: AI-101
|
|
4
|
+
title: Add usage tracking + cost guardrails
|
|
5
|
+
status: Clarifying
|
|
6
|
+
acceptance:
|
|
7
|
+
- OpenAI requests record model, token usage, and user or workspace context.
|
|
8
|
+
- The endpoint enforces a documented per-user or per-workspace usage limit.
|
|
9
|
+
- Over-limit requests return a clear non-200 response without calling OpenAI.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { useEffect } from "react";
|
|
5
|
+
import posthog from "posthog-js";
|
|
6
|
+
import { PostHogProvider as Provider } from "posthog-js/react";
|
|
7
|
+
import { getPostHogClient } from "../lib/posthog/client";
|
|
8
|
+
|
|
9
|
+
type PostHogProviderProps = {
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function PostHogProvider({ children }: PostHogProviderProps) {
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
getPostHogClient();
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
return <Provider client={posthog}>{children}</Provider>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import posthog from "posthog-js";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com";
|
|
6
|
+
|
|
7
|
+
export function getPostHogClient(): typeof posthog {
|
|
8
|
+
const key = process.env.NEXT_PUBLIC_POSTHOG_KEY;
|
|
9
|
+
const host = process.env.NEXT_PUBLIC_POSTHOG_HOST ?? DEFAULT_POSTHOG_HOST;
|
|
10
|
+
const loaded = (posthog as typeof posthog & { __loaded?: boolean }).__loaded;
|
|
11
|
+
|
|
12
|
+
if (typeof window !== "undefined" && key && !loaded) {
|
|
13
|
+
posthog.init(key, {
|
|
14
|
+
api_host: host,
|
|
15
|
+
capture_pageview: false,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return posthog;
|
|
20
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { PostHog } from "posthog-node";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com";
|
|
4
|
+
|
|
5
|
+
let client: PostHog | undefined;
|
|
6
|
+
|
|
7
|
+
export function getPostHogServerClient(): PostHog | undefined {
|
|
8
|
+
const key = process.env.NEXT_PUBLIC_POSTHOG_KEY;
|
|
9
|
+
|
|
10
|
+
if (!key) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
client ??= new PostHog(key, {
|
|
15
|
+
host: process.env.NEXT_PUBLIC_POSTHOG_HOST ?? DEFAULT_POSTHOG_HOST,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return client;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function shutdownPostHogServerClient(): Promise<void> {
|
|
22
|
+
await client?.shutdown();
|
|
23
|
+
client = undefined;
|
|
24
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name = "analytics-posthog"
|
|
2
|
+
version = "1.0.0"
|
|
3
|
+
category = "analytics"
|
|
4
|
+
description = "PostHog browser and server analytics helpers."
|
|
5
|
+
provides = ["analytics"]
|
|
6
|
+
requires = []
|
|
7
|
+
conflicts = []
|
|
8
|
+
requires_runtime = ["server"]
|
|
9
|
+
compatible_scaffolds = []
|
|
10
|
+
|
|
11
|
+
[dependencies]
|
|
12
|
+
runtime = ["posthog-js", "posthog-node"]
|
|
13
|
+
dev = []
|
|
14
|
+
|
|
15
|
+
[env]
|
|
16
|
+
required = ["NEXT_PUBLIC_POSTHOG_KEY"]
|
|
17
|
+
optional = ["NEXT_PUBLIC_POSTHOG_HOST"]
|
|
18
|
+
|
|
19
|
+
[[files]]
|
|
20
|
+
mode = "create"
|
|
21
|
+
from = "lib/posthog/client.ts"
|
|
22
|
+
to = "lib/posthog/client.ts"
|
|
23
|
+
|
|
24
|
+
[[files]]
|
|
25
|
+
mode = "create"
|
|
26
|
+
from = "lib/posthog/server.ts"
|
|
27
|
+
to = "lib/posthog/server.ts"
|
|
28
|
+
|
|
29
|
+
[[files]]
|
|
30
|
+
mode = "create"
|
|
31
|
+
from = "components/PostHogProvider.tsx"
|
|
32
|
+
to = "components/PostHogProvider.tsx"
|
|
33
|
+
|
|
34
|
+
[tasks]
|
|
35
|
+
file = "tasks.yaml"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
epic: Analytics
|
|
2
|
+
tasks:
|
|
3
|
+
- id: ANALYTICS-001
|
|
4
|
+
title: Wire posthog provider into app root
|
|
5
|
+
status: Clarifying
|
|
6
|
+
acceptance:
|
|
7
|
+
- The app root wraps client routes with PostHogProvider.
|
|
8
|
+
- Page views are captured intentionally, either by router events or a documented manual call.
|
|
9
|
+
- id: ANALYTICS-002
|
|
10
|
+
title: Add consent banner
|
|
11
|
+
status: Clarifying
|
|
12
|
+
acceptance:
|
|
13
|
+
- Users can accept or decline analytics before tracking starts.
|
|
14
|
+
- The consent choice persists across reloads.
|
|
15
|
+
- PostHog capture is disabled when consent is declined.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useTransition } from 'react';
|
|
4
|
+
import { createAuthClient } from 'better-auth/react';
|
|
5
|
+
|
|
6
|
+
const authClient = createAuthClient();
|
|
7
|
+
type SocialProvider = 'github' | 'google';
|
|
8
|
+
|
|
9
|
+
export default function LoginPage() {
|
|
10
|
+
const [error, setError] = useState<string | null>(null);
|
|
11
|
+
const [isPending, startTransition] = useTransition();
|
|
12
|
+
|
|
13
|
+
function signIn(provider: SocialProvider) {
|
|
14
|
+
setError(null);
|
|
15
|
+
startTransition(() => {
|
|
16
|
+
void authClient.signIn
|
|
17
|
+
.social({
|
|
18
|
+
provider,
|
|
19
|
+
callbackURL: '/',
|
|
20
|
+
})
|
|
21
|
+
.then((result) => {
|
|
22
|
+
if (result.error) {
|
|
23
|
+
setError(result.error.message ?? 'Unable to sign in.');
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<main className="mx-auto flex min-h-screen w-full max-w-sm flex-col justify-center px-6">
|
|
31
|
+
<div className="space-y-2">
|
|
32
|
+
<h1 className="text-2xl font-semibold tracking-tight">Sign in</h1>
|
|
33
|
+
<p className="text-sm text-muted-foreground">Continue with a configured OAuth provider.</p>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div className="mt-8 grid gap-3">
|
|
37
|
+
<button
|
|
38
|
+
className="inline-flex h-10 items-center justify-center rounded-md border border-input bg-background px-4 text-sm font-medium hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
|
|
39
|
+
disabled={isPending}
|
|
40
|
+
onClick={() => signIn('github')}
|
|
41
|
+
type="button"
|
|
42
|
+
>
|
|
43
|
+
Continue with GitHub
|
|
44
|
+
</button>
|
|
45
|
+
<button
|
|
46
|
+
className="inline-flex h-10 items-center justify-center rounded-md border border-input bg-background px-4 text-sm font-medium hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
|
|
47
|
+
disabled={isPending}
|
|
48
|
+
onClick={() => signIn('google')}
|
|
49
|
+
type="button"
|
|
50
|
+
>
|
|
51
|
+
Continue with Google
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{error ? <p className="mt-4 text-sm text-destructive">{error}</p> : null}
|
|
56
|
+
</main>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createAuth as createBetterAuth } from '@forgeailab/spark-auth-better-auth';
|
|
2
|
+
|
|
3
|
+
// Wire your database adapter here. Example (drizzle + sqlite):
|
|
4
|
+
//
|
|
5
|
+
// import { drizzleAdapter } from '@better-auth/drizzle-adapter';
|
|
6
|
+
// import { db } from '@/lib/db';
|
|
7
|
+
// import * as schema from '@/lib/db/schema';
|
|
8
|
+
// const adapter = drizzleAdapter(db, { provider: 'sqlite', schema });
|
|
9
|
+
//
|
|
10
|
+
// Then pass the adapter into createAuth({ adapter, ... }).
|
|
11
|
+
|
|
12
|
+
export const auth = createBetterAuth({
|
|
13
|
+
adapter: undefined as never, // TODO: replace with your drizzle adapter
|
|
14
|
+
secret: process.env.BETTER_AUTH_SECRET!,
|
|
15
|
+
baseURL: process.env.BETTER_AUTH_URL,
|
|
16
|
+
emailAndPassword: {
|
|
17
|
+
enabled: true,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export type Auth = typeof auth;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name = "auth-better-auth"
|
|
2
|
+
version = "1.0.0"
|
|
3
|
+
category = "auth"
|
|
4
|
+
description = "Better Auth route handler, auth instance, and login page for Next.js."
|
|
5
|
+
provides = ["auth"]
|
|
6
|
+
requires = ["db"]
|
|
7
|
+
conflicts = ["auth"]
|
|
8
|
+
requires_runtime = ["server"]
|
|
9
|
+
compatible_scaffolds = ["nextjs"]
|
|
10
|
+
|
|
11
|
+
[runtime_package]
|
|
12
|
+
package = "@forgeailab/spark-auth-better-auth"
|
|
13
|
+
version = "^0.1"
|
|
14
|
+
|
|
15
|
+
[dependencies]
|
|
16
|
+
runtime = ["@better-auth/drizzle-adapter"]
|
|
17
|
+
|
|
18
|
+
[env]
|
|
19
|
+
required = ["BETTER_AUTH_SECRET", "BETTER_AUTH_URL"]
|
|
20
|
+
|
|
21
|
+
[[files]]
|
|
22
|
+
mode = "create"
|
|
23
|
+
from = "lib/auth.ts"
|
|
24
|
+
to = "lib/auth.ts"
|
|
25
|
+
|
|
26
|
+
[[files]]
|
|
27
|
+
mode = "create"
|
|
28
|
+
from = "app/api/auth/[...all]/route.ts"
|
|
29
|
+
to = "app/api/auth/[...all]/route.ts"
|
|
30
|
+
|
|
31
|
+
[tasks]
|
|
32
|
+
file = "tasks.yaml"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
epic: Auth
|
|
2
|
+
tasks:
|
|
3
|
+
- id: AUTH-101
|
|
4
|
+
title: Wire auth instance to active db adapter
|
|
5
|
+
acceptance:
|
|
6
|
+
- Better Auth persists users, sessions, and accounts through the active db adapter
|
|
7
|
+
- id: AUTH-102
|
|
8
|
+
title: Configure OAuth providers (Google/GitHub)
|
|
9
|
+
acceptance:
|
|
10
|
+
- Google and GitHub OAuth providers can complete a sign-in flow
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { drizzleAdapter } from '@better-auth/drizzle-adapter';
|
|
2
|
+
import { createAuth as createBetterAuth } from '@forgeailab/spark-auth-better-auth';
|
|
3
|
+
import { db } from '@/lib/db';
|
|
4
|
+
import * as schema from '@/lib/db/schema';
|
|
5
|
+
|
|
6
|
+
// Postgres-flavored Better Auth wiring. Pair this pack with `db-postgres`
|
|
7
|
+
// (or `db-supabase`), and add the four Better Auth tables to your
|
|
8
|
+
// `lib/db/schema.ts`: `user`, `session`, `account`, `verification`. Snake-case
|
|
9
|
+
// column names are what Better Auth expects.
|
|
10
|
+
|
|
11
|
+
const DEV_SECRET =
|
|
12
|
+
'reference-dev-secret-change-me-reference-dev-secret-change-me';
|
|
13
|
+
|
|
14
|
+
function env(name: string, fallback: string): string {
|
|
15
|
+
return process.env[name] ?? fallback;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveTrustedOrigins(baseURL: string): string[] {
|
|
19
|
+
const fromEnv = process.env.BETTER_AUTH_TRUSTED_ORIGINS;
|
|
20
|
+
if (fromEnv) {
|
|
21
|
+
return fromEnv
|
|
22
|
+
.split(',')
|
|
23
|
+
.map((s) => s.trim())
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const origins = new Set<string>([baseURL]);
|
|
28
|
+
|
|
29
|
+
// In dev, Next.js auto-bumps the port when 3000 is busy. Trust common
|
|
30
|
+
// localhost ports so signup doesn't break with "Invalid origin" just because
|
|
31
|
+
// another server already owns 3000. Override via BETTER_AUTH_TRUSTED_ORIGINS
|
|
32
|
+
// for production lockdown.
|
|
33
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
34
|
+
for (const port of [3000, 3001, 3002, 3003, 3010, 4000, 5173, 8080]) {
|
|
35
|
+
origins.add(`http://localhost:${port}`);
|
|
36
|
+
origins.add(`http://127.0.0.1:${port}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return [...origins];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createAuthDatabase() {
|
|
44
|
+
return drizzleAdapter(db, {
|
|
45
|
+
provider: 'pg',
|
|
46
|
+
schema: {
|
|
47
|
+
...schema,
|
|
48
|
+
// Map your Drizzle table exports to the names Better Auth uses. Uncomment
|
|
49
|
+
// these once your schema has the matching tables (or use these names
|
|
50
|
+
// directly in your schema and drop the aliases).
|
|
51
|
+
// user: schema.users,
|
|
52
|
+
// session: schema.sessions,
|
|
53
|
+
// account: schema.accounts,
|
|
54
|
+
// verification: schema.verifications,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type CreateAuthOptions = {
|
|
60
|
+
database?: ReturnType<typeof createAuthDatabase>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export function createAuth(options: CreateAuthOptions = {}) {
|
|
64
|
+
const baseURL = env('BETTER_AUTH_URL', 'http://localhost:3000');
|
|
65
|
+
|
|
66
|
+
return createBetterAuth({
|
|
67
|
+
adapter: options.database ?? createAuthDatabase(),
|
|
68
|
+
baseURL,
|
|
69
|
+
secret: env('BETTER_AUTH_SECRET', DEV_SECRET),
|
|
70
|
+
trustedOrigins: resolveTrustedOrigins(baseURL),
|
|
71
|
+
emailAndPassword: {
|
|
72
|
+
enabled: true,
|
|
73
|
+
},
|
|
74
|
+
// Uncomment to enable GitHub OAuth. Add env vars to your .env.local and
|
|
75
|
+
// register the OAuth app at https://github.com/settings/developers.
|
|
76
|
+
// socialProviders: {
|
|
77
|
+
// github: {
|
|
78
|
+
// clientId: env('GITHUB_CLIENT_ID', 'github-client-id'),
|
|
79
|
+
// clientSecret: env('GITHUB_CLIENT_SECRET', 'github-client-secret'),
|
|
80
|
+
// },
|
|
81
|
+
// },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const auth = createAuth();
|
|
86
|
+
export type Auth = typeof auth;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name = "auth-better-auth-pg"
|
|
2
|
+
version = "1.0.0"
|
|
3
|
+
category = "auth"
|
|
4
|
+
description = "Better Auth wired to Postgres via Drizzle. Pairs with db-postgres or db-supabase."
|
|
5
|
+
provides = ["auth"]
|
|
6
|
+
requires = ["db-pg"]
|
|
7
|
+
conflicts = ["auth"]
|
|
8
|
+
requires_runtime = ["server"]
|
|
9
|
+
compatible_scaffolds = ["nextjs"]
|
|
10
|
+
|
|
11
|
+
[runtime_package]
|
|
12
|
+
package = "@forgeailab/spark-auth-better-auth"
|
|
13
|
+
version = "^0.1"
|
|
14
|
+
|
|
15
|
+
[dependencies]
|
|
16
|
+
runtime = ["@better-auth/drizzle-adapter"]
|
|
17
|
+
|
|
18
|
+
[env]
|
|
19
|
+
required = ["BETTER_AUTH_SECRET", "BETTER_AUTH_URL"]
|
|
20
|
+
|
|
21
|
+
[[files]]
|
|
22
|
+
mode = "create"
|
|
23
|
+
from = "lib/auth.ts"
|
|
24
|
+
to = "lib/auth.ts"
|
|
25
|
+
|
|
26
|
+
[[files]]
|
|
27
|
+
mode = "create"
|
|
28
|
+
from = "app/api/auth/[...all]/route.ts"
|
|
29
|
+
to = "app/api/auth/[...all]/route.ts"
|
|
30
|
+
|
|
31
|
+
[tasks]
|
|
32
|
+
file = "tasks.yaml"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
epic: Auth
|
|
2
|
+
tasks:
|
|
3
|
+
- id: AUTH-201
|
|
4
|
+
title: Add Better Auth tables to your Drizzle schema
|
|
5
|
+
acceptance:
|
|
6
|
+
- lib/db/schema.ts defines user, session, account, verification with pgTable
|
|
7
|
+
- Snake-case columns (email_verified, expires_at, user_id, etc.) match Better Auth contract
|
|
8
|
+
- `bun drizzle-kit push` applies the new tables to Postgres
|
|
9
|
+
- id: AUTH-202
|
|
10
|
+
title: Map schema exports in lib/auth.ts
|
|
11
|
+
acceptance:
|
|
12
|
+
- `createAuthDatabase()` passes drizzleAdapter the four required schema aliases
|
|
13
|
+
- `bunx tsc --noEmit` is clean
|
|
14
|
+
- id: AUTH-203
|
|
15
|
+
title: Configure OAuth providers (Google/GitHub) if needed
|
|
16
|
+
acceptance:
|
|
17
|
+
- `socialProviders` set in createAuth options with env-backed credentials
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Provider } from '@supabase/supabase-js';
|
|
2
|
+
import { headers } from 'next/headers';
|
|
3
|
+
import { redirect } from 'next/navigation';
|
|
4
|
+
import { createSupabaseServerClient } from '@/lib/supabase/server';
|
|
5
|
+
|
|
6
|
+
async function signInWithOAuth(formData: FormData) {
|
|
7
|
+
'use server';
|
|
8
|
+
|
|
9
|
+
const rawProvider = formData.get('provider');
|
|
10
|
+
if (rawProvider !== 'github' && rawProvider !== 'google') {
|
|
11
|
+
redirect('/login?error=unsupported-provider');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const provider: Provider = rawProvider;
|
|
15
|
+
const requestHeaders = await headers();
|
|
16
|
+
const origin = requestHeaders.get('origin') ?? 'http://localhost:3000';
|
|
17
|
+
const supabase = await createSupabaseServerClient();
|
|
18
|
+
const { data, error } = await supabase.auth.signInWithOAuth({
|
|
19
|
+
provider,
|
|
20
|
+
options: {
|
|
21
|
+
redirectTo: `${origin}/auth/callback`,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (error) {
|
|
26
|
+
redirect(`/login?error=${encodeURIComponent(error.message)}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (data.url) {
|
|
30
|
+
redirect(data.url);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
redirect('/login?error=missing-redirect-url');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default function LoginPage() {
|
|
37
|
+
return (
|
|
38
|
+
<main className="mx-auto flex min-h-screen w-full max-w-sm flex-col justify-center px-6">
|
|
39
|
+
<div className="space-y-2">
|
|
40
|
+
<h1 className="text-2xl font-semibold tracking-tight">Sign in</h1>
|
|
41
|
+
<p className="text-sm text-muted-foreground">Choose an OAuth provider to continue.</p>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<form action={signInWithOAuth} className="mt-8 grid gap-3">
|
|
45
|
+
<button
|
|
46
|
+
className="inline-flex h-10 items-center justify-center rounded-md border border-input bg-background px-4 text-sm font-medium hover:bg-accent hover:text-accent-foreground"
|
|
47
|
+
name="provider"
|
|
48
|
+
type="submit"
|
|
49
|
+
value="github"
|
|
50
|
+
>
|
|
51
|
+
Continue with GitHub
|
|
52
|
+
</button>
|
|
53
|
+
<button
|
|
54
|
+
className="inline-flex h-10 items-center justify-center rounded-md border border-input bg-background px-4 text-sm font-medium hover:bg-accent hover:text-accent-foreground"
|
|
55
|
+
name="provider"
|
|
56
|
+
type="submit"
|
|
57
|
+
value="google"
|
|
58
|
+
>
|
|
59
|
+
Continue with Google
|
|
60
|
+
</button>
|
|
61
|
+
</form>
|
|
62
|
+
</main>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
import { createSupabaseServerClient } from '@/lib/supabase/server';
|
|
3
|
+
|
|
4
|
+
export async function GET(request: NextRequest) {
|
|
5
|
+
const requestUrl = new URL(request.url);
|
|
6
|
+
const code = requestUrl.searchParams.get('code');
|
|
7
|
+
const next = requestUrl.searchParams.get('next') ?? '/';
|
|
8
|
+
|
|
9
|
+
if (code) {
|
|
10
|
+
const supabase = await createSupabaseServerClient();
|
|
11
|
+
await supabase.auth.exchangeCodeForSession(code);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return NextResponse.redirect(new URL(next, requestUrl.origin));
|
|
15
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createServerClient } from '@supabase/ssr';
|
|
2
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
3
|
+
|
|
4
|
+
export async function middleware(request: NextRequest) {
|
|
5
|
+
let response = NextResponse.next({
|
|
6
|
+
request,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const supabase = createServerClient(
|
|
10
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
11
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
12
|
+
{
|
|
13
|
+
cookies: {
|
|
14
|
+
getAll() {
|
|
15
|
+
return request.cookies.getAll();
|
|
16
|
+
},
|
|
17
|
+
setAll(cookiesToSet) {
|
|
18
|
+
cookiesToSet.forEach(({ name, value }) => {
|
|
19
|
+
request.cookies.set(name, value);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
response = NextResponse.next({
|
|
23
|
+
request,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
cookiesToSet.forEach(({ name, value, options }) => {
|
|
27
|
+
response.cookies.set(name, value, options);
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
await supabase.auth.getUser();
|
|
35
|
+
|
|
36
|
+
return response;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const config = {
|
|
40
|
+
matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
|
|
41
|
+
};
|