ugly-app 0.1.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/.claude/settings.local.json +18 -0
- package/.env.example +9 -0
- package/.yarn-metadata.json +113 -0
- package/README.md +709 -0
- package/dist/cli/authCommands.d.ts +6 -0
- package/dist/cli/authCommands.d.ts.map +1 -0
- package/dist/cli/authCommands.js +50 -0
- package/dist/cli/authCommands.js.map +1 -0
- package/dist/cli/build.d.ts +2 -0
- package/dist/cli/build.d.ts.map +1 -0
- package/dist/cli/build.js +26 -0
- package/dist/cli/build.js.map +1 -0
- package/dist/cli/configure.d.ts +2 -0
- package/dist/cli/configure.d.ts.map +1 -0
- package/dist/cli/configure.js +53 -0
- package/dist/cli/configure.js.map +1 -0
- package/dist/cli/dev.d.ts +2 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/dev.js +66 -0
- package/dist/cli/dev.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +340 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/initDb.d.ts +2 -0
- package/dist/cli/initDb.d.ts.map +1 -0
- package/dist/cli/initDb.js +110 -0
- package/dist/cli/initDb.js.map +1 -0
- package/dist/cli/logQuery.d.ts +12 -0
- package/dist/cli/logQuery.d.ts.map +1 -0
- package/dist/cli/logQuery.js +176 -0
- package/dist/cli/logQuery.js.map +1 -0
- package/dist/cli/migrate.d.ts +4 -0
- package/dist/cli/migrate.d.ts.map +1 -0
- package/dist/cli/migrate.js +62 -0
- package/dist/cli/migrate.js.map +1 -0
- package/dist/cli/publishAssets.d.ts +6 -0
- package/dist/cli/publishAssets.d.ts.map +1 -0
- package/dist/cli/publishAssets.js +87 -0
- package/dist/cli/publishAssets.js.map +1 -0
- package/dist/cli/purgeAssets.d.ts +2 -0
- package/dist/cli/purgeAssets.d.ts.map +1 -0
- package/dist/cli/purgeAssets.js +36 -0
- package/dist/cli/purgeAssets.js.map +1 -0
- package/dist/cli/scaffold.d.ts +2 -0
- package/dist/cli/scaffold.d.ts.map +1 -0
- package/dist/cli/scaffold.js +37 -0
- package/dist/cli/scaffold.js.map +1 -0
- package/dist/cli/serverLogQuery.d.ts +6 -0
- package/dist/cli/serverLogQuery.d.ts.map +1 -0
- package/dist/cli/serverLogQuery.js +76 -0
- package/dist/cli/serverLogQuery.js.map +1 -0
- package/dist/cli/storageClient.d.ts +3 -0
- package/dist/cli/storageClient.d.ts.map +1 -0
- package/dist/cli/storageClient.js +22 -0
- package/dist/cli/storageClient.js.map +1 -0
- package/dist/cli/uglyappConfig.d.ts +29 -0
- package/dist/cli/uglyappConfig.d.ts.map +1 -0
- package/dist/cli/uglyappConfig.js +125 -0
- package/dist/cli/uglyappConfig.js.map +1 -0
- package/dist/client/AppProvider.d.ts +32 -0
- package/dist/client/AppProvider.d.ts.map +1 -0
- package/dist/client/AppProvider.js +126 -0
- package/dist/client/AppProvider.js.map +1 -0
- package/dist/client/ErrorLog.d.ts +16 -0
- package/dist/client/ErrorLog.d.ts.map +1 -0
- package/dist/client/ErrorLog.js +28 -0
- package/dist/client/ErrorLog.js.map +1 -0
- package/dist/client/FeedbackContext.d.ts +7 -0
- package/dist/client/FeedbackContext.d.ts.map +1 -0
- package/dist/client/FeedbackContext.js +15 -0
- package/dist/client/FeedbackContext.js.map +1 -0
- package/dist/client/Logger.d.ts +2 -0
- package/dist/client/Logger.d.ts.map +1 -0
- package/dist/client/Logger.js +72 -0
- package/dist/client/Logger.js.map +1 -0
- package/dist/client/LoginPopup.d.ts +7 -0
- package/dist/client/LoginPopup.d.ts.map +1 -0
- package/dist/client/LoginPopup.js +55 -0
- package/dist/client/LoginPopup.js.map +1 -0
- package/dist/client/Router.d.ts +56 -0
- package/dist/client/Router.d.ts.map +1 -0
- package/dist/client/Router.js +224 -0
- package/dist/client/Router.js.map +1 -0
- package/dist/client/Screenshot.d.ts +7 -0
- package/dist/client/Screenshot.d.ts.map +1 -0
- package/dist/client/Screenshot.js +54 -0
- package/dist/client/Screenshot.js.map +1 -0
- package/dist/client/ViewFlipper.d.ts +16 -0
- package/dist/client/ViewFlipper.d.ts.map +1 -0
- package/dist/client/ViewFlipper.js +156 -0
- package/dist/client/ViewFlipper.js.map +1 -0
- package/dist/client/animation/Animated.d.ts +14 -0
- package/dist/client/animation/Animated.d.ts.map +1 -0
- package/dist/client/animation/Animated.js +97 -0
- package/dist/client/animation/Animated.js.map +1 -0
- package/dist/client/animation/animatedValue.d.ts +41 -0
- package/dist/client/animation/animatedValue.d.ts.map +1 -0
- package/dist/client/animation/animatedValue.js +123 -0
- package/dist/client/animation/animatedValue.js.map +1 -0
- package/dist/client/animations/FadeIn.d.ts +6 -0
- package/dist/client/animations/FadeIn.d.ts.map +1 -0
- package/dist/client/animations/FadeIn.js +24 -0
- package/dist/client/animations/FadeIn.js.map +1 -0
- package/dist/client/animations/SlideFromBottom.d.ts +6 -0
- package/dist/client/animations/SlideFromBottom.d.ts.map +1 -0
- package/dist/client/animations/SlideFromBottom.js +24 -0
- package/dist/client/animations/SlideFromBottom.js.map +1 -0
- package/dist/client/animations/SlideFromRight.d.ts +6 -0
- package/dist/client/animations/SlideFromRight.d.ts.map +1 -0
- package/dist/client/animations/SlideFromRight.js +24 -0
- package/dist/client/animations/SlideFromRight.js.map +1 -0
- package/dist/client/audio/AudioPlayer.d.ts +11 -0
- package/dist/client/audio/AudioPlayer.d.ts.map +1 -0
- package/dist/client/audio/AudioPlayer.js +51 -0
- package/dist/client/audio/AudioPlayer.js.map +1 -0
- package/dist/client/audio/AudioRecorder.d.ts +11 -0
- package/dist/client/audio/AudioRecorder.d.ts.map +1 -0
- package/dist/client/audio/AudioRecorder.js +66 -0
- package/dist/client/audio/AudioRecorder.js.map +1 -0
- package/dist/client/audio/audioPlayer.worklet.d.ts +2 -0
- package/dist/client/audio/audioPlayer.worklet.d.ts.map +1 -0
- package/dist/client/audio/audioPlayer.worklet.js +46 -0
- package/dist/client/audio/audioPlayer.worklet.js.map +1 -0
- package/dist/client/audio/useSTT.d.ts +19 -0
- package/dist/client/audio/useSTT.d.ts.map +1 -0
- package/dist/client/audio/useSTT.js +91 -0
- package/dist/client/audio/useSTT.js.map +1 -0
- package/dist/client/audio/useTTS.d.ts +17 -0
- package/dist/client/audio/useTTS.d.ts.map +1 -0
- package/dist/client/audio/useTTS.js +70 -0
- package/dist/client/audio/useTTS.js.map +1 -0
- package/dist/client/components/Button.d.ts +22 -0
- package/dist/client/components/Button.d.ts.map +1 -0
- package/dist/client/components/Button.js +27 -0
- package/dist/client/components/Button.js.map +1 -0
- package/dist/client/components/Card.d.ts +8 -0
- package/dist/client/components/Card.d.ts.map +1 -0
- package/dist/client/components/Card.js +6 -0
- package/dist/client/components/Card.js.map +1 -0
- package/dist/client/components/EnumInput.d.ts +16 -0
- package/dist/client/components/EnumInput.d.ts.map +1 -0
- package/dist/client/components/EnumInput.js +11 -0
- package/dist/client/components/EnumInput.js.map +1 -0
- package/dist/client/components/FeedbackButton.d.ts +9 -0
- package/dist/client/components/FeedbackButton.d.ts.map +1 -0
- package/dist/client/components/FeedbackButton.js +112 -0
- package/dist/client/components/FeedbackButton.js.map +1 -0
- package/dist/client/components/Header.d.ts +10 -0
- package/dist/client/components/Header.d.ts.map +1 -0
- package/dist/client/components/Header.js +12 -0
- package/dist/client/components/Header.js.map +1 -0
- package/dist/client/components/Image.d.ts +20 -0
- package/dist/client/components/Image.d.ts.map +1 -0
- package/dist/client/components/Image.js +12 -0
- package/dist/client/components/Image.js.map +1 -0
- package/dist/client/components/Input.d.ts +14 -0
- package/dist/client/components/Input.d.ts.map +1 -0
- package/dist/client/components/Input.js +19 -0
- package/dist/client/components/Input.js.map +1 -0
- package/dist/client/components/Modal.d.ts +9 -0
- package/dist/client/components/Modal.d.ts.map +1 -0
- package/dist/client/components/Modal.js +19 -0
- package/dist/client/components/Modal.js.map +1 -0
- package/dist/client/components/PageLayout.d.ts +9 -0
- package/dist/client/components/PageLayout.d.ts.map +1 -0
- package/dist/client/components/PageLayout.js +6 -0
- package/dist/client/components/PageLayout.js.map +1 -0
- package/dist/client/components/Panel.d.ts +8 -0
- package/dist/client/components/Panel.d.ts.map +1 -0
- package/dist/client/components/Panel.js +5 -0
- package/dist/client/components/Panel.js.map +1 -0
- package/dist/client/components/PopupPanel.d.ts +9 -0
- package/dist/client/components/PopupPanel.d.ts.map +1 -0
- package/dist/client/components/PopupPanel.js +5 -0
- package/dist/client/components/PopupPanel.js.map +1 -0
- package/dist/client/components/Pressable.d.ts +14 -0
- package/dist/client/components/Pressable.d.ts.map +1 -0
- package/dist/client/components/Pressable.js +8 -0
- package/dist/client/components/Pressable.js.map +1 -0
- package/dist/client/components/ScrollView.d.ts +9 -0
- package/dist/client/components/ScrollView.d.ts.map +1 -0
- package/dist/client/components/ScrollView.js +7 -0
- package/dist/client/components/ScrollView.js.map +1 -0
- package/dist/client/components/SettingGroup.d.ts +10 -0
- package/dist/client/components/SettingGroup.d.ts.map +1 -0
- package/dist/client/components/SettingGroup.js +8 -0
- package/dist/client/components/SettingGroup.js.map +1 -0
- package/dist/client/components/Text.d.ts +21 -0
- package/dist/client/components/Text.d.ts.map +1 -0
- package/dist/client/components/Text.js +43 -0
- package/dist/client/components/Text.js.map +1 -0
- package/dist/client/components/Toast.d.ts +10 -0
- package/dist/client/components/Toast.d.ts.map +1 -0
- package/dist/client/components/Toast.js +21 -0
- package/dist/client/components/Toast.js.map +1 -0
- package/dist/client/components/View.d.ts +9 -0
- package/dist/client/components/View.d.ts.map +1 -0
- package/dist/client/components/View.js +5 -0
- package/dist/client/components/View.js.map +1 -0
- package/dist/client/components/index.d.ts +33 -0
- package/dist/client/components/index.d.ts.map +1 -0
- package/dist/client/components/index.js +17 -0
- package/dist/client/components/index.js.map +1 -0
- package/dist/client/components/zIndex.d.ts +10 -0
- package/dist/client/components/zIndex.d.ts.map +1 -0
- package/dist/client/components/zIndex.js +12 -0
- package/dist/client/components/zIndex.js.map +1 -0
- package/dist/client/createSocket.d.ts +26 -0
- package/dist/client/createSocket.d.ts.map +1 -0
- package/dist/client/createSocket.js +138 -0
- package/dist/client/createSocket.js.map +1 -0
- package/dist/client/index.d.ts +27 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +20 -0
- package/dist/client/index.js.map +1 -0
- package/dist/playwright/index.d.ts +16 -0
- package/dist/playwright/index.d.ts.map +1 -0
- package/dist/playwright/index.js +32 -0
- package/dist/playwright/index.js.map +1 -0
- package/dist/server/App.d.ts +43 -0
- package/dist/server/App.d.ts.map +1 -0
- package/dist/server/App.js +215 -0
- package/dist/server/App.js.map +1 -0
- package/dist/server/Auth.d.ts +21 -0
- package/dist/server/Auth.d.ts.map +1 -0
- package/dist/server/Auth.js +171 -0
- package/dist/server/Auth.js.map +1 -0
- package/dist/server/Cache.d.ts +38 -0
- package/dist/server/Cache.d.ts.map +1 -0
- package/dist/server/Cache.js +113 -0
- package/dist/server/Cache.js.map +1 -0
- package/dist/server/ChangeStream.d.ts +22 -0
- package/dist/server/ChangeStream.d.ts.map +1 -0
- package/dist/server/ChangeStream.js +93 -0
- package/dist/server/ChangeStream.js.map +1 -0
- package/dist/server/DB.d.ts +17 -0
- package/dist/server/DB.d.ts.map +1 -0
- package/dist/server/DB.js +442 -0
- package/dist/server/DB.js.map +1 -0
- package/dist/server/Email.d.ts +22 -0
- package/dist/server/Email.d.ts.map +1 -0
- package/dist/server/Email.js +64 -0
- package/dist/server/Email.js.map +1 -0
- package/dist/server/EmailTemplate.d.ts +17 -0
- package/dist/server/EmailTemplate.d.ts.map +1 -0
- package/dist/server/EmailTemplate.js +29 -0
- package/dist/server/EmailTemplate.js.map +1 -0
- package/dist/server/FeedbackReport.d.ts +19 -0
- package/dist/server/FeedbackReport.d.ts.map +1 -0
- package/dist/server/FeedbackReport.js +94 -0
- package/dist/server/FeedbackReport.js.map +1 -0
- package/dist/server/ImageGen.d.ts +5 -0
- package/dist/server/ImageGen.d.ts.map +1 -0
- package/dist/server/ImageGen.js +5 -0
- package/dist/server/ImageGen.js.map +1 -0
- package/dist/server/Logging.d.ts +59 -0
- package/dist/server/Logging.d.ts.map +1 -0
- package/dist/server/Logging.js +293 -0
- package/dist/server/Logging.js.map +1 -0
- package/dist/server/Nats.d.ts +56 -0
- package/dist/server/Nats.d.ts.map +1 -0
- package/dist/server/Nats.js +162 -0
- package/dist/server/Nats.js.map +1 -0
- package/dist/server/NatsStore.d.ts +26 -0
- package/dist/server/NatsStore.d.ts.map +1 -0
- package/dist/server/NatsStore.js +86 -0
- package/dist/server/NatsStore.js.map +1 -0
- package/dist/server/PushNotification.d.ts +33 -0
- package/dist/server/PushNotification.d.ts.map +1 -0
- package/dist/server/PushNotification.js +131 -0
- package/dist/server/PushNotification.js.map +1 -0
- package/dist/server/RateLimit.d.ts +28 -0
- package/dist/server/RateLimit.d.ts.map +1 -0
- package/dist/server/RateLimit.js +101 -0
- package/dist/server/RateLimit.js.map +1 -0
- package/dist/server/Redis.d.ts +61 -0
- package/dist/server/Redis.d.ts.map +1 -0
- package/dist/server/Redis.js +226 -0
- package/dist/server/Redis.js.map +1 -0
- package/dist/server/Router.d.ts +27 -0
- package/dist/server/Router.d.ts.map +1 -0
- package/dist/server/Router.js +71 -0
- package/dist/server/Router.js.map +1 -0
- package/dist/server/Socket.d.ts +8 -0
- package/dist/server/Socket.d.ts.map +1 -0
- package/dist/server/Socket.js +278 -0
- package/dist/server/Socket.js.map +1 -0
- package/dist/server/SpendTracker.d.ts +18 -0
- package/dist/server/SpendTracker.d.ts.map +1 -0
- package/dist/server/SpendTracker.js +110 -0
- package/dist/server/SpendTracker.js.map +1 -0
- package/dist/server/Storage.d.ts +16 -0
- package/dist/server/Storage.d.ts.map +1 -0
- package/dist/server/Storage.js +95 -0
- package/dist/server/Storage.js.map +1 -0
- package/dist/server/TextGen.d.ts +5 -0
- package/dist/server/TextGen.d.ts.map +1 -0
- package/dist/server/TextGen.js +6 -0
- package/dist/server/TextGen.js.map +1 -0
- package/dist/server/WorkerQueue.d.ts +27 -0
- package/dist/server/WorkerQueue.d.ts.map +1 -0
- package/dist/server/WorkerQueue.js +147 -0
- package/dist/server/WorkerQueue.js.map +1 -0
- package/dist/server/ai/ImageGenClient.d.ts +6 -0
- package/dist/server/ai/ImageGenClient.d.ts.map +1 -0
- package/dist/server/ai/ImageGenClient.js +29 -0
- package/dist/server/ai/ImageGenClient.js.map +1 -0
- package/dist/server/ai/ProviderBalance.d.ts +15 -0
- package/dist/server/ai/ProviderBalance.d.ts.map +1 -0
- package/dist/server/ai/ProviderBalance.js +110 -0
- package/dist/server/ai/ProviderBalance.js.map +1 -0
- package/dist/server/ai/ProviderSelector.d.ts +13 -0
- package/dist/server/ai/ProviderSelector.d.ts.map +1 -0
- package/dist/server/ai/ProviderSelector.js +25 -0
- package/dist/server/ai/ProviderSelector.js.map +1 -0
- package/dist/server/ai/TextGenClient.d.ts +9 -0
- package/dist/server/ai/TextGenClient.d.ts.map +1 -0
- package/dist/server/ai/TextGenClient.js +72 -0
- package/dist/server/ai/TextGenClient.js.map +1 -0
- package/dist/server/ai/fallbacks.d.ts +9 -0
- package/dist/server/ai/fallbacks.d.ts.map +1 -0
- package/dist/server/ai/fallbacks.js +77 -0
- package/dist/server/ai/fallbacks.js.map +1 -0
- package/dist/server/ai/index.d.ts +12 -0
- package/dist/server/ai/index.d.ts.map +1 -0
- package/dist/server/ai/index.js +37 -0
- package/dist/server/ai/index.js.map +1 -0
- package/dist/server/ai/providers/Claude.d.ts +3 -0
- package/dist/server/ai/providers/Claude.d.ts.map +1 -0
- package/dist/server/ai/providers/Claude.js +124 -0
- package/dist/server/ai/providers/Claude.js.map +1 -0
- package/dist/server/ai/providers/FAL.d.ts +3 -0
- package/dist/server/ai/providers/FAL.d.ts.map +1 -0
- package/dist/server/ai/providers/FAL.js +24 -0
- package/dist/server/ai/providers/FAL.js.map +1 -0
- package/dist/server/ai/providers/Fireworks.d.ts +3 -0
- package/dist/server/ai/providers/Fireworks.d.ts.map +1 -0
- package/dist/server/ai/providers/Fireworks.js +79 -0
- package/dist/server/ai/providers/Fireworks.js.map +1 -0
- package/dist/server/ai/providers/Google.d.ts +3 -0
- package/dist/server/ai/providers/Google.d.ts.map +1 -0
- package/dist/server/ai/providers/Google.js +105 -0
- package/dist/server/ai/providers/Google.js.map +1 -0
- package/dist/server/ai/providers/GoogleImage.d.ts +3 -0
- package/dist/server/ai/providers/GoogleImage.d.ts.map +1 -0
- package/dist/server/ai/providers/GoogleImage.js +24 -0
- package/dist/server/ai/providers/GoogleImage.js.map +1 -0
- package/dist/server/ai/providers/Groq.d.ts +3 -0
- package/dist/server/ai/providers/Groq.d.ts.map +1 -0
- package/dist/server/ai/providers/Groq.js +99 -0
- package/dist/server/ai/providers/Groq.js.map +1 -0
- package/dist/server/ai/providers/Kie.d.ts +3 -0
- package/dist/server/ai/providers/Kie.d.ts.map +1 -0
- package/dist/server/ai/providers/Kie.js +79 -0
- package/dist/server/ai/providers/Kie.js.map +1 -0
- package/dist/server/ai/providers/KieImage.d.ts +3 -0
- package/dist/server/ai/providers/KieImage.d.ts.map +1 -0
- package/dist/server/ai/providers/KieImage.js +50 -0
- package/dist/server/ai/providers/KieImage.js.map +1 -0
- package/dist/server/ai/providers/OpenAIText.d.ts +3 -0
- package/dist/server/ai/providers/OpenAIText.d.ts.map +1 -0
- package/dist/server/ai/providers/OpenAIText.js +103 -0
- package/dist/server/ai/providers/OpenAIText.js.map +1 -0
- package/dist/server/ai/providers/Together.d.ts +3 -0
- package/dist/server/ai/providers/Together.d.ts.map +1 -0
- package/dist/server/ai/providers/Together.js +99 -0
- package/dist/server/ai/providers/Together.js.map +1 -0
- package/dist/server/ai/providers/TogetherImage.d.ts +3 -0
- package/dist/server/ai/providers/TogetherImage.d.ts.map +1 -0
- package/dist/server/ai/providers/TogetherImage.js +24 -0
- package/dist/server/ai/providers/TogetherImage.js.map +1 -0
- package/dist/server/ai/providers/Wavespeed.d.ts +3 -0
- package/dist/server/ai/providers/Wavespeed.d.ts.map +1 -0
- package/dist/server/ai/providers/Wavespeed.js +53 -0
- package/dist/server/ai/providers/Wavespeed.js.map +1 -0
- package/dist/server/ai/registry.d.ts +10 -0
- package/dist/server/ai/registry.d.ts.map +1 -0
- package/dist/server/ai/registry.js +26 -0
- package/dist/server/ai/registry.js.map +1 -0
- package/dist/server/ai/types.d.ts +67 -0
- package/dist/server/ai/types.d.ts.map +1 -0
- package/dist/server/ai/types.js +17 -0
- package/dist/server/ai/types.js.map +1 -0
- package/dist/server/audio/STTStream.d.ts +12 -0
- package/dist/server/audio/STTStream.d.ts.map +1 -0
- package/dist/server/audio/STTStream.js +59 -0
- package/dist/server/audio/STTStream.js.map +1 -0
- package/dist/server/audio/TTSStream.d.ts +16 -0
- package/dist/server/audio/TTSStream.d.ts.map +1 -0
- package/dist/server/audio/TTSStream.js +151 -0
- package/dist/server/audio/TTSStream.js.map +1 -0
- package/dist/server/audio/index.d.ts +5 -0
- package/dist/server/audio/index.d.ts.map +1 -0
- package/dist/server/audio/index.js +14 -0
- package/dist/server/audio/index.js.map +1 -0
- package/dist/server/audio/resample.d.ts +7 -0
- package/dist/server/audio/resample.d.ts.map +1 -0
- package/dist/server/audio/resample.js +17 -0
- package/dist/server/audio/resample.js.map +1 -0
- package/dist/server/audio/stt/GroqWhisper.d.ts +3 -0
- package/dist/server/audio/stt/GroqWhisper.d.ts.map +1 -0
- package/dist/server/audio/stt/GroqWhisper.js +91 -0
- package/dist/server/audio/stt/GroqWhisper.js.map +1 -0
- package/dist/server/audio/stt/registry.d.ts +5 -0
- package/dist/server/audio/stt/registry.d.ts.map +1 -0
- package/dist/server/audio/stt/registry.js +11 -0
- package/dist/server/audio/stt/registry.js.map +1 -0
- package/dist/server/audio/stt/types.d.ts +25 -0
- package/dist/server/audio/stt/types.d.ts.map +1 -0
- package/dist/server/audio/stt/types.js +2 -0
- package/dist/server/audio/stt/types.js.map +1 -0
- package/dist/server/audio/stt/vad.d.ts +18 -0
- package/dist/server/audio/stt/vad.d.ts.map +1 -0
- package/dist/server/audio/stt/vad.js +90 -0
- package/dist/server/audio/stt/vad.js.map +1 -0
- package/dist/server/audio/tts/InWorld.d.ts +3 -0
- package/dist/server/audio/tts/InWorld.d.ts.map +1 -0
- package/dist/server/audio/tts/InWorld.js +147 -0
- package/dist/server/audio/tts/InWorld.js.map +1 -0
- package/dist/server/audio/tts/providers/Azure.d.ts +3 -0
- package/dist/server/audio/tts/providers/Azure.d.ts.map +1 -0
- package/dist/server/audio/tts/providers/Azure.js +31 -0
- package/dist/server/audio/tts/providers/Azure.js.map +1 -0
- package/dist/server/audio/tts/registry.d.ts +6 -0
- package/dist/server/audio/tts/registry.d.ts.map +1 -0
- package/dist/server/audio/tts/registry.js +12 -0
- package/dist/server/audio/tts/registry.js.map +1 -0
- package/dist/server/audio/tts/types.d.ts +20 -0
- package/dist/server/audio/tts/types.d.ts.map +1 -0
- package/dist/server/audio/tts/types.js +2 -0
- package/dist/server/audio/tts/types.js.map +1 -0
- package/dist/server/billing/BillingGateway.d.ts +40 -0
- package/dist/server/billing/BillingGateway.d.ts.map +1 -0
- package/dist/server/billing/BillingGateway.js +116 -0
- package/dist/server/billing/BillingGateway.js.map +1 -0
- package/dist/server/billing/BillingLedger.d.ts +67 -0
- package/dist/server/billing/BillingLedger.d.ts.map +1 -0
- package/dist/server/billing/BillingLedger.js +214 -0
- package/dist/server/billing/BillingLedger.js.map +1 -0
- package/dist/server/billing/CreditStore.d.ts +58 -0
- package/dist/server/billing/CreditStore.d.ts.map +1 -0
- package/dist/server/billing/CreditStore.js +112 -0
- package/dist/server/billing/CreditStore.js.map +1 -0
- package/dist/server/billing/LimitEnforcer.d.ts +73 -0
- package/dist/server/billing/LimitEnforcer.d.ts.map +1 -0
- package/dist/server/billing/LimitEnforcer.js +276 -0
- package/dist/server/billing/LimitEnforcer.js.map +1 -0
- package/dist/server/billing/UserLimitCache.d.ts +35 -0
- package/dist/server/billing/UserLimitCache.d.ts.map +1 -0
- package/dist/server/billing/UserLimitCache.js +91 -0
- package/dist/server/billing/UserLimitCache.js.map +1 -0
- package/dist/server/billing/index.d.ts +6 -0
- package/dist/server/billing/index.d.ts.map +1 -0
- package/dist/server/billing/index.js +4 -0
- package/dist/server/billing/index.js.map +1 -0
- package/dist/server/billing/types.d.ts +95 -0
- package/dist/server/billing/types.d.ts.map +1 -0
- package/dist/server/billing/types.js +19 -0
- package/dist/server/billing/types.js.map +1 -0
- package/dist/server/embeddings/EmbeddingClient.d.ts +8 -0
- package/dist/server/embeddings/EmbeddingClient.d.ts.map +1 -0
- package/dist/server/embeddings/EmbeddingClient.js +30 -0
- package/dist/server/embeddings/EmbeddingClient.js.map +1 -0
- package/dist/server/embeddings/index.d.ts +5 -0
- package/dist/server/embeddings/index.d.ts.map +1 -0
- package/dist/server/embeddings/index.js +6 -0
- package/dist/server/embeddings/index.js.map +1 -0
- package/dist/server/embeddings/providers/OpenAI.d.ts +3 -0
- package/dist/server/embeddings/providers/OpenAI.d.ts.map +1 -0
- package/dist/server/embeddings/providers/OpenAI.js +28 -0
- package/dist/server/embeddings/providers/OpenAI.js.map +1 -0
- package/dist/server/embeddings/registry.d.ts +7 -0
- package/dist/server/embeddings/registry.d.ts.map +1 -0
- package/dist/server/embeddings/registry.js +15 -0
- package/dist/server/embeddings/registry.js.map +1 -0
- package/dist/server/embeddings/types.d.ts +12 -0
- package/dist/server/embeddings/types.d.ts.map +1 -0
- package/dist/server/embeddings/types.js +2 -0
- package/dist/server/embeddings/types.js.map +1 -0
- package/dist/server/index.d.ts +59 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +41 -0
- package/dist/server/index.js.map +1 -0
- package/dist/shared/Api.d.ts +40 -0
- package/dist/shared/Api.d.ts.map +1 -0
- package/dist/shared/Api.js +30 -0
- package/dist/shared/Api.js.map +1 -0
- package/dist/shared/Audio.d.ts +108 -0
- package/dist/shared/Audio.d.ts.map +1 -0
- package/dist/shared/Audio.js +3 -0
- package/dist/shared/Audio.js.map +1 -0
- package/dist/shared/DB.d.ts +339 -0
- package/dist/shared/DB.d.ts.map +1 -0
- package/dist/shared/DB.js +51 -0
- package/dist/shared/DB.js.map +1 -0
- package/dist/shared/Errors.d.ts +10 -0
- package/dist/shared/Errors.d.ts.map +1 -0
- package/dist/shared/Errors.js +19 -0
- package/dist/shared/Errors.js.map +1 -0
- package/dist/shared/FeedbackReport.d.ts +38 -0
- package/dist/shared/FeedbackReport.d.ts.map +1 -0
- package/dist/shared/FeedbackReport.js +14 -0
- package/dist/shared/FeedbackReport.js.map +1 -0
- package/dist/shared/Router.d.ts +28 -0
- package/dist/shared/Router.d.ts.map +1 -0
- package/dist/shared/Router.js +82 -0
- package/dist/shared/Router.js.map +1 -0
- package/dist/shared/Socket.d.ts +26 -0
- package/dist/shared/Socket.d.ts.map +1 -0
- package/dist/shared/Socket.js +2 -0
- package/dist/shared/Socket.js.map +1 -0
- package/dist/shared/index.d.ts +28 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/index.js +61 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/webrtc/index.d.ts +14 -0
- package/dist/webrtc/index.d.ts.map +1 -0
- package/dist/webrtc/index.js +24 -0
- package/dist/webrtc/index.js.map +1 -0
- package/package.json +97 -0
- package/templates/.claude/skills/assets.md +39 -0
- package/templates/.claude/skills/check-errors.md +35 -0
- package/templates/.claude/skills/check-feedback.md +23 -0
- package/templates/.claude/skills/check-perf.md +22 -0
- package/templates/.claude/skills/create-test-users.md +34 -0
- package/templates/.claude/skills/extend-api.md +37 -0
- package/templates/.claude/skills/fix-code.md +5 -0
- package/templates/.claude/skills/fix-errors.md +9 -0
- package/templates/.claude/skills/fix-feedback.md +8 -0
- package/templates/.claude/skills/fix-perf.md +8 -0
- package/templates/.claude/skills/uploads.md +38 -0
- package/templates/.claude/skills/use-ai.md +49 -0
- package/templates/.env.example +49 -0
- package/templates/.husky/pre-commit +2 -0
- package/templates/CLAUDE.md +199 -0
- package/templates/client/App.tsx +33 -0
- package/templates/client/index.html +14 -0
- package/templates/client/main.tsx +52 -0
- package/templates/client/pages/AuthDemoPage.tsx +85 -0
- package/templates/client/pages/HomePage.tsx +111 -0
- package/templates/client/pages/SearchPage.tsx +48 -0
- package/templates/client/pages/UserPage.tsx +33 -0
- package/templates/client/router.ts +4 -0
- package/templates/docker-compose.yml +50 -0
- package/templates/index.html +14 -0
- package/templates/package.json +56 -0
- package/templates/playwright.config.ts +21 -0
- package/templates/server/index.ts +24 -0
- package/templates/shared/api.ts +36 -0
- package/templates/shared/collections.ts +11 -0
- package/templates/shared/dbIndexes.ts +11 -0
- package/templates/shared/pages.ts +8 -0
- package/templates/vite.config.ts +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
# website-core
|
|
2
|
+
|
|
3
|
+
A full-stack TypeScript framework for building production-ready web applications. Provides an opinionated architecture combining an Express backend, React frontend, and MongoDB database with built-in authentication, real-time communication, storage, AI integration, and audio streaming.
|
|
4
|
+
|
|
5
|
+
## What's Included
|
|
6
|
+
|
|
7
|
+
- **Server**: Express with type-safe RPC routing and Zod schema validation
|
|
8
|
+
- **WebSockets**: Bidirectional socket server/client with RPC calls, document tracking, and file uploads
|
|
9
|
+
- **Database**: MongoDB with typed collections, cascade delete, indexes, migrations, and NATS-based real-time document tracking
|
|
10
|
+
- **Auth**: JWT + HttpOnly cookie sessions, OAuth (ugly.bot by default, extensible via `AuthProvider` interface)
|
|
11
|
+
- **Storage**: AWS S3 / Cloudflare R2 with presigned URLs and file promotion
|
|
12
|
+
- **AI**: 7 text generation providers (Together, Claude, OpenAI, Google, Groq, Fireworks, Kie) + 5 image providers (Together, FAL, Google, Kie, Wavespeed)
|
|
13
|
+
- **Audio**: Text-to-speech and speech-to-text streaming with React hooks (`useTTS`, `useSTT`), optional viseme data for lip sync, and word-level timestamps
|
|
14
|
+
- **Logging**: Multi-channel logging to MongoDB (error, console, perf, feedback) with server error capture, deduplication, and classification
|
|
15
|
+
- **Queues**: NATS JetStream worker queues, Redis pub/sub with in-memory fallback
|
|
16
|
+
- **Billing**: AI spend tracking with global/provider/per-user rolling limits (hourly/daily/weekly), threshold callbacks, profit margin markup, pre-paid user credits, full audit history, and 5-minute DB reconciliation
|
|
17
|
+
- **Email**: Transactional email via Mailgun with Handlebars template support
|
|
18
|
+
- **Rate Limiting**: Per-user/per-operation token-bucket limiting with queue management
|
|
19
|
+
- **Push Notifications**: Real-time delivery via WebSocket + Redis, with FCM support
|
|
20
|
+
- **Feedback**: Built-in user feedback collection with screenshot capture
|
|
21
|
+
- **CLI**: `web` command for dev, build, deploy, migrations, log queries, and auth utilities
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install website-core
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Peer dependencies:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install react react-dom html2canvas
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
### Scaffold a new project
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx web init my-app
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Start development
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
yarn dev
|
|
47
|
+
# Starts Docker services, Express server, Vite, TypeScript watcher, and ESLint concurrently
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
### Server
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { createApp, createUserHelper, registerFeedbackHandlers } from 'website-core';
|
|
56
|
+
import { dbDefaults } from 'website-core/shared';
|
|
57
|
+
import { functions, requests } from '../shared/api.js';
|
|
58
|
+
import { collections } from '../shared/collections.js';
|
|
59
|
+
import { pages } from '../shared/pages.js';
|
|
60
|
+
import type { User } from '../shared/types.js';
|
|
61
|
+
|
|
62
|
+
export const userHelper = createUserHelper(collections.user);
|
|
63
|
+
|
|
64
|
+
const app = createApp({ functions, requests }, collections, (config) => {
|
|
65
|
+
config.setPages({ pages });
|
|
66
|
+
|
|
67
|
+
config.setUserHelper(userHelper); // required
|
|
68
|
+
|
|
69
|
+
config.setOnUserCreate(async (userId, info, db) => { // required
|
|
70
|
+
await userHelper.set(db, {
|
|
71
|
+
...dbDefaults(),
|
|
72
|
+
id: userId,
|
|
73
|
+
email: info.email,
|
|
74
|
+
phone: info.phone,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Optional
|
|
79
|
+
// config.setAuth(myAuthProvider);
|
|
80
|
+
// config.setContextExtensions(async (base) => ({ nats: await connectNats() }));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
app.registerRequest('getMe', async (ctx) => {
|
|
84
|
+
const user = await userHelper.get(ctx.db, ctx.userId);
|
|
85
|
+
return { userId: ctx.userId, email: user?.email };
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
registerFeedbackHandlers(app, process.env.MAINTAIN_BOT_USER_ID ?? '');
|
|
89
|
+
|
|
90
|
+
const port = parseInt(process.env['PORT'] ?? '3000');
|
|
91
|
+
await app.start(port);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
> Both `setUserHelper` and `setOnUserCreate` are **required** — `createApp` throws at startup if either is missing.
|
|
95
|
+
|
|
96
|
+
### Shared types and API definitions
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { fn, req, defineFunctions, defineRequests } from 'website-core/shared';
|
|
100
|
+
import { z } from 'website-core/shared';
|
|
101
|
+
|
|
102
|
+
export const functions = defineFunctions({
|
|
103
|
+
greet: fn(
|
|
104
|
+
z.object({ name: z.string() }),
|
|
105
|
+
z.object({ message: z.string() })
|
|
106
|
+
),
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### React client
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { AppProvider, useApp, createRouter, createSocket } from 'website-core/client';
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Key APIs
|
|
117
|
+
|
|
118
|
+
### Server — `createApp()`
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
createApp(registry, appDefs, configure?)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- `registry` — `{ functions, requests }` from your shared API definitions
|
|
125
|
+
- `appDefs` — collection defs from `defineCollections()`
|
|
126
|
+
- `configure` — optional callback receiving an `AppConfigurator`:
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
interface AppConfigurator {
|
|
130
|
+
setUserHelper(helper: UserHelper<any>): void; // required
|
|
131
|
+
setOnUserCreate(handler: OnUserCreate): void; // required
|
|
132
|
+
setAuth(provider: AuthProvider): void;
|
|
133
|
+
setPages(options: PageServerOptions): void;
|
|
134
|
+
setOnSocketMessage(handler: (ws, userId, msg) => boolean): void;
|
|
135
|
+
setContextExtensions<E>(fn: (base: HandlerContext) => Promise<E>): void;
|
|
136
|
+
setWorkerQueue(queue: WorkerQueue): void;
|
|
137
|
+
registerRoutes(fn: (router: express.Router) => void): void;
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Registers handlers with full context (`userId`, `db`, `storage`, `textGen`, `imageGen`, `log`, `rateLimit`):
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
app.registerFunction('greet', async (ctx, input) => {
|
|
145
|
+
ctx.log.info('Greeting user', { userId: ctx.userId });
|
|
146
|
+
return { message: `Hello, ${input.name}` };
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
app.registerRequest('stream', async (ctx, input) => {
|
|
150
|
+
// streaming HTTP handler — ctx.res available for request handlers
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Built-in routes: `GET /health`, `POST /auth/verify`, `GET /auth/token`, `POST /auth/logout`, `GET /auth/url`, `POST /logs/client`, `GET /my_feedback`
|
|
155
|
+
|
|
156
|
+
### Auth — Cookie Sessions
|
|
157
|
+
|
|
158
|
+
After login, the server sets an `auth_token` HttpOnly cookie. On every page load the server refreshes it and injects the token into the HTML:
|
|
159
|
+
|
|
160
|
+
```html
|
|
161
|
+
<script>window.__AUTH_TOKEN__ = "eyJ..."</script>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The client reads `window.__AUTH_TOKEN__` synchronously — no localStorage, no extra HTTP round-trip. To log out:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
await fetch('/auth/logout', { method: 'POST' });
|
|
168
|
+
window.location.reload();
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Database — `TypedDB`
|
|
172
|
+
|
|
173
|
+
Define collections in `shared/collections.ts`. The `defineCollections` call injects the collection name into each def, making the def itself the collection reference:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { defineCollections } from 'website-core/shared';
|
|
177
|
+
|
|
178
|
+
export const collections = defineCollections({
|
|
179
|
+
notes: {
|
|
180
|
+
type: {} as Note,
|
|
181
|
+
meta: { cache: true, trackable: true, public: false, parent: null },
|
|
182
|
+
onDelete: async (doc, db) => { /* cleanup child docs */ },
|
|
183
|
+
},
|
|
184
|
+
comments: {
|
|
185
|
+
type: {} as Comment,
|
|
186
|
+
meta: { cache: false, trackable: true, public: false, parent: 'notes' },
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Inside handlers, use `ctx.db` — a `TypedDB` instance pre-created by `createApp`. All methods take the collection def as the first argument (not a string name):
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// Insert / replace
|
|
195
|
+
await ctx.db.setDoc(collections.notes, doc);
|
|
196
|
+
await ctx.db.setDoc(collections.notes, doc, { skipIfExists: true }); // INSERT only
|
|
197
|
+
|
|
198
|
+
// Partial updates (dot-notation, sets `updated` automatically)
|
|
199
|
+
await ctx.db.setDocFields(collections.notes, id, { title: 'New title' });
|
|
200
|
+
const doc = await ctx.db.setDocFieldsOrIgnore(collections.notes, id, { title }); // null if missing
|
|
201
|
+
const doc = await ctx.db.setDocFieldsOrCreate(collections.notes, id, fields, fullObj);
|
|
202
|
+
|
|
203
|
+
// MongoDB update operators ($inc, $addToSet, $pull, $unset, $set)
|
|
204
|
+
await ctx.db.setDocOp(collections.notes, id, { $inc: { viewCount: 1 } });
|
|
205
|
+
const doc = await ctx.db.setDocOpOrIgnore(collections.notes, id, { $addToSet: { tags: 'foo' } });
|
|
206
|
+
|
|
207
|
+
// Reads
|
|
208
|
+
const note = await ctx.db.getDoc(collections.notes, id);
|
|
209
|
+
const notes = await ctx.db.getDocs(collections.notes, { userId }, { sort: { created: -1 }, limit: 20, skip: 0 });
|
|
210
|
+
|
|
211
|
+
// Aggregate pipeline queries
|
|
212
|
+
const results = await ctx.db.getQuery<MyResult>('notes', pipeline, { skip, limit });
|
|
213
|
+
const count = await ctx.db.getQueryCount('notes', pipeline);
|
|
214
|
+
const raw = await ctx.db.getQueryRaw<RawDoc>('notes', pipeline); // no id remapping
|
|
215
|
+
|
|
216
|
+
// Delete with cascade
|
|
217
|
+
await ctx.db.deleteDoc(collections.notes, id);
|
|
218
|
+
await ctx.db.deleteQuery(collections.notes, { userId });
|
|
219
|
+
|
|
220
|
+
// Nested / child collections (parentId compound _id: "parentId:leafId")
|
|
221
|
+
await ctx.db.setDoc(collections.comments, comment, { parentId: noteId });
|
|
222
|
+
await ctx.db.getDocs(collections.comments, {}, { parentId: noteId });
|
|
223
|
+
await ctx.db.deleteDoc(collections.comments, commentId, { parentId: noteId });
|
|
224
|
+
|
|
225
|
+
// Direct cache access
|
|
226
|
+
const key = ctx.db.cacheKey('notes', id);
|
|
227
|
+
ctx.db.cacheSet(key, value, 60_000);
|
|
228
|
+
const cached = ctx.db.cacheGet<Note>(key);
|
|
229
|
+
ctx.db.cacheDelete(key);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Per-collection `cache: true` enables automatic LRU caching on `getDoc` / `setDoc` / `deleteDoc`. Use `parent: 'collectionName'` to define parent–child relationships for compound `_id`s and cascade deletes.
|
|
233
|
+
|
|
234
|
+
### WebSocket Client
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
const socket = createSocket({ functions, requests });
|
|
238
|
+
|
|
239
|
+
await socket.connect(token); // must call first
|
|
240
|
+
await socket.call('greet', { name: 'world' }); // function RPC
|
|
241
|
+
await socket.request('getMe', {}); // request RPC
|
|
242
|
+
const doc = await socket.getDoc('notes', id); // fetch document
|
|
243
|
+
socket.trackDoc('notes', id, (doc) => { /* live */ }); // live updates
|
|
244
|
+
await socket.uploadFile(file, key); // file upload (key is a string)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`trackDocs` accepts optional `sort`, `limit`, and `skip` parameters for server-side ordering and pagination:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
// Live-updating query: most recent 20 notes, newest first
|
|
251
|
+
socket.trackDocs('notes', { userId }, (docs) => { /* live */ }, {
|
|
252
|
+
sort: { created: -1 },
|
|
253
|
+
limit: 20,
|
|
254
|
+
skip: 0,
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`getDocs` on the server also accepts the same options:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
const docs = await ctx.db.getDocs(collections.notes, { userId }, {
|
|
262
|
+
sort: { created: -1 },
|
|
263
|
+
limit: 20,
|
|
264
|
+
skip: 0,
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### AI — Text Generation
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
const text = await ctx.textGen.generate(messages, { model });
|
|
272
|
+
const json = await ctx.textGen.generateJson(schema, messages, { model });
|
|
273
|
+
const result = await ctx.textGen.generateWithTools(messages, tools, { model });
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Supported providers: `together`, `claude`, `openai`, `google`, `groq`, `fireworks`, `kie`
|
|
277
|
+
|
|
278
|
+
### AI — Image Generation
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
const url = await ctx.imageGen.generate(prompt, { width: 1024, height: 1024 });
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Supported providers: `together`, `fal`, `google`, `kie`, `wavespeed`
|
|
285
|
+
|
|
286
|
+
### Audio — TTS/STT React Hooks
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
// Text-to-speech
|
|
290
|
+
const { playing, currentWord, play, stop } = useTTS(socket);
|
|
291
|
+
await play('Hello world');
|
|
292
|
+
|
|
293
|
+
// With viseme data for lip sync (opt-in — adds latency)
|
|
294
|
+
await play('Hello world', { requestVisemes: true });
|
|
295
|
+
// viseme events arrive via onViseme callback in useTTS options
|
|
296
|
+
|
|
297
|
+
// Speech-to-text
|
|
298
|
+
const { transcript, isFinal, listening, start, stop } = useSTT(socket, {
|
|
299
|
+
mode: 'realtime', // 'realtime' | 'batch' | 'auto'
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// With word-level timestamps (opt-in — uses Groq verbose_json, slower)
|
|
303
|
+
const { transcript, words } = useSTT(socket, {
|
|
304
|
+
mode: 'batch',
|
|
305
|
+
requestWords: true,
|
|
306
|
+
});
|
|
307
|
+
// words: STTWord[] — each has { word, startMs, durationMs }
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Viseme and word types are exported from `website-core/shared`:
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
import type { TTSViseme, TTSVisemeName, STTWord } from 'website-core/shared';
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Email
|
|
317
|
+
|
|
318
|
+
Send transactional email via Mailgun:
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
import { sendEmail, loadEmailTemplate, sendTemplateEmail } from 'website-core';
|
|
322
|
+
|
|
323
|
+
// One-off HTML email
|
|
324
|
+
await sendEmail({
|
|
325
|
+
to: 'user@example.com',
|
|
326
|
+
subject: 'Welcome!',
|
|
327
|
+
html: '<p>Thanks for signing up.</p>',
|
|
328
|
+
from: 'noreply@my-app.com', // optional — defaults to MAILGUN_FROM env var
|
|
329
|
+
replyTo: 'support@my-app.com',
|
|
330
|
+
headers: { 'X-Campaign-Id': 'welcome' },
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Handlebars template email
|
|
334
|
+
interface WelcomeData { name: string; confirmUrl: string }
|
|
335
|
+
const welcomeTemplate = loadEmailTemplate<WelcomeData>('/path/to/welcome.hbs');
|
|
336
|
+
|
|
337
|
+
await sendTemplateEmail('user@example.com', 'Confirm your email', welcomeTemplate, {
|
|
338
|
+
name: 'Alice',
|
|
339
|
+
confirmUrl: 'https://my-app.com/confirm?token=abc',
|
|
340
|
+
});
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Required environment variables: `MAILGUN_API_KEY`, `MAILGUN_DOMAIN`, `MAILGUN_FROM`. If `MAILGUN_API_KEY` is not set, sending is silently skipped (dev-friendly).
|
|
344
|
+
|
|
345
|
+
### Billing
|
|
346
|
+
|
|
347
|
+
Track AI spend and enforce rate limits across all cost types (`textGen`, `imageGen`, `stt`, `tts`, `embedding`, `service`):
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
import {
|
|
351
|
+
initBillingGateway,
|
|
352
|
+
getBillingGateway,
|
|
353
|
+
BillingLimitError,
|
|
354
|
+
BillingValidationError,
|
|
355
|
+
} from 'website-core';
|
|
356
|
+
|
|
357
|
+
// Initialize once at startup (before serving traffic)
|
|
358
|
+
const billing = initBillingGateway(db, {
|
|
359
|
+
global: {
|
|
360
|
+
hourlyUsd: 50, // hard cap across all providers/users
|
|
361
|
+
thresholdPct: 0.8, // fire callback at 80%
|
|
362
|
+
},
|
|
363
|
+
providers: {
|
|
364
|
+
openai: { hourlyUsd: 20, thresholdPct: 0.9 },
|
|
365
|
+
anthropic: { hourlyUsd: 30 },
|
|
366
|
+
},
|
|
367
|
+
profitMarginPct: 0.5, // optional: 50% markup applied before limit checks
|
|
368
|
+
reconcileIntervalMs: 300_000, // flush to DB every 5 min (default)
|
|
369
|
+
userLimitCacheSize: 10_000, // LRU entries (default)
|
|
370
|
+
}, creditDb); // optional CreditDB for pre-paid credits
|
|
371
|
+
|
|
372
|
+
// Supply per-user limits from your database
|
|
373
|
+
billing.setUserLimitHook(async (userId) => {
|
|
374
|
+
const plan = await db.getDoc(collections.plans, userId);
|
|
375
|
+
return plan
|
|
376
|
+
? { hourlyUsd: plan.hourlyUsd, dailyUsd: plan.dailyUsd, weeklyUsd: plan.weeklyUsd,
|
|
377
|
+
thresholds: { daily: 0.9 } }
|
|
378
|
+
: null; // null = no limits
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Alert callbacks (errors swallowed — never block a charge)
|
|
382
|
+
billing.setGlobalThresholdCallback(({ limitType, spent, limit, thresholdPct, provider }) => {
|
|
383
|
+
console.warn(`[Billing] ${limitType} at ${Math.round(thresholdPct * 100)}%: $${spent}/$${limit}`);
|
|
384
|
+
});
|
|
385
|
+
billing.setUserThresholdCallback(({ userId, limitType, spent, limit }) => {
|
|
386
|
+
void notifyUser(userId, `You've used ${Math.round(spent / limit * 100)}% of your ${limitType} budget`);
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**Recording a charge:**
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
// Returns BillingRecord on success; throws on limit violation
|
|
394
|
+
try {
|
|
395
|
+
const record = await getBillingGateway().charge({
|
|
396
|
+
userId, // optional — omit for anonymous charges
|
|
397
|
+
provider: 'openai',
|
|
398
|
+
model: 'gpt-4o',
|
|
399
|
+
type: 'textGen', // 'textGen' | 'imageGen' | 'stt' | 'tts' | 'embedding' | 'service'
|
|
400
|
+
costUsd: 0.012,
|
|
401
|
+
inputTokens: 800,
|
|
402
|
+
outputTokens: 400,
|
|
403
|
+
});
|
|
404
|
+
} catch (err) {
|
|
405
|
+
if (err instanceof BillingLimitError) {
|
|
406
|
+
// err.limitType: 'global-hourly' | 'provider-hourly' | 'user-hourly' | 'user-daily' | 'user-weekly'
|
|
407
|
+
// err.spent, err.limit
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**Querying spend and history:**
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
const billing = getBillingGateway();
|
|
416
|
+
|
|
417
|
+
// Rolling window totals
|
|
418
|
+
const globalHour = await billing.getWindowSpend(3_600_000);
|
|
419
|
+
const userDay = await billing.getWindowSpend(86_400_000, { userId });
|
|
420
|
+
const openaiHour = await billing.getWindowSpend(3_600_000, { provider: 'openai' });
|
|
421
|
+
|
|
422
|
+
// Audit history (default limit 100, max 1000)
|
|
423
|
+
const records = await billing.getHistory({ userId, limit: 50, skip: 0 });
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**Cache invalidation** (call after a user's plan changes):
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
billing.invalidateUserLimit(userId);
|
|
430
|
+
billing.invalidateAllUserLimits();
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**Pre-paid credits** (optional — requires `creditDb` passed to `initBillingGateway`):
|
|
434
|
+
|
|
435
|
+
When a user would otherwise be blocked by a rate limit, the gateway automatically deducts from their credit balance as a fallback. Global and provider limits are never bypassed.
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
import { CreditStore, MongoCreditDB } from 'website-core';
|
|
439
|
+
|
|
440
|
+
// Set up the credit DB (uses your TypedDB instance)
|
|
441
|
+
const creditDb = new MongoCreditDB(db.userCredits);
|
|
442
|
+
const billing = initBillingGateway(ledgerDb, config, creditDb);
|
|
443
|
+
|
|
444
|
+
// Grant credits (e.g. after a purchase)
|
|
445
|
+
await billing.grantCredit(userId, 5.00); // $5.00
|
|
446
|
+
|
|
447
|
+
// Check balance
|
|
448
|
+
const balance = await billing.getCreditBalance(userId);
|
|
449
|
+
|
|
450
|
+
// Invalidate cached balance (e.g. after external grant)
|
|
451
|
+
billing.invalidateCreditCache(userId);
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Charges are recorded to an `aiSpend` MongoDB collection and reconciled in-memory every 5 minutes with a forced flush on `shutdown()`. Each text and image generation call from `createTextGen`/`createImageGen` is automatically charged through the gateway when initialized.
|
|
455
|
+
|
|
456
|
+
### Rate Limiting
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
const rateLimit = createRateLimiter({
|
|
460
|
+
windowSeconds: 3600, // 1 hour (default)
|
|
461
|
+
maxPerWindow: 1.0, // max units per window (default)
|
|
462
|
+
maxQueueDepth: 100, // max queued requests (default)
|
|
463
|
+
maxWaitMs: 300_000, // max wait time in queue (default)
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
await rateLimit.check(userId, 'generate', 0.1);
|
|
467
|
+
await rateLimit.charge(userId, 'generate', actualCost);
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Worker Queues
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
const queue = createWorkerQueue({
|
|
474
|
+
streamName: 'JOBS', // NATS stream name (default: 'JOBS')
|
|
475
|
+
clockServerOnly: true, // only process when IS_CLOCK_SERVER=true (default: true)
|
|
476
|
+
concurrency: 10, // max concurrent jobs (default: 10)
|
|
477
|
+
maxRetries: 3, // max retry attempts (default: 3)
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
await queue.enqueue('email', { to: 'user@example.com' });
|
|
481
|
+
await queue.enqueue('email', payload, { delay: 5000 }); // delay in ms
|
|
482
|
+
queue.registerHandler('email', async (job) => { /* job.payload, job.working() */ });
|
|
483
|
+
await queue.start();
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Storage
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
const storage = createStorageClient();
|
|
490
|
+
|
|
491
|
+
// Server-side upload
|
|
492
|
+
const url = await storage.put('temp', key, buffer, 'image/png');
|
|
493
|
+
const publicUrl = await storage.moveToPublic(tempKey, destKey);
|
|
494
|
+
const url = storage.url('public', destKey);
|
|
495
|
+
|
|
496
|
+
// Presigned URL for browser direct upload
|
|
497
|
+
const { uploadUrl, resultUrl } = await storage.presignedPut('temp', key);
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Logging
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
// In handlers, use ctx.log
|
|
504
|
+
ctx.log.info('User signed in', { userId });
|
|
505
|
+
ctx.log.warn('Unexpected state', { context });
|
|
506
|
+
ctx.log.error('Something failed', { error: err.message });
|
|
507
|
+
|
|
508
|
+
// Performance timing — returns a stop function
|
|
509
|
+
const stop = ctx.log.perf('db-query');
|
|
510
|
+
// ... do work ...
|
|
511
|
+
stop(); // records duration to MongoDB
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Server Error Capture
|
|
515
|
+
|
|
516
|
+
`captureServerError` persists unexpected errors to a MongoDB `errorLog` collection with automatic deduplication (repeated errors increment a `count` field instead of inserting duplicates). Expected/recoverable errors can be suppressed from persistence by registering patterns.
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
import {
|
|
520
|
+
captureServerError,
|
|
521
|
+
registerExpectedErrorPattern,
|
|
522
|
+
classifyError,
|
|
523
|
+
} from 'website-core';
|
|
524
|
+
|
|
525
|
+
// Register known-recoverable patterns at startup — matched errors log to
|
|
526
|
+
// console only and are never written to MongoDB
|
|
527
|
+
registerExpectedErrorPattern('ECONNRESET')
|
|
528
|
+
registerExpectedErrorPattern('[STT] Session ended before audio was received')
|
|
529
|
+
|
|
530
|
+
// Capture an error in a catch block
|
|
531
|
+
try {
|
|
532
|
+
await riskyOperation()
|
|
533
|
+
} catch (err) {
|
|
534
|
+
captureServerError('[MyService] Failed to process request', err, { userId })
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Classify a message manually ('expected' | 'unexpected')
|
|
538
|
+
const classification = classifyError('[STT] Session ended before audio was received')
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### Push Notifications
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
import { sendPush } from 'website-core';
|
|
545
|
+
|
|
546
|
+
await sendPush(userId, { title: 'New message', body: 'You have a reply' });
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
## CLI Commands
|
|
550
|
+
|
|
551
|
+
| Command | Description |
|
|
552
|
+
|---|---|
|
|
553
|
+
| `web init <name>` | Scaffold a new project |
|
|
554
|
+
| `web dev` | Start all dev services (Docker, server, Vite, TS, ESLint) |
|
|
555
|
+
| `web build` | Production build |
|
|
556
|
+
| `web db:init` | Create/update MongoDB indexes |
|
|
557
|
+
| `web db:migrate` | Run pending database migrations (`--status` to preview) |
|
|
558
|
+
| `web publish:assets` | Deploy static assets to CDN (`--dry-run` to preview) |
|
|
559
|
+
| `web purge:assets` | Remove old build artifacts (keeps last 3 by default) |
|
|
560
|
+
| `web logs:local` | Query local dev logs |
|
|
561
|
+
| `web logs:server` | Query server logs from MongoDB |
|
|
562
|
+
| `web error:local` | Query local error logs |
|
|
563
|
+
| `web error:server` | Query server error logs from MongoDB |
|
|
564
|
+
| `web perf:local` | Query local performance metrics |
|
|
565
|
+
| `web perf:server` | Query server performance metrics from MongoDB |
|
|
566
|
+
| `web feedback` | Query user feedback submissions |
|
|
567
|
+
| `web auth:create-account` | Create an account directly in the database |
|
|
568
|
+
| `web auth:create-token` | Generate a JWT for a userId |
|
|
569
|
+
| `web test:e2e` | Run Playwright end-to-end tests (`--headed` for browser UI) |
|
|
570
|
+
|
|
571
|
+
## React Components
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
import {
|
|
575
|
+
Button, Card, Text, Input, Modal, Toast, PageLayout,
|
|
576
|
+
FeedbackButton,
|
|
577
|
+
} from 'website-core/client';
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
The `FeedbackButton` captures a screenshot and submits it with user context automatically.
|
|
581
|
+
|
|
582
|
+
## Routing (Client)
|
|
583
|
+
|
|
584
|
+
```typescript
|
|
585
|
+
const { useRouter, RouterProvider, RouterView } = createRouter(pages);
|
|
586
|
+
|
|
587
|
+
function App() {
|
|
588
|
+
const { push, replace, back } = useRouter();
|
|
589
|
+
return (
|
|
590
|
+
<RouterProvider fallback={<NotFound />} loginFallback={<Login />} isAuthenticated={() => !!userId}>
|
|
591
|
+
<RouterView renderPage={(state) => <PageSwitch route={state.name} params={state.params} />} />
|
|
592
|
+
</RouterProvider>
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
Page transitions are animated by default using `ViewFlipper`. The transition type (`PUSH`, `POP`, `REPLACE`, `NONE`) is inferred automatically from navigation calls.
|
|
598
|
+
|
|
599
|
+
### Popups
|
|
600
|
+
|
|
601
|
+
```typescript
|
|
602
|
+
import { useRouter } from './router';
|
|
603
|
+
|
|
604
|
+
function MyComponent() {
|
|
605
|
+
const { openPopup } = useRouter();
|
|
606
|
+
|
|
607
|
+
function openMenu() {
|
|
608
|
+
const handle = openPopup(<MyMenu />, {
|
|
609
|
+
mode: 'contextMenu', // 'block' | 'transient' | 'contextMenu'
|
|
610
|
+
slideFrom: 'bottom', // 'left' | 'right' | 'top' | 'bottom' | 'none'
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// Dismiss programmatically
|
|
614
|
+
handle.hide();
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
- **`block`** — modal overlay, blocks interaction behind it
|
|
620
|
+
- **`transient`** — tapping the backdrop closes it
|
|
621
|
+
- **`contextMenu`** — same as transient but typically for menus/pickers
|
|
622
|
+
|
|
623
|
+
### Animation Primitives
|
|
624
|
+
|
|
625
|
+
```typescript
|
|
626
|
+
import {
|
|
627
|
+
createAnimatedValue,
|
|
628
|
+
useAnimatedValue,
|
|
629
|
+
Animated,
|
|
630
|
+
easingFunctions,
|
|
631
|
+
} from 'website-core/client';
|
|
632
|
+
import type { EasingFunction } from 'website-core/client';
|
|
633
|
+
|
|
634
|
+
// Imperative animated value (RAF-driven, no React re-renders)
|
|
635
|
+
const spring = createAnimatedValue(0);
|
|
636
|
+
spring.animateTo(1, { duration: 300, easing: easingFunctions.easeInOut });
|
|
637
|
+
|
|
638
|
+
// React hook version
|
|
639
|
+
const opacity = useAnimatedValue(0);
|
|
640
|
+
|
|
641
|
+
// Apply to DOM via ref — zero re-renders
|
|
642
|
+
<Animated value={spring} style={(v) => ({ opacity: v, transform: `scale(${v})` })}>
|
|
643
|
+
<div>Content</div>
|
|
644
|
+
</Animated>
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
## Project Structure
|
|
648
|
+
|
|
649
|
+
Projects built with `website-core` follow this layout:
|
|
650
|
+
|
|
651
|
+
```
|
|
652
|
+
my-app/
|
|
653
|
+
├── src/
|
|
654
|
+
│ ├── server/ # Express handlers, DB collections, migrations
|
|
655
|
+
│ ├── client/ # React pages and components
|
|
656
|
+
│ └── shared/ # API definitions and shared types
|
|
657
|
+
├── static/ # Build-time static assets
|
|
658
|
+
└── docker-compose.yml
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
## Environment Variables
|
|
662
|
+
|
|
663
|
+
| Variable | Description |
|
|
664
|
+
|---|---|
|
|
665
|
+
| `MONGODB_URI` | MongoDB connection string |
|
|
666
|
+
| `JWT_SECRET` | Secret for signing JWT tokens |
|
|
667
|
+
| `REDIS_URL` | Redis connection (optional, uses in-memory fallback) |
|
|
668
|
+
| `NATS_URL` | NATS server URL |
|
|
669
|
+
| `NATS_CREDS` | NATS credentials file path (Synadia Cloud) |
|
|
670
|
+
| `STORAGE_ACCOUNT_ID` | Cloudflare R2 account ID (alias: `CLOUDFLARE_ACCOUNT_ID`) |
|
|
671
|
+
| `STORAGE_ACCESS_KEY_ID` | R2 access key (alias: `CLOUDFLARE_R2_ACCESS_KEY_ID`) |
|
|
672
|
+
| `STORAGE_SECRET_ACCESS_KEY` | R2 secret key (alias: `CLOUDFLARE_R2_SECRET_ACCESS_KEY`) |
|
|
673
|
+
| `STORAGE_PUBLIC_BUCKET` | R2 public bucket name (alias: `CLOUDFLARE_R2_PUBLIC_BUCKET`) |
|
|
674
|
+
| `STORAGE_TEMP_BUCKET` | R2 temp bucket name (alias: `CLOUDFLARE_R2_TEMP_BUCKET`) |
|
|
675
|
+
| `STORAGE_PUBLIC_URL` | Base URL for public bucket (alias: `CLOUDFLARE_R2_PUBLIC_URL`) |
|
|
676
|
+
| `STORAGE_TEMP_URL` | Base URL for temp bucket (alias: `CLOUDFLARE_R2_TEMP_URL`) |
|
|
677
|
+
| `TOGETHER_API_KEY` | Together AI API key |
|
|
678
|
+
| `ANTHROPIC_API_KEY` | Anthropic Claude API key |
|
|
679
|
+
| `OPENAI_API_KEY` | OpenAI API key |
|
|
680
|
+
| `GOOGLE_API_KEY` | Google Gemini API key |
|
|
681
|
+
| `IS_CLOCK_SERVER` | Set to `true` on the instance that processes worker queues |
|
|
682
|
+
| `MAILGUN_API_KEY` | Mailgun API key (`key-...`) |
|
|
683
|
+
| `MAILGUN_DOMAIN` | Mailgun sending domain (e.g., `mg.my-app.com`) |
|
|
684
|
+
| `MAILGUN_FROM` | Default from address (e.g., `noreply@my-app.com`) |
|
|
685
|
+
|
|
686
|
+
Client-side variables must be prefixed with `VITE_`.
|
|
687
|
+
|
|
688
|
+
## Tech Stack
|
|
689
|
+
|
|
690
|
+
- **Runtime**: Node.js, TypeScript (ES2022, NodeNext modules)
|
|
691
|
+
- **Server**: Express, ws (WebSockets)
|
|
692
|
+
- **Frontend**: React 19, Vite, Tailwind CSS
|
|
693
|
+
- **Database**: MongoDB
|
|
694
|
+
- **Messaging**: NATS with JetStream
|
|
695
|
+
- **Cache**: Redis (in-memory fallback for dev)
|
|
696
|
+
- **Storage**: AWS S3 / Cloudflare R2
|
|
697
|
+
- **Auth**: JWT (jose), OAuth
|
|
698
|
+
- **AI**: Together AI, Anthropic, OpenAI, Google, Groq, FAL
|
|
699
|
+
- **Audio**: InWorld TTS, Groq Whisper STT
|
|
700
|
+
- **Validation**: Zod
|
|
701
|
+
- **Testing**: Vitest, Playwright, mongodb-memory-server
|
|
702
|
+
|
|
703
|
+
## Development
|
|
704
|
+
|
|
705
|
+
```bash
|
|
706
|
+
npm run build # Compile TypeScript
|
|
707
|
+
npm test # Run unit tests
|
|
708
|
+
npm run test:watch # Watch mode
|
|
709
|
+
```
|