Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,6 @@ jobs:
working-directory: scripts
run: npm test

# We deliberately do NOT run `npm install` on each template in CI:
# contributors install locally before opening a PR, package-lock.json
# makes the install reproducible, and the slow per-template install
# would dominate CI time as the registry grows. The static checks below
# (JSON parse, file existence, secret patterns, SQL parse, edge-function
# tsc) cover what install would catch except the rare "package was
# yanked from npm" case — we'll catch that via Dependabot/Snyk later.
edge-function-tsc:
name: tsc --noEmit on edge functions
runs-on: ubuntu-latest
Expand Down Expand Up @@ -92,3 +85,33 @@ jobs:
exit 1
fi
done

publishable-builds:
name: Native template build and artifact admission
runs-on: ubuntu-latest
needs: [validate-registry]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Enable pinned package managers
run: corepack enable
- name: Build every native template with its controlled profile
run: |
{ node -e "for (const item of require('./registry.json')) if (item.publishingCompatibility === 'native') console.log(item.slug + '\t' + item.buildProfile)"; printf 'react\tvite-npm-v1\ntodo\tnext-static-npm-v1\n'; } |
while IFS=$'\t' read -r slug profile; do
case "$profile" in
vite-npm-v1|next-static-npm-v1)
(cd "$slug" && npm ci --ignore-scripts && npm run build)
;;
vite-pnpm-v1|next-static-pnpm-v1)
(cd "$slug" && pnpm install --frozen-lockfile --ignore-scripts && pnpm run build)
;;
*)
echo "::error::$slug declares unknown controlled profile $profile"
exit 1
;;
esac
node scripts/verify-publishable-template.mjs "$slug"
done
1 change: 1 addition & 0 deletions admin-dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</head>
<body>
<div id="root"></div>
<script src="/.well-known/insforge-runtime-config.js"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
7 changes: 6 additions & 1 deletion admin-dashboard/src/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ const schema = z.object({
VITE_INSFORGE_ANON_KEY: z.string().min(1),
})

const parsed = schema.safeParse(import.meta.env)
const runtime = (window as Window & { __INSFORGE_RUNTIME_CONFIG__?: { apiBaseURL?: string; anonKey?: string } }).__INSFORGE_RUNTIME_CONFIG__
const parsed = schema.safeParse({
...import.meta.env,
VITE_INSFORGE_URL: runtime?.apiBaseURL ?? import.meta.env.VITE_INSFORGE_URL,
VITE_INSFORGE_ANON_KEY: runtime?.anonKey ?? import.meta.env.VITE_INSFORGE_ANON_KEY,
})

