nobalmako 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +112 -0
  2. package/components.json +22 -0
  3. package/dist/nobalmako.js +272 -0
  4. package/drizzle/0000_pink_spiral.sql +126 -0
  5. package/drizzle/meta/0000_snapshot.json +1027 -0
  6. package/drizzle/meta/_journal.json +13 -0
  7. package/drizzle.config.ts +10 -0
  8. package/eslint.config.mjs +18 -0
  9. package/next.config.ts +7 -0
  10. package/package.json +80 -0
  11. package/postcss.config.mjs +7 -0
  12. package/public/file.svg +1 -0
  13. package/public/globe.svg +1 -0
  14. package/public/next.svg +1 -0
  15. package/public/vercel.svg +1 -0
  16. package/public/window.svg +1 -0
  17. package/server/index.ts +118 -0
  18. package/src/app/api/api-keys/[id]/route.ts +147 -0
  19. package/src/app/api/api-keys/route.ts +151 -0
  20. package/src/app/api/audit-logs/route.ts +84 -0
  21. package/src/app/api/auth/forgot-password/route.ts +47 -0
  22. package/src/app/api/auth/login/route.ts +99 -0
  23. package/src/app/api/auth/logout/route.ts +15 -0
  24. package/src/app/api/auth/me/route.ts +23 -0
  25. package/src/app/api/auth/mfa/setup/route.ts +33 -0
  26. package/src/app/api/auth/mfa/verify/route.ts +45 -0
  27. package/src/app/api/auth/register/route.ts +140 -0
  28. package/src/app/api/auth/reset-password/route.ts +52 -0
  29. package/src/app/api/auth/update/route.ts +71 -0
  30. package/src/app/api/auth/verify/route.ts +39 -0
  31. package/src/app/api/environments/route.ts +227 -0
  32. package/src/app/api/team-members/route.ts +385 -0
  33. package/src/app/api/teams/route.ts +217 -0
  34. package/src/app/api/variable-history/route.ts +218 -0
  35. package/src/app/api/variables/route.ts +476 -0
  36. package/src/app/api/webhooks/route.ts +77 -0
  37. package/src/app/api-keys/APIKeysClient.tsx +316 -0
  38. package/src/app/api-keys/page.tsx +10 -0
  39. package/src/app/api-reference/page.tsx +324 -0
  40. package/src/app/audit-log/AuditLogClient.tsx +229 -0
  41. package/src/app/audit-log/page.tsx +10 -0
  42. package/src/app/auth/forgot-password/page.tsx +121 -0
  43. package/src/app/auth/login/LoginForm.tsx +145 -0
  44. package/src/app/auth/login/page.tsx +11 -0
  45. package/src/app/auth/register/RegisterForm.tsx +156 -0
  46. package/src/app/auth/register/page.tsx +16 -0
  47. package/src/app/auth/reset-password/page.tsx +160 -0
  48. package/src/app/dashboard/DashboardClient.tsx +219 -0
  49. package/src/app/dashboard/page.tsx +11 -0
  50. package/src/app/docs/page.tsx +251 -0
  51. package/src/app/favicon.ico +0 -0
  52. package/src/app/globals.css +123 -0
  53. package/src/app/layout.tsx +35 -0
  54. package/src/app/page.tsx +231 -0
  55. package/src/app/profile/ProfileClient.tsx +230 -0
  56. package/src/app/profile/page.tsx +10 -0
  57. package/src/app/project/[id]/ProjectDetailsClient.tsx +512 -0
  58. package/src/app/project/[id]/page.tsx +17 -0
  59. package/src/bin/nobalmako.ts +341 -0
  60. package/src/components/ApiKeysManager.tsx +529 -0
  61. package/src/components/AppLayout.tsx +193 -0
  62. package/src/components/BulkActions.tsx +138 -0
  63. package/src/components/CreateEnvironmentDialog.tsx +207 -0
  64. package/src/components/CreateTeamDialog.tsx +174 -0
  65. package/src/components/CreateVariableDialog.tsx +311 -0
  66. package/src/components/DeleteEnvironmentDialog.tsx +104 -0
  67. package/src/components/DeleteTeamDialog.tsx +112 -0
  68. package/src/components/DeleteVariableDialog.tsx +103 -0
  69. package/src/components/EditEnvironmentDialog.tsx +202 -0
  70. package/src/components/EditMemberDialog.tsx +143 -0
  71. package/src/components/EditTeamDialog.tsx +178 -0
  72. package/src/components/EditVariableDialog.tsx +231 -0
  73. package/src/components/ImportVariablesDialog.tsx +347 -0
  74. package/src/components/InviteMemberDialog.tsx +191 -0
  75. package/src/components/LeaveProjectDialog.tsx +111 -0
  76. package/src/components/MFASettings.tsx +136 -0
  77. package/src/components/ProjectDiff.tsx +123 -0
  78. package/src/components/Providers.tsx +24 -0
  79. package/src/components/RemoveMemberDialog.tsx +112 -0
  80. package/src/components/SearchDialog.tsx +276 -0
  81. package/src/components/SecurityOverview.tsx +92 -0
  82. package/src/components/TeamMembersManager.tsx +103 -0
  83. package/src/components/VariableHistoryDialog.tsx +265 -0
  84. package/src/components/WebhooksManager.tsx +169 -0
  85. package/src/components/ui/alert-dialog.tsx +160 -0
  86. package/src/components/ui/alert.tsx +59 -0
  87. package/src/components/ui/avatar.tsx +53 -0
  88. package/src/components/ui/badge.tsx +46 -0
  89. package/src/components/ui/button.tsx +62 -0
  90. package/src/components/ui/card.tsx +92 -0
  91. package/src/components/ui/checkbox.tsx +32 -0
  92. package/src/components/ui/dialog.tsx +143 -0
  93. package/src/components/ui/dropdown-menu.tsx +257 -0
  94. package/src/components/ui/input.tsx +21 -0
  95. package/src/components/ui/label.tsx +24 -0
  96. package/src/components/ui/select.tsx +190 -0
  97. package/src/components/ui/separator.tsx +28 -0
  98. package/src/components/ui/sonner.tsx +37 -0
  99. package/src/components/ui/switch.tsx +31 -0
  100. package/src/components/ui/table.tsx +117 -0
  101. package/src/components/ui/tabs.tsx +66 -0
  102. package/src/components/ui/textarea.tsx +18 -0
  103. package/src/hooks/use-api-keys.ts +95 -0
  104. package/src/hooks/use-audit-logs.ts +58 -0
  105. package/src/hooks/use-auth.tsx +121 -0
  106. package/src/hooks/use-environments.ts +33 -0
  107. package/src/hooks/use-project-permissions.ts +49 -0
  108. package/src/hooks/use-team-members.ts +30 -0
  109. package/src/hooks/use-teams.ts +33 -0
  110. package/src/hooks/use-variables.ts +38 -0
  111. package/src/lib/audit.ts +36 -0
  112. package/src/lib/auth.ts +108 -0
  113. package/src/lib/crypto.ts +39 -0
  114. package/src/lib/db.ts +15 -0
  115. package/src/lib/dynamic-providers.ts +19 -0
  116. package/src/lib/email.ts +110 -0
  117. package/src/lib/mail.ts +51 -0
  118. package/src/lib/permissions.ts +51 -0
  119. package/src/lib/schema.ts +240 -0
  120. package/src/lib/seed.ts +107 -0
  121. package/src/lib/utils.ts +6 -0
  122. package/src/lib/webhooks.ts +42 -0
  123. package/tsconfig.json +34 -0
