synapse-gateway 2.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 (135) hide show
  1. package/README.md +385 -0
  2. package/bin/synapse.js +242 -0
  3. package/docs/PLAN.md +1723 -0
  4. package/docs/PRD.md +1799 -0
  5. package/drizzle.config.ts +12 -0
  6. package/next.config.ts +8 -0
  7. package/package.json +82 -0
  8. package/postcss.config.mjs +7 -0
  9. package/public/file.svg +1 -0
  10. package/public/globe.svg +1 -0
  11. package/public/next.svg +1 -0
  12. package/public/vercel.svg +1 -0
  13. package/public/window.svg +1 -0
  14. package/src/app/api/analytics/cost/route.ts +13 -0
  15. package/src/app/api/analytics/usage/route.ts +16 -0
  16. package/src/app/api/auth/login/route.ts +42 -0
  17. package/src/app/api/cache/route.ts +19 -0
  18. package/src/app/api/dashboard/route.ts +35 -0
  19. package/src/app/api/distill/route.ts +10 -0
  20. package/src/app/api/events/route.ts +54 -0
  21. package/src/app/api/health/route.ts +10 -0
  22. package/src/app/api/intelligence/forensics/route.ts +23 -0
  23. package/src/app/api/intelligence/neural-router/route.ts +23 -0
  24. package/src/app/api/keys/route.ts +34 -0
  25. package/src/app/api/mcp/route.ts +49 -0
  26. package/src/app/api/memory/route.ts +10 -0
  27. package/src/app/api/models/benchmark/route.ts +13 -0
  28. package/src/app/api/models/route.ts +39 -0
  29. package/src/app/api/namespace/route.ts +25 -0
  30. package/src/app/api/plugins/route.ts +41 -0
  31. package/src/app/api/providers/accounts/route.ts +91 -0
  32. package/src/app/api/providers/fetch-models/route.ts +52 -0
  33. package/src/app/api/providers/health/route.ts +10 -0
  34. package/src/app/api/providers/route.ts +46 -0
  35. package/src/app/api/routes/pipeline/route.ts +20 -0
  36. package/src/app/api/settings/route.ts +33 -0
  37. package/src/app/api/skills/route.ts +39 -0
  38. package/src/app/api/v1/chat/completions/route.ts +156 -0
  39. package/src/app/api/v1/models/route.ts +44 -0
  40. package/src/app/dashboard/intelligence/loading.tsx +14 -0
  41. package/src/app/dashboard/intelligence/page.tsx +125 -0
  42. package/src/app/dashboard/layout.tsx +143 -0
  43. package/src/app/dashboard/loading.tsx +17 -0
  44. package/src/app/dashboard/memory/loading.tsx +15 -0
  45. package/src/app/dashboard/memory/page.tsx +71 -0
  46. package/src/app/dashboard/models/loading.tsx +13 -0
  47. package/src/app/dashboard/models/page.tsx +107 -0
  48. package/src/app/dashboard/page.tsx +183 -0
  49. package/src/app/dashboard/playground/loading.tsx +17 -0
  50. package/src/app/dashboard/playground/page.tsx +212 -0
  51. package/src/app/dashboard/providers/loading.tsx +15 -0
  52. package/src/app/dashboard/providers/page.tsx +248 -0
  53. package/src/app/dashboard/routes/loading.tsx +15 -0
  54. package/src/app/dashboard/routes/page.tsx +72 -0
  55. package/src/app/dashboard/settings/loading.tsx +20 -0
  56. package/src/app/dashboard/settings/page.tsx +208 -0
  57. package/src/app/dashboard/skills/loading.tsx +26 -0
  58. package/src/app/dashboard/skills/page.tsx +137 -0
  59. package/src/app/dashboard/vault/loading.tsx +18 -0
  60. package/src/app/dashboard/vault/page.tsx +139 -0
  61. package/src/app/favicon.ico +0 -0
  62. package/src/app/globals.css +59 -0
  63. package/src/app/layout.tsx +32 -0
  64. package/src/app/login/page.tsx +87 -0
  65. package/src/app/page.tsx +5 -0
  66. package/src/components/ui/badge.tsx +32 -0
  67. package/src/components/ui/button.tsx +38 -0
  68. package/src/components/ui/card.tsx +50 -0
  69. package/src/components/ui/error-boundary.tsx +47 -0
  70. package/src/components/ui/index.ts +11 -0
  71. package/src/components/ui/input.tsx +26 -0
  72. package/src/components/ui/select.tsx +24 -0
  73. package/src/components/ui/skeleton.tsx +53 -0
  74. package/src/components/ui/toast.tsx +51 -0
  75. package/src/instrumentation.ts +6 -0
  76. package/src/lib/__tests__/auth.test.ts +42 -0
  77. package/src/lib/__tests__/format.test.ts +94 -0
  78. package/src/lib/__tests__/namespace.test.ts +102 -0
  79. package/src/lib/__tests__/squeezer.test.ts +93 -0
  80. package/src/lib/__tests__/utils.test.ts +28 -0
  81. package/src/lib/analytics/index.ts +187 -0
  82. package/src/lib/auth/guard.tsx +71 -0
  83. package/src/lib/auth/index.ts +105 -0
  84. package/src/lib/auth/middleware.ts +64 -0
  85. package/src/lib/benchmark/index.ts +137 -0
  86. package/src/lib/bootstrap.ts +122 -0
  87. package/src/lib/cache/index.ts +1 -0
  88. package/src/lib/cache/semantic.ts +211 -0
  89. package/src/lib/config/defaults.ts +61 -0
  90. package/src/lib/config/index.ts +72 -0
  91. package/src/lib/config/schema.ts +63 -0
  92. package/src/lib/db/index.ts +22 -0
  93. package/src/lib/db/migrate.ts +327 -0
  94. package/src/lib/db/schema.ts +303 -0
  95. package/src/lib/distiller/index.ts +331 -0
  96. package/src/lib/fallback/index.ts +153 -0
  97. package/src/lib/forensics/index.ts +188 -0
  98. package/src/lib/format/anthropic.ts +139 -0
  99. package/src/lib/format/gemini.ts +130 -0
  100. package/src/lib/format/index.ts +3 -0
  101. package/src/lib/format/openai.ts +78 -0
  102. package/src/lib/health/index.ts +158 -0
  103. package/src/lib/mcp/builtin.ts +83 -0
  104. package/src/lib/mcp/index.ts +1 -0
  105. package/src/lib/mcp/registry.ts +49 -0
  106. package/src/lib/memory/index.ts +3 -0
  107. package/src/lib/memory/store.ts +215 -0
  108. package/src/lib/memory/types.ts +56 -0
  109. package/src/lib/namespace/index.ts +89 -0
  110. package/src/lib/neural/features.ts +74 -0
  111. package/src/lib/neural/index.ts +85 -0
  112. package/src/lib/neural/strategies.ts +124 -0
  113. package/src/lib/pipeline/index.ts +84 -0
  114. package/src/lib/pipeline/types.ts +77 -0
  115. package/src/lib/plugins/builtin.ts +79 -0
  116. package/src/lib/plugins/index.ts +65 -0
  117. package/src/lib/prediction/index.ts +113 -0
  118. package/src/lib/providers/api-key/anthropic.ts +96 -0
  119. package/src/lib/providers/api-key/deepseek.ts +108 -0
  120. package/src/lib/providers/api-key/gemini.ts +112 -0
  121. package/src/lib/providers/api-key/openai.ts +122 -0
  122. package/src/lib/providers/api-key/openrouter.ts +112 -0
  123. package/src/lib/providers/base-adapter.ts +122 -0
  124. package/src/lib/providers/registry.ts +46 -0
  125. package/src/lib/providers/types.ts +121 -0
  126. package/src/lib/router/index.ts +82 -0
  127. package/src/lib/skills/forge.ts +57 -0
  128. package/src/lib/skills/index.ts +3 -0
  129. package/src/lib/skills/registry.ts +195 -0
  130. package/src/lib/skills/types.ts +44 -0
  131. package/src/lib/squeezer/index.ts +158 -0
  132. package/src/lib/utils/cn.ts +6 -0
  133. package/src/lib/utils/logger.ts +16 -0
  134. package/src/middleware.ts +60 -0
  135. package/tsconfig.json +34 -0
