@glinr/create-theauth-app 0.2.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/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +12 -0
- package/dist/chunk-Z6TQPZQK.js +170 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/package.json +47 -0
- package/templates/hono-mcp/.env.example +21 -0
- package/templates/hono-mcp/README.md +76 -0
- package/templates/hono-mcp/_gitignore +8 -0
- package/templates/hono-mcp/package.json +23 -0
- package/templates/hono-mcp/src/lib/kavach.ts +27 -0
- package/templates/hono-mcp/src/server.ts +192 -0
- package/templates/hono-mcp/src/tools.ts +35 -0
- package/templates/hono-mcp/tsconfig.json +18 -0
- package/templates/next-saas/.env.example +2 -0
- package/templates/next-saas/README.md +50 -0
- package/templates/next-saas/_gitignore +6 -0
- package/templates/next-saas/drizzle.config.ts +10 -0
- package/templates/next-saas/next.config.mjs +6 -0
- package/templates/next-saas/package.json +29 -0
- package/templates/next-saas/src/app/agents/page.tsx +106 -0
- package/templates/next-saas/src/app/api/auth/[...kavach]/route.ts +6 -0
- package/templates/next-saas/src/app/globals.css +12 -0
- package/templates/next-saas/src/app/layout.tsx +18 -0
- package/templates/next-saas/src/app/page.tsx +66 -0
- package/templates/next-saas/src/lib/kavach.ts +18 -0
- package/templates/next-saas/tsconfig.json +23 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TheAuth
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @glinr/create-theauth-app
|
|
2
|
+
|
|
3
|
+
Scaffold a TheAuth app in one command.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm create theauth-app@latest
|
|
7
|
+
# or
|
|
8
|
+
pnpm create theauth-app
|
|
9
|
+
# or
|
|
10
|
+
yarn create theauth-app
|
|
11
|
+
# or
|
|
12
|
+
bunx @glinr/create-theauth-app
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
You'll be asked for a project directory, a template, and a database driver. The CLI then writes the project, installs deps, and prints the next commands to run.
|
|
16
|
+
|
|
17
|
+
## Templates
|
|
18
|
+
|
|
19
|
+
| Template | Status | Stack |
|
|
20
|
+
| --- | --- | --- |
|
|
21
|
+
| `next-saas` | available | Next.js App Router · Drizzle · TheAuth auth |
|
|
22
|
+
| `hono-mcp` | available | Hono server · MCP OAuth 2.1 |
|
|
23
|
+
| `expo-mobile` | coming soon | Expo Router · React Native |
|
|
24
|
+
|
|
25
|
+
## Database drivers
|
|
26
|
+
|
|
27
|
+
- `better-sqlite3` (default): local SQLite, zero setup
|
|
28
|
+
- `pg`: Postgres (you provide the connection string)
|
|
29
|
+
|
|
30
|
+
## What you get
|
|
31
|
+
|
|
32
|
+
A working Next.js app with:
|
|
33
|
+
|
|
34
|
+
- `createKavach` already wired in `lib/auth.ts`
|
|
35
|
+
- Drizzle schema and migrations for the auth tables
|
|
36
|
+
- Sign-in / sign-up routes using the prebuilt React components
|
|
37
|
+
- `.env.example` with the secrets you need to fill in
|
|
38
|
+
|
|
39
|
+
## Next steps
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cd my-theauth-app
|
|
43
|
+
pnpm install
|
|
44
|
+
cp .env.example .env # then set THEAUTH_SECRET
|
|
45
|
+
pnpm db:push
|
|
46
|
+
pnpm dev
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Links
|
|
50
|
+
|
|
51
|
+
- TheAuth repo: <https://github.com/glincker/theauth>
|
|
52
|
+
- Docs: <https://docs.theauth.com>
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
MIT
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
import {
|
|
4
|
+
main
|
|
5
|
+
} from "./chunk-Z6TQPZQK.js";
|
|
6
|
+
|
|
7
|
+
// src/bin.ts
|
|
8
|
+
main().catch((err) => {
|
|
9
|
+
process.stderr.write(`${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
10
|
+
`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { basename, resolve } from "path";
|
|
5
|
+
import { exit } from "process";
|
|
6
|
+
import * as p from "@clack/prompts";
|
|
7
|
+
import { bold, green, yellow } from "kolorist";
|
|
8
|
+
|
|
9
|
+
// src/scaffold.ts
|
|
10
|
+
import { copyFile, mkdir, readdir, readFile, stat, writeFile } from "fs/promises";
|
|
11
|
+
import { dirname, join } from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
var DB_PACKAGE_VERSION = {
|
|
15
|
+
"better-sqlite3": "^11.0.0",
|
|
16
|
+
pg: "^8.13.0"
|
|
17
|
+
};
|
|
18
|
+
function replacePlaceholders(content, appName, dbDriver, dbUrl) {
|
|
19
|
+
const dbPkgVersion = DB_PACKAGE_VERSION[dbDriver] ?? "*";
|
|
20
|
+
return content.replaceAll("__APP_NAME__", appName).replaceAll("__DB_DRIVER__", dbDriver).replaceAll("__DB_DRIVER_VERSION__", dbPkgVersion).replaceAll("__DB_URL__", dbUrl);
|
|
21
|
+
}
|
|
22
|
+
function mapFilename(name) {
|
|
23
|
+
if (name === "_gitignore") return ".gitignore";
|
|
24
|
+
return name;
|
|
25
|
+
}
|
|
26
|
+
async function copyDir(src, dest, appName, dbDriver, dbUrl) {
|
|
27
|
+
await mkdir(dest, { recursive: true });
|
|
28
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const srcPath = join(src, entry.name);
|
|
31
|
+
const destName = mapFilename(replacePlaceholders(entry.name, appName, dbDriver, dbUrl));
|
|
32
|
+
const destPath = join(dest, destName);
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
await copyDir(srcPath, destPath, appName, dbDriver, dbUrl);
|
|
35
|
+
} else {
|
|
36
|
+
const raw = await readFile(srcPath, "utf-8");
|
|
37
|
+
const processed = replacePlaceholders(raw, appName, dbDriver, dbUrl);
|
|
38
|
+
await mkdir(dirname(destPath), { recursive: true });
|
|
39
|
+
await writeFile(destPath, processed, "utf-8");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function scaffold(opts) {
|
|
44
|
+
const templatesDir = join(__dirname, "..", "templates");
|
|
45
|
+
const templateSrc = join(templatesDir, opts.template);
|
|
46
|
+
const templateStat = await stat(templateSrc).catch(() => null);
|
|
47
|
+
if (!templateStat?.isDirectory()) {
|
|
48
|
+
throw new Error(`Template "${opts.template}" not found at ${templateSrc}`);
|
|
49
|
+
}
|
|
50
|
+
await copyDir(templateSrc, opts.targetDir, opts.appName, opts.dbDriver, opts.dbUrl);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/index.ts
|
|
54
|
+
function detectPackageManager() {
|
|
55
|
+
const ua = process.env.npm_config_user_agent ?? "";
|
|
56
|
+
if (ua.startsWith("pnpm")) return "pnpm";
|
|
57
|
+
if (ua.startsWith("yarn")) return "yarn";
|
|
58
|
+
if (ua.startsWith("bun")) return "bun";
|
|
59
|
+
return "npm";
|
|
60
|
+
}
|
|
61
|
+
function defaultDbUrl(driver) {
|
|
62
|
+
if (driver === "better-sqlite3") return "file:./kavach.db";
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
function printBanner() {
|
|
66
|
+
p.intro(`${bold(green("TheAuth"))} ${green("\xB7")} Auth OS for AI agents`);
|
|
67
|
+
}
|
|
68
|
+
function printNextSteps(targetDir, pm, template) {
|
|
69
|
+
const rel = targetDir.startsWith(process.cwd()) ? targetDir.slice(process.cwd().length + 1) : targetDir;
|
|
70
|
+
const runCmd = pm === "npm" ? "npm run" : pm;
|
|
71
|
+
const steps = [
|
|
72
|
+
`cd ${rel}`,
|
|
73
|
+
`${pm} install`,
|
|
74
|
+
`cp .env.example .env # then fill in THEAUTH_SECRET`
|
|
75
|
+
];
|
|
76
|
+
if (template === "next-saas") {
|
|
77
|
+
steps.push(`${runCmd} db:push`);
|
|
78
|
+
}
|
|
79
|
+
steps.push(`${runCmd} dev`);
|
|
80
|
+
p.note(steps.join("\n"), "Next steps");
|
|
81
|
+
p.outro(`${bold(green("Done."))} Happy building.`);
|
|
82
|
+
}
|
|
83
|
+
async function main() {
|
|
84
|
+
printBanner();
|
|
85
|
+
const answers = await p.group(
|
|
86
|
+
{
|
|
87
|
+
targetDir: () => p.text({
|
|
88
|
+
message: "Project directory",
|
|
89
|
+
placeholder: "./my-theauth-app",
|
|
90
|
+
defaultValue: "./my-theauth-app",
|
|
91
|
+
validate(value) {
|
|
92
|
+
if (!value || value.trim() === "") return "Directory is required";
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
}),
|
|
96
|
+
template: () => p.select({
|
|
97
|
+
message: "Template",
|
|
98
|
+
options: [
|
|
99
|
+
{
|
|
100
|
+
value: "next-saas",
|
|
101
|
+
label: "Next.js SaaS",
|
|
102
|
+
hint: "App Router \xB7 Drizzle \xB7 TheAuth auth"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
value: "hono-mcp",
|
|
106
|
+
label: "Hono MCP",
|
|
107
|
+
hint: "Hono server \xB7 MCP OAuth 2.1"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
value: "expo-mobile",
|
|
111
|
+
label: `Expo mobile ${yellow("(coming soon)")}`,
|
|
112
|
+
hint: "React Native \xB7 Expo Router"
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
}),
|
|
116
|
+
packageManager: () => p.select({
|
|
117
|
+
message: "Package manager",
|
|
118
|
+
initialValue: detectPackageManager(),
|
|
119
|
+
options: [
|
|
120
|
+
{ value: "pnpm", label: "pnpm" },
|
|
121
|
+
{ value: "npm", label: "npm" },
|
|
122
|
+
{ value: "yarn", label: "yarn" },
|
|
123
|
+
{ value: "bun", label: "bun" }
|
|
124
|
+
]
|
|
125
|
+
}),
|
|
126
|
+
dbDriver: () => p.select({
|
|
127
|
+
message: "Database",
|
|
128
|
+
options: [
|
|
129
|
+
{ value: "better-sqlite3", label: "SQLite", hint: "local \xB7 zero config" },
|
|
130
|
+
{ value: "pg", label: "Postgres", hint: "pg driver \xB7 set DATABASE_URL" }
|
|
131
|
+
]
|
|
132
|
+
})
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
onCancel() {
|
|
136
|
+
p.cancel("Cancelled.");
|
|
137
|
+
exit(0);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
const template = answers.template;
|
|
142
|
+
if (template === "expo-mobile") {
|
|
143
|
+
p.note(
|
|
144
|
+
`The ${bold(template)} template is not ready yet.
|
|
145
|
+
Pick ${bold("next-saas")} or ${bold("hono-mcp")} for now and stay tuned.`,
|
|
146
|
+
yellow("Coming soon")
|
|
147
|
+
);
|
|
148
|
+
exit(0);
|
|
149
|
+
}
|
|
150
|
+
const targetDir = resolve(answers.targetDir);
|
|
151
|
+
const appName = basename(targetDir);
|
|
152
|
+
const dbDriver = answers.dbDriver;
|
|
153
|
+
const dbUrl = defaultDbUrl(dbDriver);
|
|
154
|
+
const pm = answers.packageManager;
|
|
155
|
+
const spinner2 = p.spinner();
|
|
156
|
+
spinner2.start("Scaffolding project");
|
|
157
|
+
try {
|
|
158
|
+
await scaffold({ targetDir, template, appName, dbDriver, dbUrl });
|
|
159
|
+
spinner2.stop("Project created");
|
|
160
|
+
} catch (err) {
|
|
161
|
+
spinner2.stop("Scaffold failed");
|
|
162
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
163
|
+
exit(1);
|
|
164
|
+
}
|
|
165
|
+
printNextSteps(targetDir, pm, template);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export {
|
|
169
|
+
main
|
|
170
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@glinr/create-theauth-app",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Scaffold a TheAuth-powered app in seconds",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/glincker/theauth.git",
|
|
10
|
+
"directory": "packages/create-theauth-app"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://theauth.com",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"theauth",
|
|
15
|
+
"auth",
|
|
16
|
+
"agent",
|
|
17
|
+
"mcp",
|
|
18
|
+
"scaffold",
|
|
19
|
+
"cli"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"create-theauth-app": "./dist/bin.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"templates"
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@clack/prompts": "^1.2.0",
|
|
33
|
+
"kolorist": "^1.8.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.0.0",
|
|
37
|
+
"tsup": "^8.3.0",
|
|
38
|
+
"typescript": "^6.0.3",
|
|
39
|
+
"vitest": "^3.0.0"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsup src/index.ts src/bin.ts --format esm --dts --clean",
|
|
43
|
+
"dev": "tsup src/index.ts src/bin.ts --format esm --watch",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"typecheck": "tsc --noEmit"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Long random string used to sign sessions and access tokens. Generate one with:
|
|
2
|
+
# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
|
3
|
+
THEAUTH_SECRET=
|
|
4
|
+
|
|
5
|
+
# Signing secret for MCP JWT access tokens. Must be at least 32 chars.
|
|
6
|
+
MCP_SIGNING_SECRET=change-me-to-a-real-secret-at-least-32-chars
|
|
7
|
+
|
|
8
|
+
# Database driver. Set to "postgres" to use Postgres instead of local SQLite.
|
|
9
|
+
DB_PROVIDER=sqlite
|
|
10
|
+
|
|
11
|
+
# Database URL. Defaults match DB_PROVIDER.
|
|
12
|
+
# SQLite: file:./kavach.db
|
|
13
|
+
# Postgres: postgres://user:pass@host:5432/dbname
|
|
14
|
+
DATABASE_URL=__DB_URL__
|
|
15
|
+
|
|
16
|
+
# Base URL the server is reachable at. Appears in .well-known documents and
|
|
17
|
+
# access-token claims. Change to your public URL before deploying.
|
|
18
|
+
BASE_URL=http://localhost:3001
|
|
19
|
+
|
|
20
|
+
# Port to listen on.
|
|
21
|
+
PORT=3001
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# __APP_NAME__
|
|
2
|
+
|
|
3
|
+
A Hono server scaffolded with [TheAuth](https://theauth.com). Runs as an MCP OAuth 2.1 authorization server and a resource server in one process.
|
|
4
|
+
|
|
5
|
+
## What's included
|
|
6
|
+
|
|
7
|
+
- **Hono 4** on Node.js
|
|
8
|
+
- **TheAuth** for agent identity, session management, and the MCP OAuth 2.1 stack
|
|
9
|
+
- **Drizzle ORM** with __DB_DRIVER__ backing the auth schema
|
|
10
|
+
- `GET /tools/list` and `POST /tools/call/:name` protected with `withMcpAuth`
|
|
11
|
+
- `.well-known` discovery documents for MCP clients, served at the root
|
|
12
|
+
|
|
13
|
+
## Getting started
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cp .env.example .env
|
|
17
|
+
# Edit .env: set THEAUTH_SECRET to a 32+ byte random hex string
|
|
18
|
+
|
|
19
|
+
pnpm install
|
|
20
|
+
pnpm run dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
TheAuth creates the auth tables on first boot, no separate migration step for SQLite or Postgres.
|
|
24
|
+
|
|
25
|
+
The server listens on `http://localhost:3001`. Visit the root URL to see the MCP discovery links and the list of tools.
|
|
26
|
+
|
|
27
|
+
## Smoke test without an MCP client
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# 1. Create an agent with scoped MCP permissions
|
|
31
|
+
curl -X POST http://localhost:3001/api/agents \
|
|
32
|
+
-H "Content-Type: application/json" \
|
|
33
|
+
-d '{
|
|
34
|
+
"ownerId": "user-1",
|
|
35
|
+
"name": "demo-agent",
|
|
36
|
+
"type": "autonomous",
|
|
37
|
+
"permissions": [
|
|
38
|
+
{ "resource": "mcp:*", "actions": ["read", "execute"] }
|
|
39
|
+
]
|
|
40
|
+
}'
|
|
41
|
+
|
|
42
|
+
# Copy the kv_... token from the response, then call the protected route:
|
|
43
|
+
curl http://localhost:3001/tools/list \
|
|
44
|
+
-H "Authorization: Bearer kv_YOUR_TOKEN"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Environment variables
|
|
48
|
+
|
|
49
|
+
| Variable | Required | Notes |
|
|
50
|
+
|-------------------|----------|----------------------------------------------------------------------------|
|
|
51
|
+
| `THEAUTH_SECRET` | Yes | Long random string. Signs sessions and access tokens. |
|
|
52
|
+
| `DATABASE_URL` | Yes | SQLite: `file:./kavach.db`. Postgres: `postgres://user:pass@host:5432/db`. |
|
|
53
|
+
| `BASE_URL` | No | Public URL this server is reachable at. Defaults to `http://localhost:3001`. Set it before deploying so the `.well-known` documents and access-token audience match what clients use. |
|
|
54
|
+
| `PORT` | No | Port to bind. Defaults to `3001`. |
|
|
55
|
+
|
|
56
|
+
## Project structure
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
src/
|
|
60
|
+
server.ts Hono app, MCP mount, protected tool routes
|
|
61
|
+
tools.ts Example MCP tool list
|
|
62
|
+
lib/
|
|
63
|
+
kavach.ts Lazy createKavach singleton
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Deploying
|
|
67
|
+
|
|
68
|
+
Any Node runtime works. Set `BASE_URL` to your public URL before boot so MCP clients find the right endpoints, and set a real `THEAUTH_SECRET` out of band.
|
|
69
|
+
|
|
70
|
+
If you switch to Postgres, set `DB_PROVIDER=postgres` and point `DATABASE_URL` at your connection string. TheAuth creates the tables on boot.
|
|
71
|
+
|
|
72
|
+
## Next
|
|
73
|
+
|
|
74
|
+
- Replace the example tools in `src/tools.ts` with real implementations.
|
|
75
|
+
- Add per-tool permission constraints to the agents you create (rate limits, approval gates, IP allowlists) so agents cannot over-reach.
|
|
76
|
+
- Drop a frontend alongside this API, or consume it from any MCP-aware client.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__APP_NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx watch src/server.ts",
|
|
8
|
+
"start": "tsx src/server.ts",
|
|
9
|
+
"typecheck": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@hono/node-server": "^1.13.0",
|
|
13
|
+
"@glinr/theauth-hono": "^3.0.0",
|
|
14
|
+
"__DB_DRIVER__": "__DB_DRIVER_VERSION__",
|
|
15
|
+
"hono": "^4.12.0",
|
|
16
|
+
"@glinr/theauth": "^0.4.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^22.0.0",
|
|
20
|
+
"tsx": "^4.19.0",
|
|
21
|
+
"typescript": "^5.9.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createKavach } from "@glinr/theauth";
|
|
2
|
+
|
|
3
|
+
// Lazy singleton so imports do not open a DB connection before config is
|
|
4
|
+
// loaded. createKavach auto-creates the auth tables on first boot, no
|
|
5
|
+
// migration step required for development.
|
|
6
|
+
let instance: Awaited<ReturnType<typeof createKavach>> | null = null;
|
|
7
|
+
|
|
8
|
+
export async function getKavach() {
|
|
9
|
+
if (!instance) {
|
|
10
|
+
const provider = process.env["DB_PROVIDER"] === "postgres" ? "postgres" : "sqlite";
|
|
11
|
+
instance = await createKavach({
|
|
12
|
+
database: {
|
|
13
|
+
provider,
|
|
14
|
+
url: process.env["DATABASE_URL"] ?? "file:./kavach.db",
|
|
15
|
+
},
|
|
16
|
+
secret: process.env["THEAUTH_SECRET"] ?? "dev-secret-change-me-in-prod",
|
|
17
|
+
agents: {
|
|
18
|
+
enabled: true,
|
|
19
|
+
maxPerUser: 50,
|
|
20
|
+
defaultPermissions: [],
|
|
21
|
+
auditAll: true,
|
|
22
|
+
tokenExpiry: "24h",
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return instance;
|
|
27
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// __APP_NAME__
|
|
2
|
+
//
|
|
3
|
+
// A Hono server that runs as an MCP OAuth 2.1 authorization server and a
|
|
4
|
+
// resource server protecting your tools behind agent bearer tokens.
|
|
5
|
+
//
|
|
6
|
+
// Dev: pnpm dev
|
|
7
|
+
// Start: pnpm start
|
|
8
|
+
//
|
|
9
|
+
// Smoke test with a kv_ agent token (no full OAuth flow needed):
|
|
10
|
+
// curl -X POST http://localhost:3001/api/agents \
|
|
11
|
+
// -H "Content-Type: application/json" \
|
|
12
|
+
// -d '{"ownerId":"user-1","name":"demo","type":"autonomous",
|
|
13
|
+
// "permissions":[{"resource":"mcp:*","actions":["read","execute"]}]}'
|
|
14
|
+
//
|
|
15
|
+
// # Use the returned kv_ token to call the protected endpoint:
|
|
16
|
+
// curl http://localhost:3001/tools/list -H "Authorization: Bearer kv_..."
|
|
17
|
+
|
|
18
|
+
import { serve } from "@hono/node-server";
|
|
19
|
+
import { kavachHono } from "@glinr/theauth-hono";
|
|
20
|
+
import { Hono } from "hono";
|
|
21
|
+
import { users } from "@glinr/theauth";
|
|
22
|
+
import type { McpAccessToken, McpAuthModule, McpAuthorizationCode, McpClient } from "@glinr/theauth/mcp";
|
|
23
|
+
import { createMcpModule } from "@glinr/theauth/mcp";
|
|
24
|
+
|
|
25
|
+
import { getKavach } from "./lib/kavach.js";
|
|
26
|
+
import { MCP_TOOLS } from "./tools.js";
|
|
27
|
+
|
|
28
|
+
const PORT = Number(process.env["PORT"] ?? 3001);
|
|
29
|
+
const BASE_URL = process.env["BASE_URL"] ?? `http://localhost:${PORT}`;
|
|
30
|
+
const SIGNING_SECRET =
|
|
31
|
+
process.env["MCP_SIGNING_SECRET"] ?? "dev-mcp-signing-secret-at-least-32-chars-ok";
|
|
32
|
+
|
|
33
|
+
// In-memory MCP stores. In production, back these with your database or a
|
|
34
|
+
// cache layer. These are here so the scaffolded template boots without extra
|
|
35
|
+
// setup.
|
|
36
|
+
const clients = new Map<string, McpClient>();
|
|
37
|
+
const authCodes = new Map<string, McpAuthorizationCode>();
|
|
38
|
+
const tokens = new Map<string, McpAccessToken>();
|
|
39
|
+
const refreshTokenIndex = new Map<string, string>();
|
|
40
|
+
|
|
41
|
+
async function main(): Promise<void> {
|
|
42
|
+
const kavach = await getKavach();
|
|
43
|
+
|
|
44
|
+
// Seed a demo user so the /api/agents call in the smoke test has an owner.
|
|
45
|
+
try {
|
|
46
|
+
kavach.db
|
|
47
|
+
.insert(users)
|
|
48
|
+
.values({
|
|
49
|
+
id: "user-1",
|
|
50
|
+
email: "demo@__APP_NAME__.local",
|
|
51
|
+
name: "Demo User",
|
|
52
|
+
createdAt: new Date(),
|
|
53
|
+
updatedAt: new Date(),
|
|
54
|
+
})
|
|
55
|
+
.run();
|
|
56
|
+
} catch {
|
|
57
|
+
// Ignore on restart when the row already exists.
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const mcp: McpAuthModule = createMcpModule({
|
|
61
|
+
config: {
|
|
62
|
+
enabled: true,
|
|
63
|
+
issuer: BASE_URL,
|
|
64
|
+
baseUrl: `${BASE_URL}/api`,
|
|
65
|
+
signingSecret: SIGNING_SECRET,
|
|
66
|
+
scopes: ["mcp:read", "mcp:execute", "mcp:write"],
|
|
67
|
+
accessTokenTtl: 3600,
|
|
68
|
+
refreshTokenTtl: 86400,
|
|
69
|
+
},
|
|
70
|
+
storeClient: async (client) => {
|
|
71
|
+
clients.set(client.clientId, client);
|
|
72
|
+
},
|
|
73
|
+
findClient: async (clientId) => clients.get(clientId) ?? null,
|
|
74
|
+
storeAuthorizationCode: async (code) => {
|
|
75
|
+
authCodes.set(code.code, code);
|
|
76
|
+
},
|
|
77
|
+
consumeAuthorizationCode: async (code) => {
|
|
78
|
+
const found = authCodes.get(code) ?? null;
|
|
79
|
+
if (found) authCodes.delete(code);
|
|
80
|
+
return found;
|
|
81
|
+
},
|
|
82
|
+
storeToken: async (token) => {
|
|
83
|
+
tokens.set(token.accessToken, token);
|
|
84
|
+
if (token.refreshToken) {
|
|
85
|
+
refreshTokenIndex.set(token.refreshToken, token.accessToken);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
findTokenByRefreshToken: async (refreshToken) => {
|
|
89
|
+
const at = refreshTokenIndex.get(refreshToken);
|
|
90
|
+
return at ? (tokens.get(at) ?? null) : null;
|
|
91
|
+
},
|
|
92
|
+
revokeToken: async (accessToken) => {
|
|
93
|
+
const token = tokens.get(accessToken);
|
|
94
|
+
if (token?.refreshToken) refreshTokenIndex.delete(token.refreshToken);
|
|
95
|
+
tokens.delete(accessToken);
|
|
96
|
+
},
|
|
97
|
+
// Replace this with a real session lookup in production.
|
|
98
|
+
resolveUserId: async () => "user-1",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const app = new Hono();
|
|
102
|
+
|
|
103
|
+
// Mount the full TheAuth API (agents, audit, delegation) plus the MCP
|
|
104
|
+
// OAuth endpoints (.well-known, /mcp/register, /mcp/authorize, /mcp/token).
|
|
105
|
+
app.route("/api", kavachHono(kavach, { mcp }));
|
|
106
|
+
|
|
107
|
+
// Protected tool list. Accepts either a kv_ agent token or an MCP JWT.
|
|
108
|
+
app.get("/tools/list", async (c) => {
|
|
109
|
+
const token = c.req.header("authorization")?.replace("Bearer ", "");
|
|
110
|
+
if (!token) return c.json({ error: "Missing Authorization header" }, 401);
|
|
111
|
+
|
|
112
|
+
if (token.startsWith("kv_")) {
|
|
113
|
+
const result = await kavach.authorizeByToken(token, {
|
|
114
|
+
action: "read",
|
|
115
|
+
resource: "mcp:tools:list",
|
|
116
|
+
});
|
|
117
|
+
if (!result.allowed) {
|
|
118
|
+
return c.json({ error: "Forbidden", reason: result.reason }, 403);
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
const result = await mcp.validateToken(token, ["mcp:read"]);
|
|
122
|
+
if (!result.success) return c.json({ error: result.error.message }, 401);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return c.json({ tools: MCP_TOOLS });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Protected tool call.
|
|
129
|
+
app.post("/tools/call", async (c) => {
|
|
130
|
+
const token = c.req.header("authorization")?.replace("Bearer ", "");
|
|
131
|
+
if (!token) return c.json({ error: "Missing Authorization header" }, 401);
|
|
132
|
+
|
|
133
|
+
let body: { name?: string; arguments?: Record<string, unknown> };
|
|
134
|
+
try {
|
|
135
|
+
body = await c.req.json();
|
|
136
|
+
} catch {
|
|
137
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const toolName = body.name;
|
|
141
|
+
if (!toolName) return c.json({ error: "Missing tool name" }, 400);
|
|
142
|
+
const tool = MCP_TOOLS.find((t) => t.name === toolName);
|
|
143
|
+
if (!tool) return c.json({ error: `Unknown tool: ${toolName}` }, 404);
|
|
144
|
+
|
|
145
|
+
if (token.startsWith("kv_")) {
|
|
146
|
+
const result = await kavach.authorizeByToken(token, {
|
|
147
|
+
action: "execute",
|
|
148
|
+
resource: `mcp:tools:${toolName}`,
|
|
149
|
+
arguments: body.arguments,
|
|
150
|
+
});
|
|
151
|
+
if (!result.allowed) {
|
|
152
|
+
return c.json({ error: "Forbidden", reason: result.reason }, 403);
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
const result = await mcp.validateToken(token, ["mcp:execute"]);
|
|
156
|
+
if (!result.success) return c.json({ error: result.error.message }, 401);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Plug the real tool implementation in here.
|
|
160
|
+
return c.json({
|
|
161
|
+
content: [
|
|
162
|
+
{
|
|
163
|
+
type: "text",
|
|
164
|
+
text: `[simulated] ${toolName} called with args: ${JSON.stringify(body.arguments ?? {})}`,
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
app.get("/", (c) =>
|
|
171
|
+
c.json({
|
|
172
|
+
name: "__APP_NAME__",
|
|
173
|
+
mcp: {
|
|
174
|
+
authorization_server: `${BASE_URL}/.well-known/oauth-authorization-server`,
|
|
175
|
+
protected_resource: `${BASE_URL}/.well-known/oauth-protected-resource`,
|
|
176
|
+
register: `${BASE_URL}/api/mcp/register`,
|
|
177
|
+
authorize: `${BASE_URL}/api/mcp/authorize`,
|
|
178
|
+
token: `${BASE_URL}/api/mcp/token`,
|
|
179
|
+
},
|
|
180
|
+
tools: MCP_TOOLS.map((t) => t.name),
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
serve({ fetch: app.fetch, port: PORT }, (info) => {
|
|
185
|
+
console.log(`__APP_NAME__ listening on http://localhost:${info.port}`);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
main().catch((err: unknown) => {
|
|
190
|
+
console.error("Fatal error:", err);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Example MCP tool definitions. Replace with the tools your server
|
|
2
|
+
// actually wants to expose to AI agents. Each tool's name becomes the
|
|
3
|
+
// last segment of the permission resource, for example `mcp:file:read`
|
|
4
|
+
// maps to the `file:read` tool here.
|
|
5
|
+
|
|
6
|
+
export interface Tool {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
inputSchema: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const MCP_TOOLS: Tool[] = [
|
|
13
|
+
{
|
|
14
|
+
name: "list_files",
|
|
15
|
+
description: "List files in a directory",
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
path: { type: "string", description: "Directory path" },
|
|
20
|
+
},
|
|
21
|
+
required: ["path"],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "read_file",
|
|
26
|
+
description: "Read the contents of a file",
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
path: { type: "string", description: "File path" },
|
|
31
|
+
},
|
|
32
|
+
required: ["path"],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noUncheckedIndexedAccess": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"noEmit": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# __APP_NAME__
|
|
2
|
+
|
|
3
|
+
A Next.js SaaS starter with [TheAuth](https://theauth.com) auth built in.
|
|
4
|
+
|
|
5
|
+
## What's included
|
|
6
|
+
|
|
7
|
+
- **Next.js 15** with App Router and TypeScript
|
|
8
|
+
- **TheAuth** for agent identity, session management, and auth
|
|
9
|
+
- **Drizzle ORM** with **__DB_DRIVER__**: schema-first, type-safe
|
|
10
|
+
- Sign-in / sign-out flow wired to `/api/auth/[...kavach]`
|
|
11
|
+
- An agents list page at `/agents`
|
|
12
|
+
|
|
13
|
+
## Getting started
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cp .env.example .env
|
|
17
|
+
# Edit .env: set THEAUTH_SECRET and DATABASE_URL
|
|
18
|
+
|
|
19
|
+
pnpm install
|
|
20
|
+
pnpm run db:push
|
|
21
|
+
pnpm run dev
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Open [http://localhost:3000](http://localhost:3000).
|
|
25
|
+
|
|
26
|
+
## Environment variables
|
|
27
|
+
|
|
28
|
+
| Variable | Required | Notes |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `THEAUTH_SECRET` | Yes | Long random string, used to sign sessions |
|
|
31
|
+
| `DATABASE_URL` | Yes | SQLite: `file:./kavach.db` · Postgres: connection string |
|
|
32
|
+
|
|
33
|
+
## Project structure
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
src/
|
|
37
|
+
app/
|
|
38
|
+
api/auth/[...kavach]/route.ts : TheAuth HTTP handler
|
|
39
|
+
agents/page.tsx : agent list UI
|
|
40
|
+
layout.tsx : KavachProvider wrapper
|
|
41
|
+
page.tsx : home / landing
|
|
42
|
+
lib/
|
|
43
|
+
kavach.ts : singleton TheAuth instance
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Deploying
|
|
47
|
+
|
|
48
|
+
Any Node.js host works. For Vercel, add `THEAUTH_SECRET` and `DATABASE_URL` in project settings, then push.
|
|
49
|
+
|
|
50
|
+
If you switch to Postgres, update `DATABASE_URL` and run `pnpm run db:push` to apply migrations.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineConfig } from "drizzle-kit";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
dialect: "__DB_DRIVER__" === "pg" ? "postgresql" : "sqlite",
|
|
5
|
+
schema: "./src/lib/schema.ts",
|
|
6
|
+
out: "./drizzle",
|
|
7
|
+
dbCredentials: {
|
|
8
|
+
url: process.env["DATABASE_URL"] ?? "file:./kavach.db",
|
|
9
|
+
},
|
|
10
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__APP_NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "next dev",
|
|
8
|
+
"build": "next build",
|
|
9
|
+
"start": "next start",
|
|
10
|
+
"db:push": "drizzle-kit push"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@glinr/theauth-nextjs": "^2.0.0",
|
|
14
|
+
"@glinr/theauth-react": "^0.2.0",
|
|
15
|
+
"__DB_DRIVER__": "__DB_DRIVER_VERSION__",
|
|
16
|
+
"drizzle-orm": "^0.40.0",
|
|
17
|
+
"@glinr/theauth": "^0.3.0",
|
|
18
|
+
"next": "^15.0.0",
|
|
19
|
+
"react": "^19.0.0",
|
|
20
|
+
"react-dom": "^19.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.0.0",
|
|
24
|
+
"@types/react": "^19.0.0",
|
|
25
|
+
"@types/react-dom": "^19.0.0",
|
|
26
|
+
"drizzle-kit": "^0.30.0",
|
|
27
|
+
"typescript": "^5.9.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSession } from "@glinr/theauth-react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
|
|
7
|
+
interface Agent {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
type: string;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function AgentsPage() {
|
|
15
|
+
const { session } = useSession();
|
|
16
|
+
const [agents, setAgents] = useState<Agent[]>([]);
|
|
17
|
+
const [loading, setLoading] = useState(true);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!session?.user) {
|
|
21
|
+
setLoading(false);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fetch("/api/agents")
|
|
26
|
+
.then((r) => r.json())
|
|
27
|
+
.then((data: { agents: Agent[] }) => setAgents(data.agents ?? []))
|
|
28
|
+
.catch(() => setAgents([]))
|
|
29
|
+
.finally(() => setLoading(false));
|
|
30
|
+
}, [session]);
|
|
31
|
+
|
|
32
|
+
if (!session?.user) {
|
|
33
|
+
return (
|
|
34
|
+
<main style={pageStyle}>
|
|
35
|
+
<p>
|
|
36
|
+
<Link href="/api/auth/sign-in">Sign in</Link> to manage your agents.
|
|
37
|
+
</p>
|
|
38
|
+
</main>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<main style={pageStyle}>
|
|
44
|
+
<h1 style={{ fontSize: "1.75rem", fontWeight: 700 }}>Your agents</h1>
|
|
45
|
+
|
|
46
|
+
{loading ? (
|
|
47
|
+
<p style={{ color: "#888" }}>Loading…</p>
|
|
48
|
+
) : agents.length === 0 ? (
|
|
49
|
+
<div style={{ textAlign: "center", color: "#888", marginTop: "4rem" }}>
|
|
50
|
+
<p style={{ fontSize: "1.1rem" }}>No agents yet.</p>
|
|
51
|
+
<Link
|
|
52
|
+
href="/agents/new"
|
|
53
|
+
style={{
|
|
54
|
+
display: "inline-block",
|
|
55
|
+
marginTop: "1rem",
|
|
56
|
+
padding: "0.6rem 1.4rem",
|
|
57
|
+
borderRadius: "0.5rem",
|
|
58
|
+
background: "#C9A84C",
|
|
59
|
+
color: "#0a0a0a",
|
|
60
|
+
fontWeight: 600,
|
|
61
|
+
textDecoration: "none",
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
Create first agent
|
|
65
|
+
</Link>
|
|
66
|
+
</div>
|
|
67
|
+
) : (
|
|
68
|
+
<ul style={{ listStyle: "none", padding: 0, margin: 0, width: "100%", maxWidth: "40rem" }}>
|
|
69
|
+
{agents.map((agent) => (
|
|
70
|
+
<li
|
|
71
|
+
key={agent.id}
|
|
72
|
+
style={{
|
|
73
|
+
padding: "1rem",
|
|
74
|
+
borderRadius: "0.5rem",
|
|
75
|
+
background: "#111",
|
|
76
|
+
marginBottom: "0.75rem",
|
|
77
|
+
display: "flex",
|
|
78
|
+
justifyContent: "space-between",
|
|
79
|
+
alignItems: "center",
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
<div>
|
|
83
|
+
<strong>{agent.name}</strong>
|
|
84
|
+
<span style={{ marginLeft: "0.5rem", color: "#888", fontSize: "0.85rem" }}>
|
|
85
|
+
{agent.type}
|
|
86
|
+
</span>
|
|
87
|
+
</div>
|
|
88
|
+
<span style={{ color: "#555", fontSize: "0.8rem" }}>
|
|
89
|
+
{new Date(agent.createdAt).toLocaleDateString()}
|
|
90
|
+
</span>
|
|
91
|
+
</li>
|
|
92
|
+
))}
|
|
93
|
+
</ul>
|
|
94
|
+
)}
|
|
95
|
+
</main>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const pageStyle: React.CSSProperties = {
|
|
100
|
+
minHeight: "100vh",
|
|
101
|
+
display: "flex",
|
|
102
|
+
flexDirection: "column",
|
|
103
|
+
alignItems: "center",
|
|
104
|
+
padding: "4rem 2rem",
|
|
105
|
+
gap: "1.5rem",
|
|
106
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { KavachProvider } from "@glinr/theauth-react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import "./globals.css";
|
|
4
|
+
|
|
5
|
+
export const metadata = {
|
|
6
|
+
title: "__APP_NAME__",
|
|
7
|
+
description: "Powered by TheAuth",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
11
|
+
return (
|
|
12
|
+
<html lang="en">
|
|
13
|
+
<body>
|
|
14
|
+
<KavachProvider basePath="/api/auth">{children}</KavachProvider>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSession } from "@glinr/theauth-react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
|
|
6
|
+
export default function HomePage() {
|
|
7
|
+
const { session, isLoading } = useSession();
|
|
8
|
+
const user = session?.user ?? null;
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<main
|
|
12
|
+
style={{
|
|
13
|
+
minHeight: "100vh",
|
|
14
|
+
display: "flex",
|
|
15
|
+
flexDirection: "column",
|
|
16
|
+
alignItems: "center",
|
|
17
|
+
justifyContent: "center",
|
|
18
|
+
gap: "1.5rem",
|
|
19
|
+
padding: "2rem",
|
|
20
|
+
textAlign: "center",
|
|
21
|
+
}}
|
|
22
|
+
>
|
|
23
|
+
<h1 style={{ fontSize: "2.5rem", fontWeight: 700, margin: 0 }}>__APP_NAME__</h1>
|
|
24
|
+
<p style={{ color: "#888", margin: 0, maxWidth: "40ch" }}>
|
|
25
|
+
Built on TheAuth — agent-first auth for the modern web.
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
<div style={{ display: "flex", gap: "1rem", flexWrap: "wrap", justifyContent: "center" }}>
|
|
29
|
+
{isLoading ? null : user ? (
|
|
30
|
+
<>
|
|
31
|
+
<span style={{ color: "#888" }}>Signed in as {user.email}</span>
|
|
32
|
+
<Link href="/api/auth/sign-out" style={ctaStyle("#333", "#ededed")}>
|
|
33
|
+
Sign out
|
|
34
|
+
</Link>
|
|
35
|
+
</>
|
|
36
|
+
) : (
|
|
37
|
+
<Link href="/api/auth/sign-in" style={ctaStyle("#C9A84C", "#0a0a0a")}>
|
|
38
|
+
Sign in
|
|
39
|
+
</Link>
|
|
40
|
+
)}
|
|
41
|
+
|
|
42
|
+
<Link
|
|
43
|
+
href={user ? "/agents" : "#"}
|
|
44
|
+
aria-disabled={!user}
|
|
45
|
+
style={ctaStyle(user ? "#C9A84C" : "#333", user ? "#0a0a0a" : "#666")}
|
|
46
|
+
onClick={!user ? (e) => e.preventDefault() : undefined}
|
|
47
|
+
>
|
|
48
|
+
{user ? "My agents" : "Sign in to spawn an agent"}
|
|
49
|
+
</Link>
|
|
50
|
+
</div>
|
|
51
|
+
</main>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ctaStyle(bg: string, color: string): React.CSSProperties {
|
|
56
|
+
return {
|
|
57
|
+
display: "inline-block",
|
|
58
|
+
padding: "0.6rem 1.4rem",
|
|
59
|
+
borderRadius: "0.5rem",
|
|
60
|
+
background: bg,
|
|
61
|
+
color,
|
|
62
|
+
fontWeight: 600,
|
|
63
|
+
textDecoration: "none",
|
|
64
|
+
fontSize: "0.95rem",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createKavach } from "@glinr/theauth";
|
|
2
|
+
|
|
3
|
+
// Initialised lazily so Next.js doesn't try to open a DB connection at build
|
|
4
|
+
// time. Import `getKavach()` from your server components or route handlers.
|
|
5
|
+
let instance: Awaited<ReturnType<typeof createKavach>> | null = null;
|
|
6
|
+
|
|
7
|
+
export async function getKavach() {
|
|
8
|
+
if (!instance) {
|
|
9
|
+
instance = await createKavach({
|
|
10
|
+
database: {
|
|
11
|
+
provider: "__DB_DRIVER__" === "pg" ? "postgres" : "sqlite",
|
|
12
|
+
url: process.env["DATABASE_URL"] ?? "file:./kavach.db",
|
|
13
|
+
},
|
|
14
|
+
secret: process.env["THEAUTH_SECRET"] ?? "dev-secret-change-me",
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return instance;
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{ "name": "next" }],
|
|
17
|
+
"paths": {
|
|
18
|
+
"@/*": ["./src/*"]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
22
|
+
"exclude": ["node_modules"]
|
|
23
|
+
}
|