@@ -0,0 +1,316 @@
1
+ 'use client';
2
+
3
+ import { useState } from "react";
4
+ import Link from "next/link";
5
+ import AppLayout from "@/components/AppLayout";
6
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Badge } from "@/components/ui/badge";
9
+ import { Input } from "@/components/ui/input";
10
+ import { Label } from "@/components/ui/label";
11
+ import {
12
+ Dialog,
13
+ DialogContent,
14
+ DialogDescription,
15
+ DialogFooter,
16
+ DialogHeader,
17
+ DialogTitle,
18
+ DialogTrigger,
19
+ } from "@/components/ui/dialog";
20
+ import {
21
+ Select,
22
+ SelectContent,
23
+ SelectItem,
24
+ SelectTrigger,
25
+ SelectValue,
26
+ } from "@/components/ui/select";
27
+ import { Plus, Key, Copy, Trash2, Calendar, Shield, Clock, Check, AlertCircle, ArrowRight } from "lucide-react";
28
+ import { useApiKeys } from "@/hooks/use-api-keys";
29
+ import { useTeams } from "@/hooks/use-teams";
30
+ import { format } from "date-fns";
31
+ import { toast } from "sonner";
32
+
33
+ export default function APIKeysClient() {
34
+ const { apiKeys, isLoading, createApiKey, deleteApiKey } = useApiKeys();
35
+ const { teams } = useTeams();
36
+ const [isCreateOpen, setIsCreateOpen] = useState(false);
37
+ const [newKeyData, setNewKeyData] = useState({
38
+ name: "",
39
+ teamId: "personal",
40
+ expiresAt: "",
41
+ });
42
+ const [createdKey, setCreatedKey] = useState<string | null>(null);
43
+ const [isCreating, setIsCreating] = useState(false);
44
+
45
+ const handleCreate = async () => {
46
+ if (!newKeyData.name) {
47
+ toast.error("Please enter a name for the API key");
48
+ return;
49
+ }
50
+
51
+ setIsCreating(true);
52
+ try {
53
+ const result = await createApiKey({
54
+ name: newKeyData.name,
55
+ teamId: newKeyData.teamId === "personal" ? undefined : newKeyData.teamId,
56
+ expiresAt: newKeyData.expiresAt || undefined,
57
+ });
58
+ setCreatedKey(result.apiKey);
59
+ } catch (error) {
60
+ toast.error(error instanceof Error ? error.message : "Failed to create API key");
61
+ } finally {
62
+ setIsCreating(false);
63
+ }
64
+ };
65
+
66
+ const copyToClipboard = (text: string) => {
67
+ navigator.clipboard.writeText(text);
68
+ toast.success("Copied to clipboard");
69
+ };
70
+
71
+ const handleDelete = async (id: string) => {
72
+ if (confirm("Are you sure you want to delete this API key?")) {
73
+ try {
74
+ await deleteApiKey(id);
75
+ toast.success("API key deleted");
76
+ } catch (error) {
77
+ toast.error("Failed to delete API key");
78
+ }
79
+ }
80
+ };
81
+
82
+ return (
83
+ <AppLayout>
84
+ <div className="space-y-8 max-w-5xl mx-auto">
85
+ <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
86
+ <div>
87
+ <h1 className="text-3xl font-bold tracking-tight">API Keys</h1>
88
+ <p className="text-muted-foreground mt-1 text-lg">
89
+ Manage authentication keys for CI/CD pipelines, CLI, and external integrations.
90
+ </p>
91
+ </div>
92
+ <Dialog open={isCreateOpen} onOpenChange={(open) => {
93
+ setIsCreateOpen(open);
94
+ if (!open) {
95
+ setCreatedKey(null);
96
+ setNewKeyData({ name: "", teamId: "personal", expiresAt: "" });
97
+ }
98
+ }}>
99
+ <DialogTrigger asChild>
100
+ <Button className="bg-primary hover:bg-primary/90">
101
+ <Plus className="h-4 w-4 mr-2" />
102
+ Generate New Key
103
+ </Button>
104
+ </DialogTrigger>
105
+ <DialogContent className="sm:max-w-[500px]">
106
+ <DialogHeader>
107
+ <DialogTitle>{createdKey ? "Key Generated Successfully" : "Generate API Key"}</DialogTitle>
108
+ <DialogDescription>
109
+ {createdKey
110
+ ? "Copy this key now. For your security, it won't be shown again."
111
+ : "Create a new key to access the Nobalmako API programmatically."}
112
+ </DialogDescription>
113
+ </DialogHeader>
114
+
115
+ {createdKey ? (
116
+ <div className="space-y-4 pt-4">
117
+ <div className="flex items-center gap-2 p-4 bg-muted rounded-lg border border-primary/20">
118
+ <code className="flex-1 font-mono text-sm break-all">{createdKey}</code>
119
+ <Button variant="ghost" size="icon" onClick={() => copyToClipboard(createdKey)}>
120
+ <Copy className="h-4 w-4" />
121
+ </Button>
122
+ </div>
123
+ <div className="flex items-center gap-2 text-amber-600 bg-amber-50 p-3 rounded-md text-sm">
124
+ <AlertCircle className="h-4 w-4 shrink-0" />
125
+ <p>Make sure to copy your API key now. You won't be able to see it again.</p>
126
+ </div>
127
+ <DialogFooter>
128
+ <Button onClick={() => setIsCreateOpen(false)} className="w-full">Done</Button>
129
+ </DialogFooter>
130
+ </div>
131
+ ) : (
132
+ <div className="space-y-4 pt-4">
133
+ <div className="space-y-2">
134
+ <Label htmlFor="name">Key Name</Label>
135
+ <Input
136
+ id="name"
137
+ placeholder="e.g. GitHub Actions - Production"
138
+ value={newKeyData.name}
139
+ onChange={(e) => setNewKeyData({ ...newKeyData, name: e.target.value })}
140
+ />
141
+ </div>
142
+ <div className="space-y-2">
143
+ <Label htmlFor="scope">Access Scope</Label>
144
+ <Select
145
+ value={newKeyData.teamId}
146
+ onValueChange={(val) => setNewKeyData({ ...newKeyData, teamId: val })}
147
+ >
148
+ <SelectTrigger>
149
+ <SelectValue placeholder="Select scope" />
150
+ </SelectTrigger>
151
+ <SelectContent>
152
+ <SelectItem value="personal">Personal Account</SelectItem>
153
+ {teams?.map(team => (
154
+ <SelectItem key={team.id} value={team.id}>{team.name} (Project)</SelectItem>
155
+ ))}
156
+ </SelectContent>
157
+ </Select>
158
+ </div>
159
+ <div className="space-y-2">
160
+ <Label htmlFor="expires">Expires (optional)</Label>
161
+ <Input
162
+ id="expires"
163
+ type="date"
164
+ value={newKeyData.expiresAt}
165
+ onChange={(e) => setNewKeyData({ ...newKeyData, expiresAt: e.target.value })}
166
+ />
167
+ </div>
168
+ <DialogFooter className="pt-4">
169
+ <Button variant="outline" onClick={() => setIsCreateOpen(false)}>Cancel</Button>
170
+ <Button onClick={handleCreate} disabled={isCreating}>
171
+ {isCreating ? "Generating..." : "Generate Key"}
172
+ </Button>
173
+ </DialogFooter>
174
+ </div>
175
+ )}
176
+ </DialogContent>
177
+ </Dialog>
178
+ </div>
179
+
180
+ <Card className="shadow-sm border-muted">
181
+ <CardHeader>
182
+ <CardTitle className="text-xl flex items-center gap-2">
183
+ <Shield className="h-5 w-5 text-primary" />
184
+ Active Keys
185
+ </CardTitle>
186
+ <CardDescription>
187
+ A list of API keys that currently have access to your account.
188
+ </CardDescription>
189
+ </CardHeader>
190
+ <CardContent className="p-0">
191
+ <div className="overflow-x-auto">
192
+ {isLoading ? (
193
+ <div className="py-20 flex justify-center">
194
+ <div className="animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full" />
195
+ </div>
196
+ ) : apiKeys.length > 0 ? (
197
+ <table className="w-full text-sm text-left">
198
+ <thead className="text-xs text-muted-foreground uppercase bg-muted/30">
199
+ <tr>
200
+ <th className="px-6 py-4 font-medium">Key Name</th>
201
+ <th className="px-6 py-4 font-medium">Scope</th>
202
+ <th className="px-6 py-4 font-medium">Last Used</th>
203
+ <th className="px-6 py-4 font-medium">Created</th>
204
+ <th className="px-6 py-4 font-medium text-right">Actions</th>
205
+ </tr>
206
+ </thead>
207
+ <tbody className="divide-y divide-border">
208
+ {apiKeys.map((key) => (
209
+ <tr key={key.id} className="hover:bg-muted/30 transition-colors">
210
+ <td className="px-6 py-4 font-medium">
211
+ <div className="flex flex-col">
212
+ <span className="text-sm font-semibold">{key.name}</span>
213
+ <div className="flex items-center gap-2 mt-1">
214
+ <code className="text-[10px] bg-muted px-1.5 py-0.5 rounded">eg_••••••{key.id.slice(-4)}</code>
215
+ {key.expiresAt && new Date(key.expiresAt) < new Date() ? (
216
+ <Badge variant="destructive" className="text-[10px] h-4">Expired</Badge>
217
+ ) : (
218
+ <Badge variant="secondary" className="text-[10px] h-4">Active</Badge>
219
+ )}
220
+ </div>
221
+ </div>
222
+ </td>
223
+ <td className="px-6 py-4">
224
+ <div className="flex items-center gap-1.5">
225
+ {key.teamName ? (
226
+ <Badge variant="outline" className="font-normal text-blue-600 bg-blue-50 border-blue-200">
227
+ {key.teamName}
228
+ </Badge>
229
+ ) : (
230
+ <Badge variant="outline" className="font-normal">Personal</Badge>
231
+ )}
232
+ </div>
233
+ </td>
234
+ <td className="px-6 py-4 text-muted-foreground">
235
+ <div className="flex items-center gap-1.5">
236
+ <Clock className="h-3.5 w-3.5" />
237
+ {key.lastUsed ? format(new Date(key.lastUsed), "MMM d, yyyy") : "Never"}
238
+ </div>
239
+ </td>
240
+ <td className="px-6 py-4 text-muted-foreground">
241
+ {format(new Date(key.createdAt), "MMM d, yyyy")}
242
+ </td>
243
+ <td className="px-6 py-4 text-right">
244
+ <Button
245
+ variant="ghost"
246
+ size="icon"
247
+ className="text-destructive hover:text-destructive hover:bg-destructive/10"
248
+ onClick={() => handleDelete(key.id)}
249
+ >
250
+ <Trash2 className="h-4 w-4" />
251
+ </Button>
252
+ </td>
253
+ </tr>
254
+ ))}
255
+ </tbody>
256
+ </table>
257
+ ) : (
258
+ <div className="flex flex-col items-center justify-center py-20 text-center px-4">
259
+ <div className="p-4 bg-muted rounded-full mb-4">
260
+ <Key className="h-8 w-8 text-muted-foreground/50" />
261
+ </div>
262
+ <h3 className="text-lg font-semibold mb-1">No API keys yet</h3>
263
+ <p className="text-muted-foreground max-w-xs mb-6 text-sm">
264
+ Generate an API key to start using Nobalmako via CLI or CI/CD.
265
+ </p>
266
+ <Button variant="outline" onClick={() => setIsCreateOpen(true)}>
267
+ Generate My First Key
268
+ </Button>
269
+ </div>
270
+ )}
271
+ </div>
272
+ </CardContent>
273
+ </Card>
274
+
275
+ {/* Integration Tips */}
276
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
277
+ <Card className="bg-muted/20 border-border/40">
278
+ <CardHeader>
279
+ <CardTitle className="text-sm">Quick Setup</CardTitle>
280
+ </CardHeader>
281
+ <CardContent>
282
+ <div className="space-y-4">
283
+ <p className="text-xs text-muted-foreground">Authenticate your CI/CD pipeline using the API key:</p>
284
+ <div className="bg-zinc-950 p-4 rounded-lg font-mono text-[11px] text-zinc-300">
285
+ <span className="text-zinc-500"># Set as environment variable</span><br/>
286
+ export NOBALMAKO_TOKEN="your_api_key_here"<br/><br/>
287
+ <span className="text-zinc-500"># Download variables</span><br/>
288
+ nobalmako pull --project my-app --env prod
289
+ </div>
290
+ </div>
291
+ </CardContent>
292
+ </Card>
293
+
294
+ <Card className="bg-muted/20 border-border/40">
295
+ <CardHeader className="flex flex-row items-center justify-between space-y-0">
296
+ <CardTitle className="text-sm">API Reference</CardTitle>
297
+ <Link href="/api-reference" className="text-[10px] text-primary hover:underline flex items-center gap-1 font-medium">
298
+ Full Docs <ArrowRight className="h-2 w-2" />
299
+ </Link>
300
+ </CardHeader>
301
+ <CardContent>
302
+ <div className="space-y-4">
303
+ <p className="text-xs text-muted-foreground">Fetch variables directly via HTTP:</p>
304
+ <div className="bg-zinc-950 p-4 rounded-lg font-mono text-[11px] text-zinc-300 overflow-x-auto whitespace-pre">
305
+ <span className="text-emerald-400">GET</span> /api/variables?teamId=...<br/>
306
+ <span className="text-zinc-500">Headers:</span><br/>
307
+ Authorization: Bearer <span className="text-sky-400">your_api_key</span>
308
+ </div>
309
+ </div>
310
+ </CardContent>
311
+ </Card>
312
+ </div>
313
+ </div>
314
+ </AppLayout>
315
+ );
316
+ }
@@ -0,0 +1,10 @@
1
+ import APIKeysClient from "./APIKeysClient";
2
+
3
+ export const metadata = {
4
+ title: "API Keys - Nobalmako",
5
+ description: "Manage your API keys for external access",
6
+ };
7
+
8
+ export default function APIKeysPage() {
9
+ return <APIKeysClient />;
10
+ }
@@ -0,0 +1,324 @@
1
+ 'use client';
2
+
3
+ import AppLayout from "@/components/AppLayout";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Terminal, Copy, Globe, Lock, Code, Check } from "lucide-react";
6
+ import { useState } from "react";
7
+ import { Button } from "@/components/ui/button";
8
+
9
+ export default function ApiReferencePage() {
10
+ const [copied, setCopied] = useState<string | null>(null);
11
+
12
+ const copyToClipboard = (text: string, id: string) => {
13
+ navigator.clipboard.writeText(text);
14
+ setCopied(id);
15
+ setTimeout(() => setCopied(null), 2000);
16
+ };
17
+
18
+ const endpoints = [
19
+ {
20
+ method: "GET",
21
+ path: "/api/variables",
22
+ description: "List all environment variables for a team and environment.",
23
+ params: [
24
+ { name: "teamId", type: "UUID", required: true, description: "The ID of the project" },
25
+ { name: "environmentId", type: "UUID", required: true, description: "The ID of the environment" },
26
+ ],
27
+ response: `{
28
+ "variables": [
29
+ {
30
+ "id": "uuid",
31
+ "key": "DATABASE_URL",
32
+ "value": "postgres://...",
33
+ "isSecret": true
34
+ }
35
+ ]
36
+ }`
37
+ },
38
+ {
39
+ method: "POST",
40
+ path: "/api/variables",
41
+ description: "Create a new environment variable.",
42
+ body: `{
43
+ "teamId": "uuid",
44
+ "environmentId": "uuid",
45
+ "key": "API_KEY",
46
+ "value": "secret_value",
47
+ "isSecret": true
48
+ }`,
49
+ response: `{
50
+ "success": true,
51
+ "variable": { "id": "uuid", "key": "API_KEY" }
52
+ }`
53
+ },
54
+ {
55
+ method: "GET",
56
+ path: "/api/environments",
57
+ description: "Get all environments for a specific project.",
58
+ params: [
59
+ { name: "teamId", type: "UUID", required: true, description: "The project ID" },
60
+ ],
61
+ response: `{
62
+ "environments": [
63
+ { "id": "uuid", "name": "Production", "color": "#ef4444" },
64
+ { "id": "uuid", "name": "Staging", "color": "#f59e0b" }
65
+ ]
66
+ }`
67
+ },
68
+ {
69
+ method: "GET",
70
+ path: "/api/audit-logs",
71
+ description: "Retrieve security audit logs for a project.",
72
+ params: [
73
+ { name: "teamId", type: "UUID", required: true, description: "The project ID" },
74
+ { name: "limit", type: "number", required: false, description: "Number of logs to return (default: 50)" },
75
+ ],
76
+ response: `{
77
+ "logs": [
78
+ {
79
+ "id": "uuid",
80
+ "action": "update",
81
+ "resourceType": "variable",
82
+ "userName": "John Doe",
83
+ "createdAt": "2026-01-31T12:00:00Z"
84
+ }
85
+ ]
86
+ }`
87
+ }
88
+ ];
89
+
90
+ return (
91
+ <AppLayout>
92
+ <div className="max-w-6xl mx-auto py-10 px-6 sm:px-8">
93
+ <header className="mb-14 border-b pb-8">
94
+ <Badge className="mb-4 bg-primary/10 text-primary border-primary/20 hover:bg-primary/20" variant="outline">REST API v1.0</Badge>
95
+ <h1 className="text-5xl font-extrabold tracking-tight mb-4 text-foreground ">API Reference</h1>
96
+ <p className="text-xl text-muted-foreground max-w-2xl">
97
+ Integrate Nobalmako into your own tools and CI/CD pipelines with our secure, developer-friendly REST API.
98
+ </p>
99
+ </header>
100
+
101
+ <div className="flex flex-col lg:flex-row gap-16">
102
+ {/* Sidebar Navigation */}
103
+ <aside className="lg:w-64 shrink-0">
104
+ <nav className="sticky top-28 space-y-8">
105
+ <div>
106
+ <p className="text-[10px] font-bold text-muted-foreground uppercase tracking-[0.2em] mb-4 px-2">Introduction</p>
107
+ <div className="space-y-1">
108
+ <a href="#authentication" className="group flex items-center px-3 py-2 text-sm font-medium rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-all">
109
+ <Lock className="mr-3 h-4 w-4 opacity-50 group-hover:opacity-100" />
110
+ Authentication
111
+ </a>
112
+ <a href="#base-url" className="group flex items-center px-3 py-2 text-sm font-medium rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-all">
113
+ <Globe className="mr-3 h-4 w-4 opacity-50 group-hover:opacity-100" />
114
+ Base URL
115
+ </a>
116
+ </div>
117
+ </div>
118
+
119
+ <div>
120
+ <p className="text-[10px] font-bold text-muted-foreground uppercase tracking-[0.2em] mb-4 px-2">Endpoints</p>
121
+ <div className="space-y-1">
122
+ <a href="#variables" className="group flex items-center px-3 py-2 text-sm font-medium rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-all">
123
+ <Code className="mr-3 h-4 w-4 opacity-50 group-hover:opacity-100" />
124
+ Variables
125
+ </a>
126
+ <a href="#cli-installation" className="group flex items-center px-3 py-2 text-sm font-medium rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-all">
127
+ <Terminal className="mr-3 h-4 w-4 opacity-50 group-hover:opacity-100" />
128
+ CLI Installation
129
+ </a>
130
+ </div>
131
+ </div>
132
+ </nav>
133
+ </aside>
134
+
135
+ {/* Content */}
136
+ <div className="flex-1 max-w-4xl space-y-24">
137
+ <section id="authentication" className="scroll-mt-28">
138
+ <div className="flex items-center gap-3 mb-6">
139
+ <div className="p-2 bg-primary/10 rounded-lg">
140
+ <Lock className="h-6 w-6 text-primary" />
141
+ </div>
142
+ <h2 className="text-3xl font-bold text-foreground ">Authentication</h2>
143
+ </div>
144
+ <div className="prose max-w-none prose-p:text-muted-foreground">
145
+ <p className="text-lg leading-relaxed">
146
+ All API requests must include an <code>Authorization</code> header with your personal API key.
147
+ Generate and manage your keys in the <a href="/api-keys" className="text-primary font-semibold hover:underline decoration-primary/30 underline-offset-4">API Keys dashboard</a>.
148
+ </p>
149
+
150
+ <div className="mt-8 bg-slate-50 rounded-xl overflow-hidden shadow-sm border border-border">
151
+ <div className="px-4 py-2 border-b border-border bg-slate-100/50 flex items-center">
152
+ <span className="text-[10px] font-bold text-slate-500 uppercase tracking-widest">HTTP Header</span>
153
+ </div>
154
+ <div className="p-6 font-mono text-sm relative group">
155
+ <span className="text-slate-500 font-bold">Authorization:</span><span className="text-slate-900"> Bearer </span><span className="text-emerald-600 font-semibold">YOUR_API_KEY</span>
156
+ <Button
157
+ variant="ghost"
158
+ size="icon"
159
+ className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity text-slate-400 hover:text-slate-900 hover:bg-slate-200 h-8 w-8"
160
+ onClick={() => copyToClipboard('Authorization: Bearer YOUR_API_KEY', 'auth')}
161
+ >
162
+ {copied === 'auth' ? <Check className="h-4 w-4 text-emerald-600" /> : <Copy className="h-4 w-4" />}
163
+ </Button>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </section>
168
+
169
+ <section id="base-url" className="scroll-mt-28">
170
+ <div className="flex items-center gap-3 mb-6">
171
+ <div className="p-2 bg-primary/10 rounded-lg">
172
+ <Globe className="h-6 w-6 text-primary" />
173
+ </div>
174
+ <h2 className="text-3xl font-bold text-foreground ">Base URL</h2>
175
+ </div>
176
+ <div className="prose max-w-none prose-p:text-muted-foreground">
177
+ <p className="text-lg leading-relaxed">
178
+ All API requests should be made to our globally distributed edge network using the following base URL:
179
+ </p>
180
+ <div className="mt-8 bg-slate-50 rounded-xl overflow-hidden shadow-sm border border-border">
181
+ <div className="px-4 py-2 border-b border-border bg-slate-100/50 flex items-center">
182
+ <span className="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Live Endpoint</span>
183
+ </div>
184
+ <div className="p-6 font-mono text-sm relative group">
185
+ <span className="text-emerald-700 font-medium selection:bg-emerald-100">https://nobalmako.com/api</span>
186
+ <Button
187
+ variant="ghost"
188
+ size="icon"
189
+ className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity text-slate-400 hover:text-slate-900 hover:bg-slate-200 h-8 w-8"
190
+ onClick={() => copyToClipboard('https://nobalmako.com/api', 'base-url')}
191
+ >
192
+ {copied === 'base-url' ? <Check className="h-4 w-4 text-emerald-600" /> : <Copy className="h-4 w-4" />}
193
+ </Button>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ </section>
198
+
199
+ <div className="h-px bg-border/60" />
200
+
201
+ <section id="variables" className="scroll-mt-28 space-y-10">
202
+ <header>
203
+ <h2 className="text-3xl font-bold text-foreground mb-2">Variables</h2>
204
+ <p className="text-muted-foreground text-lg">Programmatically manage your project-level environment variables.</p>
205
+ </header>
206
+
207
+ <div className="space-y-16">
208
+ {endpoints.map((endpoint, i) => (
209
+ <div key={i} className="group/endpoint">
210
+ <div className="flex items-center gap-4 mb-6">
211
+ <Badge
212
+ variant="outline"
213
+ className={`font-mono px-3 py-1 text-xs font-bold uppercase tracking-wider ${
214
+ endpoint.method === 'GET' ? 'bg-blue-600/10 text-blue-600 border-blue-600/30' :
215
+ endpoint.method === 'POST' ? 'bg-emerald-600/10 text-emerald-600 border-emerald-600/30' : ''
216
+ }`}
217
+ >
218
+ {endpoint.method}
219
+ </Badge>
220
+ <code className="text-sm font-bold font-mono text-foreground tracking-tight py-1 px-3 bg-muted rounded-md">{endpoint.path}</code>
221
+ </div>
222
+
223
+ <div className="grid lg:grid-cols-2 gap-10">
224
+ <div className="space-y-8">
225
+ <div>
226
+ <p className="text-base text-muted-foreground leading-relaxed">{endpoint.description}</p>
227
+ </div>
228
+
229
+ {endpoint.params && (
230
+ <div className="space-y-4">
231
+ <p className="text-[10px] font-bold text-muted-foreground uppercase tracking-[0.2em] px-1">Parameters</p>
232
+ <div className="space-y-2">
233
+ {endpoint.params.map((p, j) => (
234
+ <div key={j} className="flex flex-col gap-1.5 p-4 rounded-xl bg-muted/40 border border-border/50 hover:bg-muted/60 transition-colors">
235
+ <div className="flex items-center justify-between">
236
+ <code className="text-primary font-bold text-sm tracking-tight">{p.name}</code>
237
+ <span className="text-[10px] font-mono font-semibold text-muted-foreground/70 uppercase">
238
+ {p.type}{p.required ? '*' : ''}
239
+ </span>
240
+ </div>
241
+ <p className="text-xs text-muted-foreground leading-relaxed">{p.description}</p>
242
+ </div>
243
+ ))}
244
+ </div>
245
+ </div>
246
+ )}
247
+
248
+ {endpoint.body && (
249
+ <div className="space-y-4">
250
+ <p className="text-[10px] font-bold text-muted-foreground uppercase tracking-[0.2em] px-1">Request Payload</p>
251
+ <div className="relative group/code">
252
+ <pre className="bg-slate-50 p-6 rounded-2xl text-xs text-slate-800 overflow-x-auto border border-border leading-relaxed font-mono shadow-sm">
253
+ {endpoint.body}
254
+ </pre>
255
+ <Button
256
+ variant="ghost"
257
+ size="icon"
258
+ className="absolute top-4 right-4 opacity-0 group-hover/code:opacity-100 transition-opacity text-slate-400 hover:text-slate-900 hover:bg-slate-200 h-8 w-8"
259
+ onClick={() => copyToClipboard(endpoint.body!, `body-${i}`)}
260
+ >
261
+ {copied === `body-${i}` ? <Check className="h-4 w-4 text-emerald-600" /> : <Copy className="h-4 w-4" />}
262
+ </Button>
263
+ </div>
264
+ </div>
265
+ )}
266
+ </div>
267
+
268
+ <div className="space-y-4">
269
+ <p className="text-[10px] font-bold text-muted-foreground uppercase tracking-[0.2em] px-1">Response Sample</p>
270
+ <div className="relative group/code">
271
+ <pre className="bg-slate-50 p-6 rounded-2xl text-xs text-slate-800 overflow-x-auto border border-border leading-relaxed font-mono shadow-sm">
272
+ {endpoint.response}
273
+ </pre>
274
+ <Button
275
+ variant="ghost"
276
+ size="icon"
277
+ className="absolute top-4 right-4 opacity-0 group-hover/code:opacity-100 transition-opacity text-slate-400 hover:text-slate-900 hover:bg-slate-200 h-8 w-8"
278
+ onClick={() => copyToClipboard(endpoint.response, `resp-${i}`)}
279
+ >
280
+ {copied === `resp-${i}` ? <Check className="h-4 w-4 text-emerald-600" /> : <Copy className="h-4 w-4" />}
281
+ </Button>
282
+ </div>
283
+ </div>
284
+ </div>
285
+ </div>
286
+ ))}
287
+ </div>
288
+ </section>
289
+
290
+ <div className="p-10 bg-slate-50 rounded-[2rem] text-slate-900 border border-border shadow-sm mt-16 relative overflow-hidden group">
291
+ <div className="absolute top-0 right-0 w-64 h-64 bg-primary/5 rounded-full blur-[100px] -mr-32 -mt-32" />
292
+
293
+ <h3 className="text-2xl font-bold mb-8 flex items-center gap-4 relative z-10">
294
+ <div className="p-2 bg-primary/10 rounded-xl">
295
+ <Terminal className="h-6 w-6 text-primary" />
296
+ </div>
297
+ Quick Start: cURL
298
+ </h3>
299
+
300
+ <div className="relative group/code z-10">
301
+ <pre className="bg-white p-6 rounded-2xl font-mono text-sm overflow-x-auto border border-border leading-relaxed shadow-inner">
302
+ <span className="text-emerald-600 font-medium">curl</span> <span className="text-slate-500">-X</span> <span className="text-slate-900">GET </span><span className="text-blue-600 font-medium">"https://nobalmako.com/api/variables?teamId=123&envId=456"</span> \<br />
303
+ &nbsp;&nbsp;<span className="text-slate-500 font-medium">-H</span> <span className="text-blue-600 font-medium">"Authorization: Bearer {'$'}{'{'}NOBALMAKO_TOKEN{'}'}"</span>
304
+ </pre>
305
+ <Button
306
+ variant="ghost"
307
+ size="icon"
308
+ className="absolute top-4 right-4 opacity-0 group-hover/code:opacity-100 transition-opacity text-slate-400 hover:text-slate-900 hover:bg-slate-100 h-8 w-8"
309
+ onClick={() => copyToClipboard(`curl -X GET "https://nobalmako.com/api/variables?teamId=123&envId=456" \\\n -H "Authorization: Bearer \${NOBALMAKO_TOKEN}"`, 'curl')}
310
+ >
311
+ {copied === 'curl' ? <Check className="h-4 w-4 text-emerald-600" /> : <Copy className="h-4 w-4" />}
312
+ </Button>
313
+ </div>
314
+
315
+ <p className="mt-6 text-sm text-slate-500 relative z-10">
316
+ Replace <code>NOBALMAKO_TOKEN</code> with your actual API key to start pulling secrets instantly.
317
+ </p>
318
+ </div>
319
+ </div>
320
+ </div>
321
+ </div>
322
+ </AppLayout>
323
+ );
324
+ }