@@ -0,0 +1,87 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { Zap } from 'lucide-react'
5
+
6
+ export default function LoginPage() {
7
+ const [password, setPassword] = useState('')
8
+ const [error, setError] = useState('')
9
+ const [loading, setLoading] = useState(false)
10
+
11
+ async function handleSubmit(e: React.FormEvent) {
12
+ e.preventDefault()
13
+ if (!password) return
14
+ setLoading(true)
15
+ setError('')
16
+
17
+ try {
18
+ const res = await fetch('/api/auth/login', {
19
+ method: 'POST',
20
+ headers: { 'Content-Type': 'application/json' },
21
+ body: JSON.stringify({ password }),
22
+ })
23
+ const data = await res.json()
24
+
25
+ if (!res.ok) {
26
+ setError(data.error || 'Login failed')
27
+ return
28
+ }
29
+
30
+ localStorage.setItem('synapse_token', data.token)
31
+ localStorage.setItem('synapse_user', JSON.stringify(data.user))
32
+ window.location.href = '/dashboard'
33
+ } catch {
34
+ setError('Network error')
35
+ } finally {
36
+ setLoading(false)
37
+ }
38
+ }
39
+
40
+ return (
41
+ <div className="min-h-screen flex items-center justify-center bg-background p-4">
42
+ <div className="w-full max-w-sm space-y-8">
43
+ <div className="text-center space-y-2">
44
+ <div className="flex items-center justify-center gap-2 mb-4">
45
+ <div className="p-2 rounded-lg bg-primary/10">
46
+ <Zap className="h-6 w-6 text-primary" />
47
+ </div>
48
+ </div>
49
+ <h1 className="text-2xl font-bold tracking-tight">Synapse</h1>
50
+ <p className="text-sm text-muted-foreground">AI Gateway & Intelligence Platform</p>
51
+ </div>
52
+
53
+ <form onSubmit={handleSubmit} className="space-y-4">
54
+ <div>
55
+ <label htmlFor="password" className="text-sm font-medium block mb-1.5">Password</label>
56
+ <input
57
+ id="password"
58
+ type="password"
59
+ value={password}
60
+ onChange={(e) => setPassword(e.target.value)}
61
+ placeholder="Enter admin password"
62
+ className="w-full bg-muted border border-border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
63
+ autoFocus
64
+ disabled={loading}
65
+ />
66
+ </div>
67
+
68
+ {error && (
69
+ <div className="text-sm text-destructive bg-destructive/10 px-3 py-2 rounded-md">{error}</div>
70
+ )}
71
+
72
+ <button
73
+ type="submit"
74
+ disabled={loading || !password}
75
+ className="w-full py-2 bg-primary text-primary-foreground rounded-md text-sm font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
76
+ >
77
+ {loading ? 'Signing in...' : 'Sign In'}
78
+ </button>
79
+ </form>
80
+
81
+ <p className="text-xs text-center text-muted-foreground">
82
+ Default password: <code className="font-mono bg-muted px-1.5 py-0.5 rounded">changeme</code>
83
+ </p>
84
+ </div>
85
+ </div>
86
+ )
87
+ }
@@ -0,0 +1,5 @@
1
+ import { redirect } from 'next/navigation'
2
+
3
+ export default function Home() {
4
+ redirect('/dashboard')
5
+ }
@@ -0,0 +1,32 @@
1
+ import * as React from 'react'
2
+ import { cva, type VariantProps } from 'class-variance-authority'
3
+ import { cn } from '@/lib/utils/cn'
4
+
5
+ const badgeVariants = cva(
6
+ 'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium transition-colors',
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: 'bg-primary/10 text-primary',
11
+ secondary: 'bg-secondary text-secondary-foreground',
12
+ accent: 'bg-accent/10 text-accent',
13
+ destructive: 'bg-destructive/10 text-destructive',
14
+ outline: 'border border-border text-foreground',
15
+ warning: 'bg-yellow-500/10 text-yellow-500',
16
+ },
17
+ },
18
+ defaultVariants: {
19
+ variant: 'default',
20
+ },
21
+ },
22
+ )
23
+
24
+ export interface BadgeProps
25
+ extends React.HTMLAttributes<HTMLSpanElement>,
26
+ VariantProps<typeof badgeVariants> {}
27
+
28
+ function Badge({ className, variant, ...props }: BadgeProps) {
29
+ return <span className={cn(badgeVariants({ variant }), className)} {...props} />
30
+ }
31
+
32
+ export { Badge, badgeVariants }
@@ -0,0 +1,38 @@
1
+ import * as React from 'react'
2
+ import { cn } from '@/lib/utils/cn'
3
+
4
+ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
5
+ variant?: 'default' | 'secondary' | 'outline' | 'ghost' | 'destructive'
6
+ size?: 'sm' | 'md' | 'lg' | 'icon'
7
+ }
8
+
9
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
10
+ ({ className, variant = 'default', size = 'md', ...props }, ref) => {
11
+ return (
12
+ <button
13
+ className={cn(
14
+ 'inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
15
+ {
16
+ 'bg-primary text-primary-foreground hover:bg-primary/90': variant === 'default',
17
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80': variant === 'secondary',
18
+ 'border border-border bg-transparent hover:bg-muted': variant === 'outline',
19
+ 'hover:bg-muted': variant === 'ghost',
20
+ 'bg-destructive text-destructive-foreground hover:bg-destructive/90': variant === 'destructive',
21
+ },
22
+ {
23
+ 'h-7 px-2.5 text-xs': size === 'sm',
24
+ 'h-9 px-4': size === 'md',
25
+ 'h-11 px-6 text-base': size === 'lg',
26
+ 'h-8 w-8 p-0': size === 'icon',
27
+ },
28
+ className,
29
+ )}
30
+ ref={ref}
31
+ {...props}
32
+ />
33
+ )
34
+ },
35
+ )
36
+ Button.displayName = 'Button'
37
+
38
+ export { Button }
@@ -0,0 +1,50 @@
1
+ import * as React from 'react'
2
+ import { cn } from '@/lib/utils/cn'
3
+
4
+ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
5
+ ({ className, ...props }, ref) => (
6
+ <div
7
+ ref={ref}
8
+ className={cn('rounded-lg border border-border bg-card text-card-foreground', className)}
9
+ {...props}
10
+ />
11
+ ),
12
+ )
13
+ Card.displayName = 'Card'
14
+
15
+ const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
16
+ ({ className, ...props }, ref) => (
17
+ <div ref={ref} className={cn('px-4 py-3 border-b border-border', className)} {...props} />
18
+ ),
19
+ )
20
+ CardHeader.displayName = 'CardHeader'
21
+
22
+ const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
23
+ ({ className, ...props }, ref) => (
24
+ <h3 ref={ref} className={cn('text-sm font-medium', className)} {...props} />
25
+ ),
26
+ )
27
+ CardTitle.displayName = 'CardTitle'
28
+
29
+ const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
30
+ ({ className, ...props }, ref) => (
31
+ <p ref={ref} className={cn('text-xs text-muted-foreground', className)} {...props} />
32
+ ),
33
+ )
34
+ CardDescription.displayName = 'CardDescription'
35
+
36
+ const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
37
+ ({ className, ...props }, ref) => (
38
+ <div ref={ref} className={cn('p-4', className)} {...props} />
39
+ ),
40
+ )
41
+ CardContent.displayName = 'CardContent'
42
+
43
+ const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
44
+ ({ className, ...props }, ref) => (
45
+ <div ref={ref} className={cn('px-4 py-3 border-t border-border flex items-center', className)} {...props} />
46
+ ),
47
+ )
48
+ CardFooter.displayName = 'CardFooter'
49
+
50
+ export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter }
@@ -0,0 +1,47 @@
1
+ 'use client'
2
+
3
+ import { Component, type ReactNode } from 'react'
4
+
5
+ interface Props {
6
+ children: ReactNode
7
+ fallback?: ReactNode
8
+ }
9
+
10
+ interface State {
11
+ hasError: boolean
12
+ error: Error | null
13
+ }
14
+
15
+ export class ErrorBoundary extends Component<Props, State> {
16
+ constructor(props: Props) {
17
+ super(props)
18
+ this.state = { hasError: false, error: null }
19
+ }
20
+
21
+ static getDerivedStateFromError(error: Error): State {
22
+ return { hasError: true, error }
23
+ }
24
+
25
+ render() {
26
+ if (this.state.hasError) {
27
+ if (this.props.fallback) return this.props.fallback
28
+
29
+ return (
30
+ <div className="flex items-center justify-center min-h-[200px] p-8">
31
+ <div className="text-center space-y-3">
32
+ <div className="text-destructive text-sm font-medium">Something went wrong</div>
33
+ <p className="text-xs text-muted-foreground max-w-md">{this.state.error?.message || 'An unexpected error occurred'}</p>
34
+ <button
35
+ onClick={() => this.setState({ hasError: false, error: null })}
36
+ className="px-3 py-1.5 text-xs border border-border rounded-md hover:bg-muted transition-colors"
37
+ >
38
+ Try Again
39
+ </button>
40
+ </div>
41
+ </div>
42
+ )
43
+ }
44
+
45
+ return this.props.children
46
+ }
47
+ }
@@ -0,0 +1,11 @@
1
+ export { Button } from './button'
2
+ export type { ButtonProps } from './button'
3
+ export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './card'
4
+ export { Badge } from './badge'
5
+ export type { BadgeProps } from './badge'
6
+ export { Input } from './input'
7
+ export type { InputProps } from './input'
8
+ export { Select } from './select'
9
+ export { Skeleton, CardSkeleton, TableSkeleton, StatsSkeleton } from './skeleton'
10
+ export { ErrorBoundary } from './error-boundary'
11
+ export { ToastProvider, useToast } from './toast'
@@ -0,0 +1,26 @@
1
+ import * as React from 'react'
2
+ import { cn } from '@/lib/utils/cn'
3
+
4
+ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
5
+ error?: boolean
6
+ }
7
+
8
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
9
+ ({ className, type, error, ...props }, ref) => {
10
+ return (
11
+ <input
12
+ type={type}
13
+ className={cn(
14
+ 'flex h-9 w-full rounded-md border border-border bg-muted px-3 py-1.5 text-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
15
+ error && 'border-destructive focus-visible:ring-destructive',
16
+ className,
17
+ )}
18
+ ref={ref}
19
+ {...props}
20
+ />
21
+ )
22
+ },
23
+ )
24
+ Input.displayName = 'Input'
25
+
26
+ export { Input }
@@ -0,0 +1,24 @@
1
+ import * as React from 'react'
2
+ import { cn } from '@/lib/utils/cn'
3
+
4
+ export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {}
5
+
6
+ const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
7
+ ({ className, children, ...props }, ref) => {
8
+ return (
9
+ <select
10
+ className={cn(
11
+ 'flex h-9 w-full rounded-md border border-border bg-muted px-3 py-1.5 text-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
12
+ className,
13
+ )}
14
+ ref={ref}
15
+ {...props}
16
+ >
17
+ {children}
18
+ </select>
19
+ )
20
+ },
21
+ )
22
+ Select.displayName = 'Select'
23
+
24
+ export { Select }
@@ -0,0 +1,53 @@
1
+ export function Skeleton({ className }: { className?: string }) {
2
+ return (
3
+ <div className={`animate-pulse rounded-md bg-muted ${className || ''}`} />
4
+ )
5
+ }
6
+
7
+ export function CardSkeleton() {
8
+ return (
9
+ <div className="bg-card border border-border rounded-lg p-4 space-y-3">
10
+ <div className="flex items-center justify-between">
11
+ <Skeleton className="h-4 w-32" />
12
+ <Skeleton className="h-5 w-16 rounded-full" />
13
+ </div>
14
+ <Skeleton className="h-3 w-48" />
15
+ <div className="flex gap-4">
16
+ <Skeleton className="h-3 w-16" />
17
+ <Skeleton className="h-3 w-20" />
18
+ </div>
19
+ </div>
20
+ )
21
+ }
22
+
23
+ export function TableSkeleton({ rows = 5 }: { rows?: number }) {
24
+ return (
25
+ <div className="bg-card border border-border rounded-lg overflow-hidden">
26
+ <div className="border-b border-border bg-muted/50 px-4 py-2.5">
27
+ <Skeleton className="h-3 w-full" />
28
+ </div>
29
+ {Array.from({ length: rows }).map((_, i) => (
30
+ <div key={i} className="border-b border-border px-4 py-3 flex gap-4">
31
+ <Skeleton className="h-3 w-32" />
32
+ <Skeleton className="h-3 w-20" />
33
+ <Skeleton className="h-3 w-16" />
34
+ <Skeleton className="h-3 w-12" />
35
+ <Skeleton className="h-3 w-16" />
36
+ </div>
37
+ ))}
38
+ </div>
39
+ )
40
+ }
41
+
42
+ export function StatsSkeleton() {
43
+ return (
44
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
45
+ {Array.from({ length: 4 }).map((_, i) => (
46
+ <div key={i} className="bg-card border border-border rounded-lg p-4 space-y-2">
47
+ <Skeleton className="h-3 w-20" />
48
+ <Skeleton className="h-7 w-24" />
49
+ </div>
50
+ ))}
51
+ </div>
52
+ )
53
+ }
@@ -0,0 +1,51 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState, useCallback, createContext, useContext, type ReactNode } from 'react'
4
+
5
+ interface Toast {
6
+ id: string
7
+ message: string
8
+ type: 'success' | 'error' | 'info'
9
+ }
10
+
11
+ interface ToastContextValue {
12
+ toast: (message: string, type?: Toast['type']) => void
13
+ }
14
+
15
+ const ToastContext = createContext<ToastContextValue>({ toast: () => {} })
16
+
17
+ export function useToast() {
18
+ return useContext(ToastContext)
19
+ }
20
+
21
+ export function ToastProvider({ children }: { children: ReactNode }) {
22
+ const [toasts, setToasts] = useState<Toast[]>([])
23
+
24
+ const toast = useCallback((message: string, type: Toast['type'] = 'info') => {
25
+ const id = crypto.randomUUID()
26
+ setToasts((prev) => [...prev, { id, message, type }])
27
+ setTimeout(() => {
28
+ setToasts((prev) => prev.filter((t) => t.id !== id))
29
+ }, 4000)
30
+ }, [])
31
+
32
+ return (
33
+ <ToastContext.Provider value={{ toast }}>
34
+ {children}
35
+ <div className="fixed bottom-4 right-4 z-50 space-y-2 pointer-events-none">
36
+ {toasts.map((t) => (
37
+ <div
38
+ key={t.id}
39
+ className={`pointer-events-auto px-4 py-2.5 rounded-md text-sm shadow-lg border animate-in slide-in-from-right fade-in duration-200 ${
40
+ t.type === 'success' ? 'bg-primary/10 border-primary/20 text-primary' :
41
+ t.type === 'error' ? 'bg-destructive/10 border-destructive/20 text-destructive' :
42
+ 'bg-card border-border text-foreground'
43
+ }`}
44
+ >
45
+ {t.message}
46
+ </div>
47
+ ))}
48
+ </div>
49
+ </ToastContext.Provider>
50
+ )
51
+ }
@@ -0,0 +1,6 @@
1
+ export async function register() {
2
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
3
+ const { bootstrap } = await import('./lib/bootstrap')
4
+ await bootstrap()
5
+ }
6
+ }
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { hashApiKey, verifyJwt, signJwt } from '../auth'
3
+
4
+ describe('Auth', () => {
5
+ describe('hashApiKey', () => {
6
+ it('produces consistent SHA-256 hashes', async () => {
7
+ const hash1 = await hashApiKey('test-key')
8
+ const hash2 = await hashApiKey('test-key')
9
+ expect(hash1).toBe(hash2)
10
+ expect(hash1).toHaveLength(64)
11
+ })
12
+
13
+ it('produces different hashes for different inputs', async () => {
14
+ const hash1 = await hashApiKey('key-1')
15
+ const hash2 = await hashApiKey('key-2')
16
+ expect(hash1).not.toBe(hash2)
17
+ })
18
+ })
19
+
20
+ describe('JWT', () => {
21
+ it('signs and verifies a token', async () => {
22
+ const token = await signJwt({ sub: 'admin', role: 'admin', name: 'Admin' })
23
+ expect(token).toBeTruthy()
24
+
25
+ const payload = await verifyJwt(token)
26
+ expect(payload).not.toBeNull()
27
+ expect(payload!.sub).toBe('admin')
28
+ expect(payload!.role).toBe('admin')
29
+ expect(payload!.name).toBe('Admin')
30
+ })
31
+
32
+ it('rejects invalid tokens', async () => {
33
+ const payload = await verifyJwt('invalid-token')
34
+ expect(payload).toBeNull()
35
+ })
36
+
37
+ it('rejects empty tokens', async () => {
38
+ const payload = await verifyJwt('')
39
+ expect(payload).toBeNull()
40
+ })
41
+ })
42
+ })
@@ -0,0 +1,94 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { toOpenAIRequest, fromOpenAIResponse } from '../format/openai'
3
+ import { toAnthropicRequest, fromAnthropicResponse } from '../format/anthropic'
4
+ import { toGeminiRequest, fromGeminiResponse } from '../format/gemini'
5
+ import type { NormalizedRequest } from '../providers/types'
6
+
7
+ function makeReq(messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>): NormalizedRequest {
8
+ return {
9
+ model: 'test-model',
10
+ messages: messages.map((m) => ({ ...m, role: m.role as 'system' | 'user' | 'assistant' | 'tool' })),
11
+ }
12
+ }
13
+
14
+ describe('Format Translators', () => {
15
+ describe('OpenAI', () => {
16
+ it('converts normalized request to OpenAI format', () => {
17
+ const req = makeReq([
18
+ { role: 'user', content: 'Hello' },
19
+ { role: 'assistant', content: 'Hi' },
20
+ ])
21
+ const result = toOpenAIRequest(req)
22
+ expect(result.messages).toHaveLength(2)
23
+ expect(result.messages[0].role).toBe('user')
24
+ expect(result.messages[1].role).toBe('assistant')
25
+ })
26
+
27
+ it('parses OpenAI response', () => {
28
+ const raw = {
29
+ id: 'chatcmpl-123',
30
+ model: 'gpt-4o',
31
+ choices: [{ index: 0, message: { role: 'assistant', content: 'Hello!' }, finish_reason: 'stop' }],
32
+ usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
33
+ }
34
+ const result = fromOpenAIResponse(raw)
35
+ expect(result.id).toBe('chatcmpl-123')
36
+ expect(result.choices[0].message.content).toBe('Hello!')
37
+ expect(result.usage.totalTokens).toBe(15)
38
+ })
39
+ })
40
+
41
+ describe('Anthropic', () => {
42
+ it('extracts system message from anthropic format', () => {
43
+ const req = makeReq([
44
+ { role: 'system', content: 'You are helpful' },
45
+ { role: 'user', content: 'Hello' },
46
+ ])
47
+ const result = toAnthropicRequest(req)
48
+ expect(result.system).toBe('You are helpful')
49
+ })
50
+
51
+ it('parses Anthropic response', () => {
52
+ const raw = {
53
+ id: 'msg-123',
54
+ model: 'claude-sonnet-4',
55
+ content: [{ type: 'text', text: 'Hello!' }],
56
+ stop_reason: 'end_turn',
57
+ usage: { input_tokens: 10, output_tokens: 5 },
58
+ }
59
+ const result = fromAnthropicResponse(raw)
60
+ expect(result.id).toBe('msg-123')
61
+ expect(result.choices[0].message.content).toBe('Hello!')
62
+ })
63
+ })
64
+
65
+ describe('Gemini', () => {
66
+ it('creates Gemini contents array', () => {
67
+ const req = makeReq([
68
+ { role: 'user', content: 'Hello' },
69
+ { role: 'assistant', content: 'Hi' },
70
+ ])
71
+ const result = toGeminiRequest(req)
72
+ expect(result.contents).toBeDefined()
73
+ expect(result.contents.length).toBeGreaterThan(0)
74
+ })
75
+
76
+ it('extracts systemInstruction', () => {
77
+ const req = makeReq([
78
+ { role: 'system', content: 'Be helpful' },
79
+ { role: 'user', content: 'Hello' },
80
+ ])
81
+ const result = toGeminiRequest(req)
82
+ expect(result.systemInstruction).toBeDefined()
83
+ })
84
+
85
+ it('parses Gemini response', () => {
86
+ const raw = {
87
+ candidates: [{ content: { parts: [{ text: 'Hello!' }] }, finishReason: 'STOP' }],
88
+ usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 5, totalTokenCount: 15 },
89
+ }
90
+ const result = fromGeminiResponse(raw)
91
+ expect(result.choices[0].message.content).toBe('Hello!')
92
+ })
93
+ })
94
+ })