if (!parsed.success) {
// Surface a clear message in the browser console so missing config is obvious.
Expand Down
1 change: 1 addition & 0 deletions insight-flow-agent-chat/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
</head>
<body>
<div id="root"></div>
<script src="/.well-known/insforge-runtime-config.js"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
5 changes: 3 additions & 2 deletions insight-flow-agent-chat/src/lib/insforge.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { createClient } from '@insforge/sdk';

const baseUrl = import.meta.env.VITE_INSFORGE_URL?.trim().replace(/\/$/, '');
const anonKey = import.meta.env.VITE_INSFORGE_ANON_KEY?.trim();
const runtime = (window as Window & { __INSFORGE_RUNTIME_CONFIG__?: { apiBaseURL?: string; anonKey?: string } }).__INSFORGE_RUNTIME_CONFIG__;
const baseUrl = (runtime?.apiBaseURL ?? import.meta.env.VITE_INSFORGE_URL)?.trim().replace(/\/$/, '');
const anonKey = (runtime?.anonKey ?? import.meta.env.VITE_INSFORGE_ANON_KEY)?.trim();

export const connected = Boolean(baseUrl && anonKey);
export const insforgeBaseUrl = baseUrl ?? '';
Expand Down
1 change: 1 addition & 0 deletions react/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
</head>
<body>
<div id="root"></div>
<script src="/.well-known/insforge-runtime-config.js"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
5 changes: 3 additions & 2 deletions react/src/lib/insforge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { createClient } from '@insforge/sdk';
let browserClient: ReturnType<typeof createClient> | null = null;

export function getInsforgeConfig() {
const baseUrl = import.meta.env.VITE_INSFORGE_BASE_URL?.trim();
const anonKey = import.meta.env.VITE_INSFORGE_ANON_KEY?.trim();
const runtime = (window as Window & { __INSFORGE_RUNTIME_CONFIG__?: { apiBaseURL?: string; anonKey?: string } }).__INSFORGE_RUNTIME_CONFIG__;
const baseUrl = (runtime?.apiBaseURL ?? import.meta.env.VITE_INSFORGE_BASE_URL)?.trim();
const anonKey = (runtime?.anonKey ?? import.meta.env.VITE_INSFORGE_ANON_KEY)?.trim();

return {
baseUrl: baseUrl ?? '',
Expand Down
22 changes: 22 additions & 0 deletions registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"description": "Launch an online store in an afternoon. Product catalog, cart, wishlist, real Stripe Checkout with Apple Pay and Google Pay, a live order status timeline, and an admin fulfillment flow that marks orders shipped and delivered.",
"category": "e-commerce",
"framework": "nextjs",
"publishingCompatibility": "requires-node-runtime",
"publishingBlockers": ["Uses request-time cookies, Server Actions, and dynamic routes"],
"features": ["Stripe Checkout", "Order Timeline", "Wishlist", "Admin Fulfillment", "shadcn/ui", "Auth"],
"tags": ["e-commerce", "storefront", "stripe", "payments", "wishlist"],
"cover": "assets/covers/e-commerce.png",
Expand All @@ -18,6 +20,8 @@
"description": "Tell visitors what you're shipping before you've shipped it. Interactive product preview with tab-switching scenes, animated KPIs, pricing toggle, and a waitlist that writes straight to your database. Drop your copy in and launch.",
"category": "marketing",
"framework": "nextjs",
"publishingCompatibility": "requires-node-runtime",
"publishingBlockers": ["Uses request-time cookies, Server Actions, and Route Handlers"],
"features": ["Waitlist Capture", "Pricing Page", "shadcn/ui", "Auth"],
"tags": ["landing", "saas", "marketing", "waitlist", "shadcn"],
"cover": "assets/covers/landing.png",
Expand All @@ -31,6 +35,8 @@
"description": "Run a sales pipeline without paying Salesforce. Drag-and-drop kanban for leads, follow-up reminders, and a per-contact activity log — all multi-tenant from row one.",
"category": "crm",
"framework": "nextjs",
"publishingCompatibility": "requires-node-runtime",
"publishingBlockers": ["Uses server-only authentication, redirects, and API routes"],
"features": ["dnd-kit", "Kanban Pipeline", "Auth"],
"tags": ["crm", "sales", "pipeline", "leads"],
"cover": "assets/covers/crm.png",
Expand All @@ -44,6 +50,8 @@
"description": "Calendly meets Yelp: customers browse providers, book appointments around real availability, message about details, and leave reviews after. Two-sided RLS baked in.",
"category": "marketplace",
"framework": "nextjs",
"publishingCompatibility": "requires-node-runtime",
"publishingBlockers": ["Uses request-time cookies and Server Actions"],
"features": ["Availability Scheduling", "Provider Reviews", "Auth"],
"tags": ["booking", "appointments", "marketplace", "scheduling", "calendly"],
"cover": "assets/covers/booking.png",
Expand All @@ -57,6 +65,8 @@
"description": "Build a streaming AI chat app with persisted conversations and file attachments. AI and object storage are provided by the application's InsForge runtime, so generated projects only need the public InsForge endpoint and anon key.",
"category": "AI",
"framework": "nextjs",
"publishingCompatibility": "requires-node-runtime",
"publishingBlockers": ["Uses API routes and request-time authentication"],
"features": [
"Streaming AI Chat",
"Conversation History",
Expand Down Expand Up @@ -87,6 +97,8 @@
"description": "An open-source thinking partner for your PDFs. Grounded in your own sources, with auto-generated mindmaps, spaced-repetition flashcards, two-host audio overviews, and RAG chat that highlights cited passages inside the source PDF. Self-hosted, your data, your model.",
"category": "AI",
"framework": "nextjs",
"publishingCompatibility": "requires-node-runtime",
"publishingBlockers": ["Uses middleware and API routes for RAG, auth, and document processing"],
"features": ["RAG", "react-pdf", "OpenAI TTS", "Better Auth"],
"tags": ["pdf", "study-tool", "notebooklm", "rag", "flashcards", "pgvector"],
"cover": "assets/covers/ai-pdf-chatbot.png",
Expand All @@ -100,6 +112,8 @@
"description": "Multi-user SaaS admin with workspace invites, sortable tables, real-time chat, charts, and a Composio-powered apps grid that pushes tasks to a chosen channel with two clicks. Vite + React + InsForge auth, database, RLS, storage, and realtime.",
"category": "admin",
"framework": "react",
"buildProfile": "vite-npm-v1",
"publishingCompatibility": "native",
"features": ["TanStack Table", "Recharts", "Real-time Chat", "Auth", "Composio Integrations", "Outbound Actions"],
"tags": ["admin", "dashboard", "shadcn", "tanstack-table", "workspace", "saas", "composio", "oauth"],
"cover": "assets/covers/admin-dashboard.png",
Expand All @@ -113,6 +127,8 @@
"description": "Your own Notion clone, no SaaS bill. Nested pages, BlockNote rich-text editor, image embeds, and shareable read-only links — perfect for internal wikis or docs sites.",
"category": "productivity",
"framework": "nextjs",
"publishingCompatibility": "requires-node-runtime",
"publishingBlockers": ["Uses request-time cookies, redirects, and Server Actions"],
"features": ["BlockNote", "Nested Pages", "Public Share Links"],
"tags": ["notion", "workspace", "docs", "wiki", "blocknote"],
"cover": "assets/covers/workspace.png",
Expand All @@ -126,6 +142,8 @@
"description": "ChatGPT 风格的 Insight Flow Agent 对话应用:登录用户在独立设置页保存 API Key 与 model/agent 参数,聊天页通过 InsForge Edge Function 流式呈现 OpenAI-compatible SSE。",
"category": "AI",
"framework": "react",
"buildProfile": "vite-npm-v1",
"publishingCompatibility": "native",
"features": [
"ChatGPT-style UI",
"Backend Agent Settings",
Expand Down Expand Up @@ -155,6 +173,8 @@
"description": "面向可追溯网页调研的 AI Agent:通过浏览器访问多个来源,生成带引用的结论,并将每条判断关联到原始证据,便于复核与事实核查。",
"category": "AI",
"framework": "react",
"buildProfile": "vite-npm-v1",
"publishingCompatibility": "native",
"features": [
"Browser-backed Research",
"Evidence Ledger",
Expand Down Expand Up @@ -183,6 +203,8 @@
"description": "持续监控重要网页:定期保存内容快照、对比前后变化,并由 AI 归纳变化内容、类型和优先级,减少无效提醒。",
"category": "productivity",
"framework": "react",
"buildProfile": "vite-npm-v1",
"publishingCompatibility": "native",
"features": [
"Scheduled Page Checks",
"Before and After Diffs",
Expand Down
1 change: 1 addition & 0 deletions scripts/__tests__/validate-registry.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ function entry(slug, overrides = {}) {
description: 'd',
category: 'ai',
framework: 'nextjs',
publishingCompatibility: 'unsupported',
features: [],
tags: [],
cover: `assets/covers/${slug}.png`,
Expand Down
21 changes: 20 additions & 1 deletion scripts/validate-registry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ const SCHEMA = {
description: { type: 'string', minLength: 1 },
category: { type: 'string', minLength: 1 },
framework: { type: 'string', minLength: 1 },
buildProfile: { enum: ['vite-npm-v1', 'vite-pnpm-v1', 'next-static-npm-v1', 'next-static-pnpm-v1'] },
publishingCompatibility: { enum: ['native', 'conversion-required', 'requires-node-runtime', 'reference-only', 'unsupported'] },
publishingBlockers: { type: 'array', uniqueItems: true, items: { type: 'string', minLength: 1 } },
features: { type: 'array', items: { type: 'string' } },
tags: { type: 'array', items: { type: 'string' } },
requiredCapabilities: {
Expand Down Expand Up @@ -148,15 +151,31 @@ export async function validateTemplate(entry, repoRoot) {
return { ok: false, errors };
}
const pkgPath = join(subdir, 'package.json');
let pkg;
if (!existsSync(pkgPath)) {
errors.push(`${entry.slug}/package.json: missing`);
} else {
try {
JSON.parse(readFileSync(pkgPath, 'utf8'));
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
} catch (e) {
errors.push(`${entry.slug}/package.json: invalid JSON (${e.message})`);
}
}
if (!entry.publishingCompatibility) {
errors.push(`${entry.slug}: publishingCompatibility is required`);
} else if (entry.publishingCompatibility === 'native') {
if (!entry.buildProfile) errors.push(`${entry.slug}: native templates require a controlled buildProfile`);
const pnpm = entry.buildProfile?.includes('-pnpm-');
const lockfile = pnpm ? 'pnpm-lock.yaml' : 'package-lock.json';
if (!existsSync(join(subdir, lockfile))) errors.push(`${entry.slug}/${lockfile}: required by ${entry.buildProfile}`);
if (pnpm && !pkg?.packageManager?.startsWith('pnpm@')) errors.push(`${entry.slug}: packageManager must pin pnpm for a pnpm build profile`);
const allowedBuildScripts = entry.buildProfile?.startsWith('vite-')
? ['vite build', 'tsc -b && vite build', 'vue-tsc -b && vite build']
: ['next build'];
if (!allowedBuildScripts.includes(pkg?.scripts?.build)) errors.push(`${entry.slug}: build script is not allowed by ${entry.buildProfile}`);
} else if ((entry.publishingCompatibility === 'requires-node-runtime' || entry.publishingCompatibility === 'conversion-required') && !entry.publishingBlockers?.length) {
errors.push(`${entry.slug}: non-native templates must explain their publishing blockers`);
}
if (!existsSync(join(subdir, 'LICENSE'))) {
errors.push(`${entry.slug}/LICENSE: missing`);
}
Expand Down
43 changes: 43 additions & 0 deletions scripts/verify-publishable-template.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
import { join } from 'node:path';

const slug = process.argv[2];
const root = process.cwd();
const registry = JSON.parse(readFileSync(join(root, 'registry.json'), 'utf8'));
const builtInProfiles = { react: 'vite-npm-v1', todo: 'next-static-npm-v1' };
const entry = registry.find((item) => item.slug === slug)
?? (builtInProfiles[slug] ? { slug, publishingCompatibility: 'native', buildProfile: builtInProfiles[slug] } : null);
if (!entry || entry.publishingCompatibility !== 'native') throw new Error(`${slug}: not a native template`);
const output = entry.buildProfile.startsWith('vite-') ? 'dist' : 'out';
const outputRoot = join(root, slug, output);
if (!existsSync(join(outputRoot, 'index.html'))) throw new Error(`${slug}: ${output}/index.html missing`);
const html = readFileSync(join(outputRoot, 'index.html'), 'utf8');
if (!html.includes('/.well-known/insforge-runtime-config.js')) throw new Error(`${slug}: runtime config script missing from production HTML`);
const isSPA = entry.buildProfile.startsWith('vite-');
if (!isSPA && !existsSync(join(outputRoot, '404.html'))) throw new Error(`${slug}: static export must provide 404.html for non-SPA deep links`);

const secretPatterns = [
/-----BEGIN [A-Z ]*PRIVATE KEY-----/,
/\b(?:ghp_|github_pat_)[A-Za-z0-9_]{20,}\b/,
/\b(?:sk_live_|xox[abp]-)[A-Za-z0-9-]{16,}\b/,
/\bik_[A-Za-z0-9_-]{24,}\b/,
];
let files = 0;
const visit = (directory) => {
for (const item of readdirSync(directory, { withFileTypes: true })) {
const file = join(directory, item.name);
if (item.isSymbolicLink()) throw new Error(`${slug}: production output contains symlink: ${file}`);
if (item.isDirectory()) visit(file);
else if (item.isFile()) {
files += 1;
const body = readFileSync(file);
if (body.length > 20 * 1024 * 1024) throw new Error(`${slug}: output file exceeds 20 MiB: ${file}`);
const text = body.toString('utf8');
if (secretPatterns.some((pattern) => pattern.test(text))) throw new Error(`${slug}: production output contains credential material: ${file}`);
}
}
};
visit(outputRoot);
if (!files || statSync(outputRoot).isSymbolicLink()) throw new Error(`${slug}: invalid production output`);
const deepLinkResult = isSPA ? 'index.html (SPA fallback)' : '404.html (ordinary static routing)';
console.log(`${slug}: ${entry.buildProfile} admission passed (${files} files in ${output}; / => index.html; /admission-deep-link => ${deepLinkResult})`);
2 changes: 2 additions & 0 deletions todo/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
output: "export",
images: { unoptimized: true },
webpack: (config, { dev }) => {
if (dev) {
config.cache = false;
Expand Down
7 changes: 5 additions & 2 deletions todo/src/app/insforge-client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { createClient } from "@insforge/sdk";

const runtime = typeof window === "undefined" ? undefined :
(window as Window & { __INSFORGE_RUNTIME_CONFIG__?: { apiBaseURL?: string; anonKey?: string } }).__INSFORGE_RUNTIME_CONFIG__;

export const insforge = createClient({
baseUrl: process.env.NEXT_PUBLIC_INSFORGE_URL!,
anonKey: process.env.NEXT_PUBLIC_INSFORGE_ANON_KEY!,
baseUrl: runtime?.apiBaseURL ?? process.env.NEXT_PUBLIC_INSFORGE_URL ?? "https://placeholder.invalid",
anonKey: runtime?.anonKey ?? process.env.NEXT_PUBLIC_INSFORGE_ANON_KEY ?? "anon_placeholder",
});
1 change: 1 addition & 0 deletions todo/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default function RootLayout({
}>) {
return (
<html lang="en" data-theme="dark">
<head><script src="/.well-known/insforge-runtime-config.js" /></head>
<body className="antialiased">{children}</body>
</html>
);
Expand Down
31 changes: 1 addition & 30 deletions todo/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,5 @@
import { TodoApp } from "./todo-app";

function getProjectIdFromLinkFile(): string | null {
try {
const fs = require("fs");
const path = require("path");
let dir = process.cwd();
const root = path.parse(dir).root;
while (dir !== root) {
const filePath = path.join(dir, ".insforge", "project.json");
if (fs.existsSync(filePath)) {
const content = JSON.parse(fs.readFileSync(filePath, "utf-8"));
return content.project_id ?? null;
}
dir = path.dirname(dir);
}
return null;
} catch {
return null;
}
}

function getDashboardUrl(): string {
const projectId = getProjectIdFromLinkFile();
if (projectId) {
return `https://insforge.dev/dashboard/project/${projectId}?route=/dashboard/database/tables`;
}
return "https://insforge.dev/dashboard";
}

export default function Home() {
const dashboardUrl = getDashboardUrl();
return <TodoApp dashboardUrl={dashboardUrl} />;
return <TodoApp dashboardUrl="https://insforge.dev/dashboard" />;
}
1 change: 1 addition & 0 deletions web-research-agent/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</head>
<body>
<div id="root"></div>
<script src="/.well-known/insforge-runtime-config.js"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
5 changes: 3 additions & 2 deletions web-research-agent/src/lib/insforge.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { createClient } from "@insforge/sdk";

const baseUrl = import.meta.env.VITE_INSFORGE_URL?.trim();
const anonKey = import.meta.env.VITE_INSFORGE_ANON_KEY?.trim();
const runtime = (window as Window & { __INSFORGE_RUNTIME_CONFIG__?: { apiBaseURL?: string; anonKey?: string } }).__INSFORGE_RUNTIME_CONFIG__;
const baseUrl = (runtime?.apiBaseURL ?? import.meta.env.VITE_INSFORGE_URL)?.trim();
const anonKey = (runtime?.anonKey ?? import.meta.env.VITE_INSFORGE_ANON_KEY)?.trim();

export const connected = Boolean(baseUrl && anonKey);
export const insforge = connected ? createClient({ baseUrl, anonKey }) : null;
1 change: 1 addition & 0 deletions website-change-monitor/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</head>
<body>
<div id="root"></div>
<script src="/.well-known/insforge-runtime-config.js"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Loading
Loading