@insureco/cli 0.1.10 → 0.1.11

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.
Files changed (49) hide show
  1. package/dist/commands/deploy.d.ts.map +1 -1
  2. package/dist/commands/deploy.js +6 -52
  3. package/dist/commands/deploy.js.map +1 -1
  4. package/dist/commands/push.d.ts.map +1 -1
  5. package/dist/commands/push.js +10 -1
  6. package/dist/commands/push.js.map +1 -1
  7. package/dist/commands/rollback.d.ts.map +1 -1
  8. package/dist/commands/rollback.js +10 -35
  9. package/dist/commands/rollback.js.map +1 -1
  10. package/dist/commands/sample.d.ts +2 -0
  11. package/dist/commands/sample.d.ts.map +1 -1
  12. package/dist/commands/sample.js +140 -12
  13. package/dist/commands/sample.js.map +1 -1
  14. package/dist/commands/versions.d.ts.map +1 -1
  15. package/dist/commands/versions.js +10 -56
  16. package/dist/commands/versions.js.map +1 -1
  17. package/dist/index.js +2 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/lib/builder.d.ts.map +1 -1
  20. package/dist/lib/builder.js +46 -3
  21. package/dist/lib/builder.js.map +1 -1
  22. package/dist/lib/watch.d.ts +26 -0
  23. package/dist/lib/watch.d.ts.map +1 -0
  24. package/dist/lib/watch.js +136 -0
  25. package/dist/lib/watch.js.map +1 -0
  26. package/dist/templates/nextjs-oauth/.env.example +14 -0
  27. package/dist/templates/nextjs-oauth/Dockerfile +51 -0
  28. package/dist/templates/nextjs-oauth/README.md +128 -0
  29. package/dist/templates/nextjs-oauth/catalog-info.yaml +17 -0
  30. package/dist/templates/nextjs-oauth/helm/{{name}}/Chart.yaml +9 -0
  31. package/dist/templates/nextjs-oauth/helm/{{name}}/templates/deployment.yaml +68 -0
  32. package/dist/templates/nextjs-oauth/helm/{{name}}/templates/service.yaml +17 -0
  33. package/dist/templates/nextjs-oauth/helm/{{name}}/values.yaml +51 -0
  34. package/dist/templates/nextjs-oauth/next.config.js +23 -0
  35. package/dist/templates/nextjs-oauth/package.json +30 -0
  36. package/dist/templates/nextjs-oauth/public/.gitkeep +0 -0
  37. package/dist/templates/nextjs-oauth/src/app/api/auth/callback/route.ts +64 -0
  38. package/dist/templates/nextjs-oauth/src/app/api/auth/login/route.ts +16 -0
  39. package/dist/templates/nextjs-oauth/src/app/api/auth/logout/route.ts +9 -0
  40. package/dist/templates/nextjs-oauth/src/app/api/auth/session/route.ts +40 -0
  41. package/dist/templates/nextjs-oauth/src/app/api/example/route.ts +63 -0
  42. package/dist/templates/nextjs-oauth/src/app/api/health/route.ts +10 -0
  43. package/dist/templates/nextjs-oauth/src/app/dashboard/page.tsx +92 -0
  44. package/dist/templates/nextjs-oauth/src/app/layout.tsx +18 -0
  45. package/dist/templates/nextjs-oauth/src/app/page.tsx +110 -0
  46. package/dist/templates/nextjs-oauth/src/lib/auth.ts +274 -0
  47. package/dist/templates/nextjs-oauth/src/middleware.ts +70 -0
  48. package/dist/templates/nextjs-oauth/tsconfig.json +26 -0
  49. package/package.json +1 -1
@@ -0,0 +1,68 @@
1
+ apiVersion: apps/v1
2
+ kind: Deployment
3
+ metadata:
4
+ name: {{ .Release.Name }}
5
+ labels:
6
+ app: {{ .Release.Name }}
7
+ app.kubernetes.io/name: {{ .Release.Name }}
8
+ app.kubernetes.io/instance: {{ .Release.Name }}
9
+ spec:
10
+ replicas: {{ .Values.replicaCount }}
11
+ selector:
12
+ matchLabels:
13
+ app: {{ .Release.Name }}
14
+ template:
15
+ metadata:
16
+ labels:
17
+ app: {{ .Release.Name }}
18
+ app.kubernetes.io/name: {{ .Release.Name }}
19
+ app.kubernetes.io/instance: {{ .Release.Name }}
20
+ spec:
21
+ {{- with .Values.imagePullSecrets }}
22
+ imagePullSecrets:
23
+ {{- toYaml . | nindent 8 }}
24
+ {{- end }}
25
+ containers:
26
+ - name: {{ .Chart.Name }}
27
+ image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
28
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
29
+ ports:
30
+ - name: http
31
+ containerPort: {{ .Values.service.port }}
32
+ protocol: TCP
33
+ envFrom:
34
+ - configMapRef:
35
+ name: {{ .Release.Name }}-config
36
+ optional: true
37
+ - secretRef:
38
+ name: {{ .Release.Name }}-secrets
39
+ optional: true
40
+ {{- with .Values.env }}
41
+ env:
42
+ {{- range $key, $value := . }}
43
+ - name: {{ $key }}
44
+ value: {{ $value | quote }}
45
+ {{- end }}
46
+ {{- end }}
47
+ {{- with .Values.livenessProbe }}
48
+ livenessProbe:
49
+ {{- toYaml . | nindent 12 }}
50
+ {{- end }}
51
+ {{- with .Values.readinessProbe }}
52
+ readinessProbe:
53
+ {{- toYaml . | nindent 12 }}
54
+ {{- end }}
55
+ resources:
56
+ {{- toYaml .Values.resources | nindent 12 }}
57
+ {{- with .Values.nodeSelector }}
58
+ nodeSelector:
59
+ {{- toYaml . | nindent 8 }}
60
+ {{- end }}
61
+ {{- with .Values.affinity }}
62
+ affinity:
63
+ {{- toYaml . | nindent 8 }}
64
+ {{- end }}
65
+ {{- with .Values.tolerations }}
66
+ tolerations:
67
+ {{- toYaml . | nindent 8 }}
68
+ {{- end }}
@@ -0,0 +1,17 @@
1
+ apiVersion: v1
2
+ kind: Service
3
+ metadata:
4
+ name: {{ .Release.Name }}
5
+ labels:
6
+ app: {{ .Release.Name }}
7
+ app.kubernetes.io/name: {{ .Release.Name }}
8
+ app.kubernetes.io/instance: {{ .Release.Name }}
9
+ spec:
10
+ type: {{ .Values.service.type }}
11
+ ports:
12
+ - port: {{ .Values.service.port }}
13
+ targetPort: http
14
+ protocol: TCP
15
+ name: http
16
+ selector:
17
+ app: {{ .Release.Name }}
@@ -0,0 +1,51 @@
1
+ replicaCount: 1
2
+
3
+ image:
4
+ repository: registry.digitalocean.com/insureco/{{name}}
5
+ tag: latest
6
+ pullPolicy: IfNotPresent
7
+
8
+ imagePullSecrets:
9
+ - name: do-registry
10
+
11
+ service:
12
+ type: ClusterIP
13
+ port: 3000
14
+
15
+ resources:
16
+ limits:
17
+ cpu: 500m
18
+ memory: 512Mi
19
+ requests:
20
+ cpu: 100m
21
+ memory: 128Mi
22
+
23
+ autoscaling:
24
+ enabled: false
25
+ minReplicas: 1
26
+ maxReplicas: 5
27
+ targetCPUUtilizationPercentage: 80
28
+
29
+ nodeSelector: {}
30
+
31
+ tolerations: []
32
+
33
+ affinity: {}
34
+
35
+ env: {}
36
+
37
+ secrets: {}
38
+
39
+ livenessProbe:
40
+ httpGet:
41
+ path: /api/health
42
+ port: 3000
43
+ initialDelaySeconds: 15
44
+ periodSeconds: 10
45
+
46
+ readinessProbe:
47
+ httpGet:
48
+ path: /api/health
49
+ port: 3000
50
+ initialDelaySeconds: 5
51
+ periodSeconds: 5
@@ -0,0 +1,23 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ output: 'standalone',
4
+ reactStrictMode: true,
5
+ poweredByHeader: false,
6
+
7
+ // Enable experimental features as needed
8
+ // experimental: {
9
+ // serverActions: true,
10
+ // },
11
+
12
+ // Configure allowed image domains
13
+ // images: {
14
+ // remotePatterns: [
15
+ // {
16
+ // protocol: 'https',
17
+ // hostname: '*.insureco.io',
18
+ // },
19
+ // ],
20
+ // },
21
+ }
22
+
23
+ module.exports = nextConfig
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "version": "0.1.0",
4
+ "description": "{{description}}",
5
+ "private": true,
6
+ "scripts": {
7
+ "dev": "next dev",
8
+ "build": "next build",
9
+ "start": "next start",
10
+ "lint": "next lint",
11
+ "typecheck": "tsc --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "next": "^14.2.0",
15
+ "react": "^18.3.0",
16
+ "react-dom": "^18.3.0",
17
+ "jose": "^5.9.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^22.0.0",
21
+ "@types/react": "^18.3.0",
22
+ "@types/react-dom": "^18.3.0",
23
+ "eslint": "^9.0.0",
24
+ "eslint-config-next": "^14.2.0",
25
+ "typescript": "^5.7.0"
26
+ },
27
+ "engines": {
28
+ "node": ">=20.0.0"
29
+ }
30
+ }
File without changes
@@ -0,0 +1,64 @@
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import { cookies } from 'next/headers'
3
+ import { exchangeCodeForTokens, setAuthCookies, sanitizeReturnTo } from '@/lib/auth'
4
+
5
+ const APP_URL = process.env.APP_URL || 'http://localhost:3000'
6
+
7
+ export async function GET(request: NextRequest) {
8
+ const searchParams = request.nextUrl.searchParams
9
+ const code = searchParams.get('code')
10
+ const state = searchParams.get('state')
11
+ const error = searchParams.get('error')
12
+ const errorDescription = searchParams.get('error_description')
13
+
14
+ const cookieStore = await cookies()
15
+ const storedState = cookieStore.get('oauth_state')?.value
16
+ const codeVerifier = cookieStore.get('oauth_code_verifier')?.value
17
+ const returnTo = cookieStore.get('oauth_return_to')?.value || '/dashboard'
18
+
19
+ // Clear OAuth flow cookies
20
+ cookieStore.delete('oauth_state')
21
+ cookieStore.delete('oauth_code_verifier')
22
+ cookieStore.delete('oauth_return_to')
23
+
24
+ // Handle error from authorization server
25
+ if (error) {
26
+ const errorUrl = new URL('/', APP_URL)
27
+ errorUrl.searchParams.set('error', error)
28
+ if (errorDescription) {
29
+ errorUrl.searchParams.set('error_description', errorDescription)
30
+ }
31
+ return NextResponse.redirect(errorUrl)
32
+ }
33
+
34
+ // Validate state to prevent CSRF
35
+ if (!state || state !== storedState) {
36
+ const errorUrl = new URL('/', APP_URL)
37
+ errorUrl.searchParams.set('error', 'invalid_state')
38
+ errorUrl.searchParams.set('error_description', 'State parameter mismatch')
39
+ return NextResponse.redirect(errorUrl)
40
+ }
41
+
42
+ // Validate required params
43
+ if (!code || !codeVerifier) {
44
+ const errorUrl = new URL('/', APP_URL)
45
+ errorUrl.searchParams.set('error', 'invalid_request')
46
+ errorUrl.searchParams.set('error_description', 'Missing authorization code')
47
+ return NextResponse.redirect(errorUrl)
48
+ }
49
+
50
+ try {
51
+ const tokens = await exchangeCodeForTokens(code, codeVerifier)
52
+ await setAuthCookies(tokens.access_token, tokens.refresh_token, tokens.expires_in)
53
+ const safeReturnTo = sanitizeReturnTo(returnTo)
54
+ return NextResponse.redirect(new URL(safeReturnTo, APP_URL))
55
+ } catch (err) {
56
+ const errorUrl = new URL('/', APP_URL)
57
+ errorUrl.searchParams.set('error', 'token_exchange_failed')
58
+ errorUrl.searchParams.set(
59
+ 'error_description',
60
+ err instanceof Error ? err.message : 'Failed to exchange authorization code'
61
+ )
62
+ return NextResponse.redirect(errorUrl)
63
+ }
64
+ }
@@ -0,0 +1,16 @@
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import { getLoginUrl } from '@/lib/auth'
3
+
4
+ export async function GET(request: NextRequest) {
5
+ const returnTo = request.nextUrl.searchParams.get('returnTo') || '/dashboard'
6
+
7
+ try {
8
+ const loginUrl = await getLoginUrl(returnTo)
9
+ return NextResponse.redirect(loginUrl)
10
+ } catch (error) {
11
+ const message = error instanceof Error ? error.message : 'Login failed'
12
+ return NextResponse.redirect(
13
+ new URL(`/?error=${encodeURIComponent(message)}`, request.url)
14
+ )
15
+ }
16
+ }
@@ -0,0 +1,9 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { clearAuthCookies } from '@/lib/auth'
3
+
4
+ const APP_URL = process.env.APP_URL || 'http://localhost:3000'
5
+
6
+ export async function POST() {
7
+ await clearAuthCookies()
8
+ return NextResponse.redirect(new URL('/', APP_URL), { status: 303 })
9
+ }
@@ -0,0 +1,40 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { cookies } from 'next/headers'
3
+ import {
4
+ getCurrentUser,
5
+ refreshAccessToken,
6
+ setAuthCookies,
7
+ fetchUserInfo,
8
+ COOKIE_PREFIX,
9
+ } from '@/lib/auth'
10
+
11
+ const NO_CACHE_HEADERS = { 'Cache-Control': 'no-store, no-cache, must-revalidate' }
12
+
13
+ export async function GET() {
14
+ try {
15
+ const user = await getCurrentUser()
16
+
17
+ if (user) {
18
+ return NextResponse.json({ user }, { headers: NO_CACHE_HEADERS })
19
+ }
20
+
21
+ // Access token expired or missing — try refresh
22
+ const cookieStore = await cookies()
23
+ const refreshToken = cookieStore.get(`${COOKIE_PREFIX}_refresh_token`)?.value
24
+
25
+ if (refreshToken) {
26
+ try {
27
+ const tokens = await refreshAccessToken(refreshToken)
28
+ await setAuthCookies(tokens.access_token, tokens.refresh_token, tokens.expires_in)
29
+ const refreshedUser = await fetchUserInfo(tokens.access_token)
30
+ return NextResponse.json({ user: refreshedUser }, { headers: NO_CACHE_HEADERS })
31
+ } catch {
32
+ // Refresh token also invalid — user must re-login
33
+ }
34
+ }
35
+
36
+ return NextResponse.json({ user: null }, { headers: NO_CACHE_HEADERS })
37
+ } catch {
38
+ return NextResponse.json({ user: null }, { headers: NO_CACHE_HEADERS })
39
+ }
40
+ }
@@ -0,0 +1,63 @@
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+
3
+ // In-memory storage for demo
4
+ const items: Map<string, { id: string; name: string; createdAt: string }> = new Map()
5
+
6
+ export async function GET() {
7
+ const itemList = Array.from(items.values())
8
+ return NextResponse.json({
9
+ success: true,
10
+ data: itemList,
11
+ meta: {
12
+ total: itemList.length,
13
+ timestamp: new Date().toISOString(),
14
+ },
15
+ })
16
+ }
17
+
18
+ export async function POST(request: NextRequest) {
19
+ try {
20
+ const body = await request.json()
21
+
22
+ if (!body.name || typeof body.name !== 'string') {
23
+ return NextResponse.json(
24
+ {
25
+ success: false,
26
+ error: {
27
+ code: 'VALIDATION_ERROR',
28
+ message: 'Name is required',
29
+ },
30
+ },
31
+ { status: 400 }
32
+ )
33
+ }
34
+
35
+ const id = Date.now().toString(36) + Math.random().toString(36).substring(2)
36
+ const item = {
37
+ id,
38
+ name: body.name,
39
+ createdAt: new Date().toISOString(),
40
+ }
41
+
42
+ items.set(id, item)
43
+
44
+ return NextResponse.json(
45
+ {
46
+ success: true,
47
+ data: item,
48
+ },
49
+ { status: 201 }
50
+ )
51
+ } catch {
52
+ return NextResponse.json(
53
+ {
54
+ success: false,
55
+ error: {
56
+ code: 'PARSE_ERROR',
57
+ message: 'Invalid JSON body',
58
+ },
59
+ },
60
+ { status: 400 }
61
+ )
62
+ }
63
+ }
@@ -0,0 +1,10 @@
1
+ import { NextResponse } from 'next/server'
2
+
3
+ export async function GET() {
4
+ return NextResponse.json({
5
+ status: 'healthy',
6
+ service: '{{name}}',
7
+ timestamp: new Date().toISOString(),
8
+ environment: process.env.NODE_ENV,
9
+ })
10
+ }
@@ -0,0 +1,92 @@
1
+ import { redirect } from 'next/navigation'
2
+ import { getCurrentUser } from '@/lib/auth'
3
+
4
+ export default async function DashboardPage() {
5
+ const user = await getCurrentUser()
6
+
7
+ if (!user) {
8
+ redirect('/api/auth/login?returnTo=/dashboard')
9
+ }
10
+
11
+ return (
12
+ <main style={{
13
+ maxWidth: '48rem',
14
+ margin: '0 auto',
15
+ padding: '2rem',
16
+ fontFamily: 'system-ui, -apple-system, sans-serif',
17
+ }}>
18
+ <div style={{
19
+ display: 'flex',
20
+ justifyContent: 'space-between',
21
+ alignItems: 'center',
22
+ marginBottom: '2rem',
23
+ }}>
24
+ <h1 style={{ fontSize: '2rem', margin: 0 }}>Dashboard</h1>
25
+ <form action="/api/auth/logout" method="post">
26
+ <button
27
+ type="submit"
28
+ style={{
29
+ padding: '0.5rem 1rem',
30
+ backgroundColor: '#dc2626',
31
+ color: 'white',
32
+ borderRadius: '0.375rem',
33
+ border: 'none',
34
+ cursor: 'pointer',
35
+ fontSize: '0.875rem',
36
+ }}
37
+ >
38
+ Logout
39
+ </button>
40
+ </form>
41
+ </div>
42
+
43
+ <div style={{
44
+ backgroundColor: '#f9fafb',
45
+ border: '1px solid #e5e7eb',
46
+ borderRadius: '0.5rem',
47
+ padding: '1.5rem',
48
+ marginBottom: '1.5rem',
49
+ }}>
50
+ <h2 style={{ fontSize: '1.25rem', marginTop: 0, marginBottom: '1rem' }}>
51
+ Welcome, {user.name}
52
+ </h2>
53
+ <dl style={{ margin: 0 }}>
54
+ <dt style={{ fontWeight: 600, color: '#6b7280', fontSize: '0.875rem' }}>Email</dt>
55
+ <dd style={{ margin: '0 0 0.75rem 0' }}>{user.email}</dd>
56
+
57
+ <dt style={{ fontWeight: 600, color: '#6b7280', fontSize: '0.875rem' }}>User ID</dt>
58
+ <dd style={{ margin: '0 0 0.75rem 0', fontFamily: 'monospace', fontSize: '0.875rem' }}>{user.id}</dd>
59
+
60
+ <dt style={{ fontWeight: 600, color: '#6b7280', fontSize: '0.875rem' }}>Roles</dt>
61
+ <dd style={{ margin: 0 }}>
62
+ {user.roles.length > 0 ? (
63
+ <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
64
+ {user.roles.map((role) => (
65
+ <span
66
+ key={role}
67
+ style={{
68
+ backgroundColor: '#dbeafe',
69
+ color: '#1e40af',
70
+ padding: '0.125rem 0.5rem',
71
+ borderRadius: '9999px',
72
+ fontSize: '0.75rem',
73
+ }}
74
+ >
75
+ {role}
76
+ </span>
77
+ ))}
78
+ </div>
79
+ ) : (
80
+ <span style={{ color: '#9ca3af' }}>No roles assigned</span>
81
+ )}
82
+ </dd>
83
+ </dl>
84
+ </div>
85
+
86
+ <p style={{ color: '#6b7280', fontSize: '0.875rem' }}>
87
+ This page is protected by middleware and server-side auth checks.
88
+ Edit <code>src/app/dashboard/page.tsx</code> to customize.
89
+ </p>
90
+ </main>
91
+ )
92
+ }
@@ -0,0 +1,18 @@
1
+ import type { Metadata } from 'next'
2
+
3
+ export const metadata: Metadata = {
4
+ title: '{{name}}',
5
+ description: '{{description}}',
6
+ }
7
+
8
+ export default function RootLayout({
9
+ children,
10
+ }: {
11
+ children: React.ReactNode
12
+ }) {
13
+ return (
14
+ <html lang="en">
15
+ <body>{children}</body>
16
+ </html>
17
+ )
18
+ }
@@ -0,0 +1,110 @@
1
+ import { getCurrentUser } from '@/lib/auth'
2
+
3
+ export default async function Home() {
4
+ const user = await getCurrentUser()
5
+
6
+ return (
7
+ <main style={{
8
+ display: 'flex',
9
+ flexDirection: 'column',
10
+ alignItems: 'center',
11
+ justifyContent: 'center',
12
+ minHeight: '100vh',
13
+ padding: '2rem',
14
+ fontFamily: 'system-ui, -apple-system, sans-serif',
15
+ }}>
16
+ <h1 style={{ fontSize: '3rem', marginBottom: '1rem' }}>
17
+ {'{{name}}'}
18
+ </h1>
19
+ <p style={{ fontSize: '1.25rem', color: '#666', marginBottom: '2rem' }}>
20
+ {'{{description}}'}
21
+ </p>
22
+
23
+ {user ? (
24
+ <div style={{ textAlign: 'center', marginBottom: '2rem' }}>
25
+ <p style={{ marginBottom: '1rem' }}>
26
+ Signed in as <strong>{user.name}</strong> ({user.email})
27
+ </p>
28
+ <div style={{ display: 'flex', gap: '1rem' }}>
29
+ <a
30
+ href="/dashboard"
31
+ style={{
32
+ padding: '0.75rem 1.5rem',
33
+ backgroundColor: '#0070f3',
34
+ color: 'white',
35
+ borderRadius: '0.5rem',
36
+ textDecoration: 'none',
37
+ }}
38
+ >
39
+ Dashboard
40
+ </a>
41
+ <form action="/api/auth/logout" method="post" style={{ display: 'inline' }}>
42
+ <button
43
+ type="submit"
44
+ style={{
45
+ padding: '0.75rem 1.5rem',
46
+ backgroundColor: '#dc2626',
47
+ color: 'white',
48
+ borderRadius: '0.5rem',
49
+ border: 'none',
50
+ cursor: 'pointer',
51
+ fontSize: 'inherit',
52
+ }}
53
+ >
54
+ Logout
55
+ </button>
56
+ </form>
57
+ </div>
58
+ </div>
59
+ ) : (
60
+ <div style={{ display: 'flex', gap: '1rem', marginBottom: '2rem' }}>
61
+ <a
62
+ href="/api/auth/login"
63
+ style={{
64
+ padding: '0.75rem 1.5rem',
65
+ backgroundColor: '#0070f3',
66
+ color: 'white',
67
+ borderRadius: '0.5rem',
68
+ textDecoration: 'none',
69
+ }}
70
+ >
71
+ Sign In
72
+ </a>
73
+ </div>
74
+ )}
75
+
76
+ <div style={{ display: 'flex', gap: '1rem' }}>
77
+ <a
78
+ href="/api/health"
79
+ style={{
80
+ padding: '0.5rem 1rem',
81
+ border: '1px solid #e5e7eb',
82
+ borderRadius: '0.5rem',
83
+ textDecoration: 'none',
84
+ color: '#666',
85
+ fontSize: '0.875rem',
86
+ }}
87
+ >
88
+ Health Check
89
+ </a>
90
+ <a
91
+ href="/api/example"
92
+ style={{
93
+ padding: '0.5rem 1rem',
94
+ border: '1px solid #e5e7eb',
95
+ borderRadius: '0.5rem',
96
+ textDecoration: 'none',
97
+ color: '#666',
98
+ fontSize: '0.875rem',
99
+ }}
100
+ >
101
+ Example API
102
+ </a>
103
+ </div>
104
+
105
+ <footer style={{ marginTop: '4rem', color: '#999' }}>
106
+ Deployed on Tawa Platform
107
+ </footer>
108
+ </main>
109
+ )
110
+ }