@soederpop/luca 0.0.2
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.md +71 -0
- package/README.md +78 -0
- package/bun.lock +2928 -0
- package/bunfig.toml +3 -0
- package/commands/audit-docs.ts +740 -0
- package/commands/build-scaffolds.ts +154 -0
- package/commands/generate-api-docs.ts +114 -0
- package/commands/update-introspection.ts +67 -0
- package/docs/CLI.md +335 -0
- package/docs/README.md +88 -0
- package/docs/TABLE-OF-CONTENTS.md +157 -0
- package/docs/apis/clients/elevenlabs.md +84 -0
- package/docs/apis/clients/graph.md +56 -0
- package/docs/apis/clients/openai.md +69 -0
- package/docs/apis/clients/rest.md +41 -0
- package/docs/apis/clients/websocket.md +107 -0
- package/docs/apis/features/agi/assistant.md +471 -0
- package/docs/apis/features/agi/assistants-manager.md +154 -0
- package/docs/apis/features/agi/claude-code.md +602 -0
- package/docs/apis/features/agi/conversation-history.md +352 -0
- package/docs/apis/features/agi/conversation.md +333 -0
- package/docs/apis/features/agi/docs-reader.md +121 -0
- package/docs/apis/features/agi/openai-codex.md +318 -0
- package/docs/apis/features/agi/openapi.md +138 -0
- package/docs/apis/features/agi/semantic-search.md +387 -0
- package/docs/apis/features/agi/skills-library.md +216 -0
- package/docs/apis/features/node/container-link.md +133 -0
- package/docs/apis/features/node/content-db.md +313 -0
- package/docs/apis/features/node/disk-cache.md +379 -0
- package/docs/apis/features/node/dns.md +651 -0
- package/docs/apis/features/node/docker.md +705 -0
- package/docs/apis/features/node/downloader.md +81 -0
- package/docs/apis/features/node/esbuild.md +59 -0
- package/docs/apis/features/node/file-manager.md +182 -0
- package/docs/apis/features/node/fs.md +581 -0
- package/docs/apis/features/node/git.md +330 -0
- package/docs/apis/features/node/google-auth.md +174 -0
- package/docs/apis/features/node/google-calendar.md +187 -0
- package/docs/apis/features/node/google-docs.md +151 -0
- package/docs/apis/features/node/google-drive.md +225 -0
- package/docs/apis/features/node/google-sheets.md +179 -0
- package/docs/apis/features/node/grep.md +290 -0
- package/docs/apis/features/node/helpers.md +135 -0
- package/docs/apis/features/node/ink.md +334 -0
- package/docs/apis/features/node/ipc-socket.md +260 -0
- package/docs/apis/features/node/json-tree.md +86 -0
- package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
- package/docs/apis/features/node/networking.md +281 -0
- package/docs/apis/features/node/nlp.md +133 -0
- package/docs/apis/features/node/opener.md +97 -0
- package/docs/apis/features/node/os.md +118 -0
- package/docs/apis/features/node/package-finder.md +402 -0
- package/docs/apis/features/node/postgres.md +212 -0
- package/docs/apis/features/node/proc.md +430 -0
- package/docs/apis/features/node/process-manager.md +210 -0
- package/docs/apis/features/node/python.md +278 -0
- package/docs/apis/features/node/repl.md +88 -0
- package/docs/apis/features/node/runpod.md +673 -0
- package/docs/apis/features/node/secure-shell.md +169 -0
- package/docs/apis/features/node/semantic-search.md +401 -0
- package/docs/apis/features/node/sqlite.md +211 -0
- package/docs/apis/features/node/telegram.md +254 -0
- package/docs/apis/features/node/tts.md +118 -0
- package/docs/apis/features/node/ui.md +703 -0
- package/docs/apis/features/node/vault.md +64 -0
- package/docs/apis/features/node/vm.md +84 -0
- package/docs/apis/features/node/window-manager.md +337 -0
- package/docs/apis/features/node/yaml-tree.md +85 -0
- package/docs/apis/features/node/yaml.md +176 -0
- package/docs/apis/features/web/asset-loader.md +47 -0
- package/docs/apis/features/web/container-link.md +133 -0
- package/docs/apis/features/web/esbuild.md +59 -0
- package/docs/apis/features/web/helpers.md +135 -0
- package/docs/apis/features/web/network.md +30 -0
- package/docs/apis/features/web/speech.md +55 -0
- package/docs/apis/features/web/vault.md +64 -0
- package/docs/apis/features/web/vm.md +84 -0
- package/docs/apis/features/web/voice.md +67 -0
- package/docs/apis/servers/express.md +127 -0
- package/docs/apis/servers/mcp.md +213 -0
- package/docs/apis/servers/websocket.md +99 -0
- package/docs/documentation-audit.md +134 -0
- package/docs/examples/content-db.md +77 -0
- package/docs/examples/disk-cache.md +83 -0
- package/docs/examples/docker.md +101 -0
- package/docs/examples/downloader.md +70 -0
- package/docs/examples/esbuild.md +80 -0
- package/docs/examples/file-manager.md +82 -0
- package/docs/examples/fs.md +83 -0
- package/docs/examples/git.md +85 -0
- package/docs/examples/google-auth.md +88 -0
- package/docs/examples/google-calendar.md +94 -0
- package/docs/examples/google-docs.md +82 -0
- package/docs/examples/google-drive.md +96 -0
- package/docs/examples/google-sheets.md +95 -0
- package/docs/examples/grep.md +85 -0
- package/docs/examples/ink-blocks.md +75 -0
- package/docs/examples/ink-renderer.md +41 -0
- package/docs/examples/ink.md +103 -0
- package/docs/examples/ipc-socket.md +103 -0
- package/docs/examples/json-tree.md +91 -0
- package/docs/examples/launcher-app-command-listener.md +120 -0
- package/docs/examples/networking.md +58 -0
- package/docs/examples/nlp.md +91 -0
- package/docs/examples/opener.md +78 -0
- package/docs/examples/os.md +72 -0
- package/docs/examples/package-finder.md +89 -0
- package/docs/examples/port-exposer.md +89 -0
- package/docs/examples/postgres.md +91 -0
- package/docs/examples/proc.md +81 -0
- package/docs/examples/process-manager.md +79 -0
- package/docs/examples/python.md +91 -0
- package/docs/examples/repl.md +93 -0
- package/docs/examples/runpod.md +119 -0
- package/docs/examples/secure-shell.md +92 -0
- package/docs/examples/sqlite.md +86 -0
- package/docs/examples/telegram.md +77 -0
- package/docs/examples/tts.md +86 -0
- package/docs/examples/ui.md +80 -0
- package/docs/examples/vault.md +70 -0
- package/docs/examples/vm.md +86 -0
- package/docs/examples/window-manager.md +125 -0
- package/docs/examples/yaml-tree.md +93 -0
- package/docs/examples/yaml.md +104 -0
- package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
- package/docs/ideas/container-use-api.md +9 -0
- package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
- package/docs/ideas/feature-stacks.md +22 -0
- package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
- package/docs/ideas/mcp-design.md +9 -0
- package/docs/ideas/web-container-debugging-feature.md +13 -0
- package/docs/introspection-audit.md +49 -0
- package/docs/introspection.md +154 -0
- package/docs/mcp/readme.md +162 -0
- package/docs/models.ts +38 -0
- package/docs/philosophy.md +85 -0
- package/docs/principles.md +7 -0
- package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
- package/docs/prompts/mcp-test-easy-command.md +27 -0
- package/docs/reports/assistant-bugs.md +38 -0
- package/docs/reports/attach-pattern-usage.md +18 -0
- package/docs/reports/code-audit-results.md +391 -0
- package/docs/reports/introspection-audit-tasks.md +378 -0
- package/docs/reports/luca-mcp-improvements.md +128 -0
- package/docs/scaffolds/client.md +140 -0
- package/docs/scaffolds/command.md +106 -0
- package/docs/scaffolds/endpoint.md +176 -0
- package/docs/scaffolds/feature.md +148 -0
- package/docs/scaffolds/server.md +187 -0
- package/docs/tasks/web-container-helper-discovery.md +71 -0
- package/docs/todos.md +1 -0
- package/docs/tutorials/01-getting-started.md +106 -0
- package/docs/tutorials/02-container.md +210 -0
- package/docs/tutorials/03-scripts.md +194 -0
- package/docs/tutorials/04-features-overview.md +196 -0
- package/docs/tutorials/05-state-and-events.md +171 -0
- package/docs/tutorials/06-servers.md +157 -0
- package/docs/tutorials/07-endpoints.md +198 -0
- package/docs/tutorials/08-commands.md +171 -0
- package/docs/tutorials/09-clients.md +162 -0
- package/docs/tutorials/10-creating-features.md +198 -0
- package/docs/tutorials/11-contentbase.md +191 -0
- package/docs/tutorials/12-assistants.md +215 -0
- package/docs/tutorials/13-introspection.md +147 -0
- package/docs/tutorials/14-type-system.md +174 -0
- package/docs/tutorials/15-project-patterns.md +222 -0
- package/docs/tutorials/16-google-features.md +534 -0
- package/docs/tutorials/17-tui-blocks.md +530 -0
- package/docs/tutorials/18-semantic-search.md +334 -0
- package/index.ts +1 -0
- package/luca.console.ts +9 -0
- package/main.py +6 -0
- package/package.json +154 -0
- package/pyproject.toml +7 -0
- package/scripts/animations/chrome-glitch.ts +55 -0
- package/scripts/animations/index.ts +16 -0
- package/scripts/animations/neon-pulse.ts +64 -0
- package/scripts/animations/types.ts +6 -0
- package/scripts/build-web.ts +28 -0
- package/scripts/examples/ask-luca-expert.ts +42 -0
- package/scripts/examples/assistant-questions.ts +12 -0
- package/scripts/examples/excalidraw-expert.ts +75 -0
- package/scripts/examples/expert-chat.ts +0 -0
- package/scripts/examples/file-manager.ts +14 -0
- package/scripts/examples/ideas.ts +12 -0
- package/scripts/examples/interactive-chat.ts +20 -0
- package/scripts/examples/openai-tool-calls.ts +113 -0
- package/scripts/examples/opening-a-web-browser.ts +5 -0
- package/scripts/examples/telegram-bot.ts +79 -0
- package/scripts/examples/telegram-ink-ui.ts +302 -0
- package/scripts/examples/using-assistant-with-mcp.ts +560 -0
- package/scripts/examples/using-claude-code.ts +10 -0
- package/scripts/examples/using-contentdb.ts +35 -0
- package/scripts/examples/using-conversations.ts +35 -0
- package/scripts/examples/using-disk-cache.ts +10 -0
- package/scripts/examples/using-docker-shell.ts +75 -0
- package/scripts/examples/using-elevenlabs.ts +25 -0
- package/scripts/examples/using-google-calendar.ts +57 -0
- package/scripts/examples/using-google-docs.ts +74 -0
- package/scripts/examples/using-google-drive.ts +74 -0
- package/scripts/examples/using-google-sheets.ts +89 -0
- package/scripts/examples/using-nlp.ts +55 -0
- package/scripts/examples/using-ollama.ts +10 -0
- package/scripts/examples/using-openai-codex.ts +23 -0
- package/scripts/examples/using-postgres.ts +55 -0
- package/scripts/examples/using-runpod.ts +32 -0
- package/scripts/examples/using-tts.ts +40 -0
- package/scripts/examples/vm-loading-esm-modules.ts +16 -0
- package/scripts/scaffold.ts +391 -0
- package/scripts/scratch.ts +15 -0
- package/scripts/test-command-listener.ts +123 -0
- package/scripts/test-window-manager-lifecycle.ts +86 -0
- package/scripts/test-window-manager.ts +43 -0
- package/scripts/update-introspection-data.ts +58 -0
- package/src/agi/README.md +14 -0
- package/src/agi/container.server.ts +114 -0
- package/src/agi/endpoints/ask.ts +60 -0
- package/src/agi/endpoints/conversations/[id].ts +45 -0
- package/src/agi/endpoints/conversations.ts +31 -0
- package/src/agi/endpoints/experts.ts +37 -0
- package/src/agi/features/assistant.ts +767 -0
- package/src/agi/features/assistants-manager.ts +260 -0
- package/src/agi/features/claude-code.ts +1111 -0
- package/src/agi/features/conversation-history.ts +497 -0
- package/src/agi/features/conversation.ts +799 -0
- package/src/agi/features/openai-codex.ts +631 -0
- package/src/agi/features/openapi.ts +438 -0
- package/src/agi/features/skills-library.ts +425 -0
- package/src/agi/index.ts +6 -0
- package/src/agi/lib/token-counter.ts +122 -0
- package/src/browser.ts +25 -0
- package/src/bus.ts +100 -0
- package/src/cli/cli.ts +70 -0
- package/src/client.ts +461 -0
- package/src/clients/civitai/index.ts +541 -0
- package/src/clients/client-template.ts +41 -0
- package/src/clients/comfyui/index.ts +597 -0
- package/src/clients/elevenlabs/index.ts +291 -0
- package/src/clients/openai/index.ts +451 -0
- package/src/clients/supabase/index.ts +366 -0
- package/src/command.ts +164 -0
- package/src/commands/chat.ts +182 -0
- package/src/commands/console.ts +192 -0
- package/src/commands/describe.ts +433 -0
- package/src/commands/eval.ts +116 -0
- package/src/commands/help.ts +214 -0
- package/src/commands/index.ts +14 -0
- package/src/commands/mcp.ts +64 -0
- package/src/commands/prompt.ts +807 -0
- package/src/commands/run.ts +257 -0
- package/src/commands/sandbox-mcp.ts +439 -0
- package/src/commands/scaffold.ts +79 -0
- package/src/commands/serve.ts +172 -0
- package/src/container.ts +781 -0
- package/src/endpoint.ts +340 -0
- package/src/feature.ts +75 -0
- package/src/hash-object.ts +97 -0
- package/src/helper.ts +543 -0
- package/src/introspection/generated.agi.ts +23388 -0
- package/src/introspection/generated.node.ts +18899 -0
- package/src/introspection/generated.web.ts +2021 -0
- package/src/introspection/index.ts +256 -0
- package/src/introspection/scan.ts +912 -0
- package/src/node/container.ts +354 -0
- package/src/node/feature.ts +13 -0
- package/src/node/features/container-link.ts +558 -0
- package/src/node/features/content-db.ts +475 -0
- package/src/node/features/disk-cache.ts +382 -0
- package/src/node/features/dns.ts +655 -0
- package/src/node/features/docker.ts +912 -0
- package/src/node/features/downloader.ts +92 -0
- package/src/node/features/esbuild.ts +68 -0
- package/src/node/features/file-manager.ts +357 -0
- package/src/node/features/fs.ts +534 -0
- package/src/node/features/git.ts +492 -0
- package/src/node/features/google-auth.ts +502 -0
- package/src/node/features/google-calendar.ts +300 -0
- package/src/node/features/google-docs.ts +404 -0
- package/src/node/features/google-drive.ts +339 -0
- package/src/node/features/google-sheets.ts +279 -0
- package/src/node/features/grep.ts +406 -0
- package/src/node/features/helpers.ts +374 -0
- package/src/node/features/ink.ts +490 -0
- package/src/node/features/ipc-socket.ts +459 -0
- package/src/node/features/json-tree.ts +188 -0
- package/src/node/features/launcher-app-command-listener.ts +388 -0
- package/src/node/features/networking.ts +925 -0
- package/src/node/features/nlp.ts +211 -0
- package/src/node/features/opener.ts +166 -0
- package/src/node/features/os.ts +157 -0
- package/src/node/features/package-finder.ts +539 -0
- package/src/node/features/port-exposer.ts +342 -0
- package/src/node/features/postgres.ts +273 -0
- package/src/node/features/proc.ts +502 -0
- package/src/node/features/process-manager.ts +542 -0
- package/src/node/features/python.ts +444 -0
- package/src/node/features/repl.ts +194 -0
- package/src/node/features/runpod.ts +802 -0
- package/src/node/features/secure-shell.ts +248 -0
- package/src/node/features/semantic-search.ts +924 -0
- package/src/node/features/sqlite.ts +289 -0
- package/src/node/features/telegram.ts +342 -0
- package/src/node/features/tts.ts +184 -0
- package/src/node/features/ui.ts +857 -0
- package/src/node/features/vault.ts +164 -0
- package/src/node/features/vm.ts +312 -0
- package/src/node/features/window-manager.ts +804 -0
- package/src/node/features/yaml-tree.ts +149 -0
- package/src/node/features/yaml.ts +132 -0
- package/src/node.ts +70 -0
- package/src/react/index.ts +175 -0
- package/src/registry.ts +199 -0
- package/src/scaffolds/generated.ts +1613 -0
- package/src/scaffolds/template.ts +37 -0
- package/src/schemas/base.ts +255 -0
- package/src/server.ts +135 -0
- package/src/servers/express.ts +209 -0
- package/src/servers/mcp.ts +805 -0
- package/src/servers/socket.ts +120 -0
- package/src/state.ts +101 -0
- package/src/web/clients/socket.ts +82 -0
- package/src/web/container.ts +74 -0
- package/src/web/extension.ts +30 -0
- package/src/web/feature.ts +12 -0
- package/src/web/features/asset-loader.ts +64 -0
- package/src/web/features/container-link.ts +385 -0
- package/src/web/features/esbuild.ts +79 -0
- package/src/web/features/helpers.ts +267 -0
- package/src/web/features/network.ts +61 -0
- package/src/web/features/speech.ts +87 -0
- package/src/web/features/vault.ts +189 -0
- package/src/web/features/vm.ts +78 -0
- package/src/web/features/voice-recognition.ts +129 -0
- package/src/web/shims/isomorphic-vm.ts +149 -0
- package/test/bus.test.ts +134 -0
- package/test/clients-servers.test.ts +216 -0
- package/test/container-link.test.ts +274 -0
- package/test/features.test.ts +160 -0
- package/test/integration.test.ts +787 -0
- package/test/node-container.test.ts +121 -0
- package/test/rate-limit.test.ts +272 -0
- package/test/semantic-search.test.ts +550 -0
- package/test/state.test.ts +121 -0
- package/test-integration/assistant.test.ts +138 -0
- package/test-integration/assistants-manager.test.ts +123 -0
- package/test-integration/claude-code.test.ts +98 -0
- package/test-integration/conversation-history.test.ts +205 -0
- package/test-integration/conversation.test.ts +137 -0
- package/test-integration/elevenlabs.test.ts +55 -0
- package/test-integration/google-services.test.ts +80 -0
- package/test-integration/helpers.ts +89 -0
- package/test-integration/openai-codex.test.ts +93 -0
- package/test-integration/runpod.test.ts +58 -0
- package/test-integration/server-endpoints.test.ts +97 -0
- package/test-integration/skills-library.test.ts +157 -0
- package/test-integration/telegram.test.ts +46 -0
- package/tsconfig.json +58 -0
- package/uv.lock +8 -0
package/src/endpoint.ts
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { Helper } from './helper.js'
|
|
2
|
+
import type { Container, ContainerContext } from './container.js'
|
|
3
|
+
import { Registry } from './registry.js'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import { EndpointStateSchema, EndpointOptionsSchema, EndpointEventsSchema } from './schemas/base.js'
|
|
6
|
+
|
|
7
|
+
export interface AvailableEndpoints {}
|
|
8
|
+
|
|
9
|
+
export type EndpointState = z.infer<typeof EndpointStateSchema>
|
|
10
|
+
export type EndpointOptions = z.infer<typeof EndpointOptionsSchema>
|
|
11
|
+
|
|
12
|
+
export type EndpointHandler = (
|
|
13
|
+
parameters: Record<string, any>,
|
|
14
|
+
context: EndpointContext
|
|
15
|
+
) => Promise<any> | any
|
|
16
|
+
|
|
17
|
+
export type EndpointContext = {
|
|
18
|
+
container: Container<any>
|
|
19
|
+
request: any
|
|
20
|
+
response: any
|
|
21
|
+
query: Record<string, any>
|
|
22
|
+
body: Record<string, any>
|
|
23
|
+
params: Record<string, any>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface EndpointRateLimit {
|
|
27
|
+
/** Maximum requests allowed per window */
|
|
28
|
+
maxRequests: number
|
|
29
|
+
/** Window size in seconds (default: 1) */
|
|
30
|
+
windowSeconds?: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface EndpointModule {
|
|
34
|
+
path: string
|
|
35
|
+
get?: EndpointHandler
|
|
36
|
+
post?: EndpointHandler
|
|
37
|
+
put?: EndpointHandler
|
|
38
|
+
patch?: EndpointHandler
|
|
39
|
+
delete?: EndpointHandler
|
|
40
|
+
getSchema?: z.ZodType
|
|
41
|
+
postSchema?: z.ZodType
|
|
42
|
+
putSchema?: z.ZodType
|
|
43
|
+
patchSchema?: z.ZodType
|
|
44
|
+
deleteSchema?: z.ZodType
|
|
45
|
+
/** Rate limit applied to all methods on this endpoint */
|
|
46
|
+
rateLimit?: EndpointRateLimit
|
|
47
|
+
/** Per-method rate limits (overrides the endpoint-level rateLimit) */
|
|
48
|
+
getRateLimit?: EndpointRateLimit
|
|
49
|
+
postRateLimit?: EndpointRateLimit
|
|
50
|
+
putRateLimit?: EndpointRateLimit
|
|
51
|
+
patchRateLimit?: EndpointRateLimit
|
|
52
|
+
deleteRateLimit?: EndpointRateLimit
|
|
53
|
+
description?: string
|
|
54
|
+
tags?: string[]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete'] as const
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Sliding-window rate limiter keyed by IP address.
|
|
61
|
+
* Tracks timestamps of requests and prunes entries older than the window.
|
|
62
|
+
*/
|
|
63
|
+
class RateLimiter {
|
|
64
|
+
private _windows = new Map<string, number[]>()
|
|
65
|
+
|
|
66
|
+
/** Returns true if the request is allowed, false if rate-limited. */
|
|
67
|
+
allow(key: string, maxRequests: number, windowMs: number): boolean {
|
|
68
|
+
const now = Date.now()
|
|
69
|
+
let timestamps = this._windows.get(key)
|
|
70
|
+
|
|
71
|
+
if (!timestamps) {
|
|
72
|
+
timestamps = []
|
|
73
|
+
this._windows.set(key, timestamps)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Prune timestamps outside the window
|
|
77
|
+
while (timestamps.length > 0 && timestamps[0] <= now - windowMs) {
|
|
78
|
+
timestamps.shift()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (timestamps.length >= maxRequests) {
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
timestamps.push(now)
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Clear all tracking state */
|
|
90
|
+
reset(): void {
|
|
91
|
+
this._windows.clear()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type EndpointFactory = <T extends keyof AvailableEndpoints>(
|
|
96
|
+
key: T,
|
|
97
|
+
options?: ConstructorParameters<AvailableEndpoints[T]>[0]
|
|
98
|
+
) => NonNullable<InstanceType<AvailableEndpoints[T]>>
|
|
99
|
+
|
|
100
|
+
export interface EndpointsInterface {
|
|
101
|
+
endpoints: EndpointsRegistry
|
|
102
|
+
endpoint: EndpointFactory
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export class Endpoint<
|
|
106
|
+
T extends EndpointState = EndpointState,
|
|
107
|
+
K extends EndpointOptions = EndpointOptions
|
|
108
|
+
> extends Helper<T, K> {
|
|
109
|
+
static override shortcut = 'endpoints.base'
|
|
110
|
+
static override description = 'File-based HTTP endpoint with Remix-like DX'
|
|
111
|
+
static override stateSchema = EndpointStateSchema
|
|
112
|
+
static override optionsSchema = EndpointOptionsSchema
|
|
113
|
+
static override eventsSchema = EndpointEventsSchema
|
|
114
|
+
|
|
115
|
+
private _module: EndpointModule | null = null
|
|
116
|
+
private _rateLimiter = new RateLimiter()
|
|
117
|
+
|
|
118
|
+
static attach(container: Container & EndpointsInterface): any {
|
|
119
|
+
Object.assign(container, {
|
|
120
|
+
get endpoints() {
|
|
121
|
+
return endpoints
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
endpoint<T extends keyof AvailableEndpoints>(
|
|
125
|
+
id: T,
|
|
126
|
+
options?: ConstructorParameters<AvailableEndpoints[T]>[0]
|
|
127
|
+
): InstanceType<AvailableEndpoints[T]> {
|
|
128
|
+
const BaseClass = endpoints.lookup(id as string) as any
|
|
129
|
+
|
|
130
|
+
return container.createHelperInstance({
|
|
131
|
+
cache: helperCache,
|
|
132
|
+
type: 'endpoint',
|
|
133
|
+
id: String(id),
|
|
134
|
+
BaseClass,
|
|
135
|
+
options,
|
|
136
|
+
fallbackName: String(id),
|
|
137
|
+
}) as InstanceType<AvailableEndpoints[T]>
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
container.registerHelperType('endpoints', 'endpoint')
|
|
142
|
+
|
|
143
|
+
return container
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
override get initialState(): T {
|
|
147
|
+
return ({
|
|
148
|
+
mounted: false,
|
|
149
|
+
path: this.options.path || '',
|
|
150
|
+
methods: [],
|
|
151
|
+
requestCount: 0,
|
|
152
|
+
} as unknown) as T
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
get path() {
|
|
156
|
+
return this.options.path
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
get module() {
|
|
160
|
+
return this._module
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
get methods(): string[] {
|
|
164
|
+
if (!this._module) return []
|
|
165
|
+
return HTTP_METHODS.filter((m) => typeof (this._module as any)[m] === 'function')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
get isMounted() {
|
|
169
|
+
return !!this.state.get('mounted')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async load(mod?: EndpointModule): Promise<this> {
|
|
173
|
+
if (mod) {
|
|
174
|
+
this._module = mod
|
|
175
|
+
} else if (this.options.filePath) {
|
|
176
|
+
const imported = await import(`${this.options.filePath}?t=${Date.now()}`)
|
|
177
|
+
this._module = imported.default || imported
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.state.set('methods', this.methods)
|
|
181
|
+
this.state.set('path', this.path)
|
|
182
|
+
this.emit('loaded', this._module)
|
|
183
|
+
return this
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async reload(): Promise<this> {
|
|
187
|
+
this._module = null
|
|
188
|
+
return this.load()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
handler(method: string): EndpointHandler | undefined {
|
|
192
|
+
return this._module?.[method as keyof EndpointModule] as EndpointHandler | undefined
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
schema(method: string): z.ZodType | undefined {
|
|
196
|
+
return this._module?.[`${method}Schema` as keyof EndpointModule] as z.ZodType | undefined
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Returns the rate limit config for a given method, or undefined if none. */
|
|
200
|
+
rateLimitFor(method: string): EndpointRateLimit | undefined {
|
|
201
|
+
const perMethod = this._module?.[`${method}RateLimit` as keyof EndpointModule] as EndpointRateLimit | undefined
|
|
202
|
+
return perMethod || this._module?.rateLimit
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Access the rate limiter instance (useful for testing or manual resets) */
|
|
206
|
+
get rateLimiter(): RateLimiter {
|
|
207
|
+
return this._rateLimiter
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
mount(app: any): this {
|
|
211
|
+
for (const method of this.methods) {
|
|
212
|
+
const endpoint = this
|
|
213
|
+
|
|
214
|
+
app[method](this.path, async (req: any, res: any) => {
|
|
215
|
+
try {
|
|
216
|
+
// Rate limit check
|
|
217
|
+
const limit = endpoint.rateLimitFor(method)
|
|
218
|
+
if (limit) {
|
|
219
|
+
const ip = req.ip || req.socket?.remoteAddress || 'unknown'
|
|
220
|
+
const key = `${method}:${ip}`
|
|
221
|
+
const windowMs = (limit.windowSeconds ?? 1) * 1000
|
|
222
|
+
if (!endpoint._rateLimiter.allow(key, limit.maxRequests, windowMs)) {
|
|
223
|
+
endpoint.emit('error', new Error(`Rate limit exceeded for ${method.toUpperCase()} ${endpoint.path}`))
|
|
224
|
+
res.status(429).json({ error: 'Too Many Requests' })
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const currentHandler = endpoint.handler(method)
|
|
230
|
+
if (!currentHandler) {
|
|
231
|
+
res.status(404).json({ error: 'Not found' })
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const parameters = { ...req.query, ...req.body, ...req.params }
|
|
236
|
+
const currentSchema = endpoint.schema(method)
|
|
237
|
+
const validated = currentSchema ? currentSchema.parse(parameters) : parameters
|
|
238
|
+
|
|
239
|
+
const ctx: EndpointContext = {
|
|
240
|
+
container: endpoint.container,
|
|
241
|
+
request: req,
|
|
242
|
+
response: res,
|
|
243
|
+
query: req.query || {},
|
|
244
|
+
body: req.body || {},
|
|
245
|
+
params: req.params || {},
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const result = await currentHandler(validated, ctx)
|
|
249
|
+
endpoint.state.set('requestCount', (endpoint.state.get('requestCount') || 0) + 1)
|
|
250
|
+
endpoint.emit('request', method, endpoint.path, parameters)
|
|
251
|
+
|
|
252
|
+
if (!res.headersSent) {
|
|
253
|
+
res.json(result)
|
|
254
|
+
}
|
|
255
|
+
} catch (err: any) {
|
|
256
|
+
endpoint.emit('error', err)
|
|
257
|
+
if (!res.headersSent) {
|
|
258
|
+
if (err.name === 'ZodError') {
|
|
259
|
+
const issues = err.issues || err.errors || []
|
|
260
|
+
const details = issues.map((e: any) => `${(e.path || []).join('.')}: ${e.message}`).join(', ')
|
|
261
|
+
console.error(`[${method.toUpperCase()} ${endpoint.path}] Validation failed: ${details}`)
|
|
262
|
+
res.status(400).json({ error: `Validation failed: ${details}`, details: issues })
|
|
263
|
+
} else {
|
|
264
|
+
console.error(`[${method.toUpperCase()} ${endpoint.path}] ${err.message}`)
|
|
265
|
+
res.status(500).json({ error: err.message })
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
this.state.set('mounted', true)
|
|
273
|
+
this.emit('mounted', this.path)
|
|
274
|
+
return this
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
toOpenAPIPathItem(): Record<string, any> {
|
|
278
|
+
const pathItem: Record<string, any> = {}
|
|
279
|
+
|
|
280
|
+
for (const method of this.methods) {
|
|
281
|
+
const methodSchema = this.schema(method)
|
|
282
|
+
const operationId = `${method}_${this.path.replace(/\//g, '_').replace(/^_/, '')}`
|
|
283
|
+
|
|
284
|
+
const operation: Record<string, any> = {
|
|
285
|
+
operationId,
|
|
286
|
+
summary: this._module?.description || `${method.toUpperCase()} ${this.path}`,
|
|
287
|
+
tags: this._module?.tags || [],
|
|
288
|
+
responses: {
|
|
289
|
+
'200': {
|
|
290
|
+
description: 'Successful response',
|
|
291
|
+
content: { 'application/json': { schema: { type: 'object' } } },
|
|
292
|
+
},
|
|
293
|
+
...(this.rateLimitFor(method) ? { '429': { description: 'Rate limit exceeded' } } : {}),
|
|
294
|
+
'400': { description: 'Validation error' },
|
|
295
|
+
'500': { description: 'Server error' },
|
|
296
|
+
},
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (methodSchema) {
|
|
300
|
+
try {
|
|
301
|
+
const jsonSchema = (methodSchema as any).toJSONSchema()
|
|
302
|
+
|
|
303
|
+
if (method === 'get' || method === 'delete') {
|
|
304
|
+
operation.parameters = Object.entries((jsonSchema as any).properties || {}).map(
|
|
305
|
+
([name, prop]: [string, any]) => ({
|
|
306
|
+
name,
|
|
307
|
+
in: 'query',
|
|
308
|
+
required: (jsonSchema as any).required?.includes(name) || false,
|
|
309
|
+
schema: prop,
|
|
310
|
+
description: prop.description || '',
|
|
311
|
+
})
|
|
312
|
+
)
|
|
313
|
+
} else {
|
|
314
|
+
operation.requestBody = {
|
|
315
|
+
required: true,
|
|
316
|
+
content: { 'application/json': { schema: jsonSchema } },
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} catch {
|
|
320
|
+
// Schema conversion failed, serve without parameter docs
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
pathItem[method] = operation
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return pathItem
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export class EndpointsRegistry extends Registry<Endpoint<any>> {
|
|
332
|
+
override scope = 'endpoints'
|
|
333
|
+
override baseClass = Endpoint
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export const endpoints = new EndpointsRegistry()
|
|
337
|
+
|
|
338
|
+
export const helperCache = new Map()
|
|
339
|
+
|
|
340
|
+
export default Endpoint
|
package/src/feature.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Helper } from './helper.js';
|
|
2
|
+
import { Registry } from './registry.js'
|
|
3
|
+
import type { ContainerContext } from './container.js'
|
|
4
|
+
import { kebabCase, camelCase } from 'lodash-es'
|
|
5
|
+
import type { YAML } from './node/features/yaml.js';
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from './schemas/base.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Use module augmentation to register features, the same way you would register
|
|
11
|
+
* them at runtime. This will help developers get autocomplete etc.
|
|
12
|
+
*/
|
|
13
|
+
export interface AvailableFeatures {
|
|
14
|
+
yaml: typeof YAML
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type FeatureOptions = z.infer<typeof FeatureOptionsSchema>
|
|
18
|
+
export type FeatureState = z.infer<typeof FeatureStateSchema>
|
|
19
|
+
|
|
20
|
+
export abstract class Feature<T extends FeatureState = FeatureState, K extends FeatureOptions = FeatureOptions> extends Helper<T, K> {
|
|
21
|
+
static override stateSchema = FeatureStateSchema
|
|
22
|
+
static override optionsSchema = FeatureOptionsSchema
|
|
23
|
+
static override eventsSchema = FeatureEventsSchema
|
|
24
|
+
|
|
25
|
+
get shortcut() {
|
|
26
|
+
return (this.constructor as any).shortcut as string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get isEnabled() {
|
|
30
|
+
return this.state.get('enabled')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
constructor(options: K, context: ContainerContext) {
|
|
34
|
+
super(options, context)
|
|
35
|
+
|
|
36
|
+
if(typeof context.container !== 'object') {
|
|
37
|
+
console.error(this, options, context)
|
|
38
|
+
throw new Error('You should not instantiate a feature directly. Use container.feature() instead.')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if(options?.enable) {
|
|
43
|
+
this.enable()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* For features where there only needs to be a single instance, you
|
|
49
|
+
* can use this method to attach the feature to the container.
|
|
50
|
+
*/
|
|
51
|
+
protected attachToContainer() {
|
|
52
|
+
Object.defineProperty(this.container, this.shortcut.split('.').pop()!, {
|
|
53
|
+
get: () => this,
|
|
54
|
+
configurable: true,
|
|
55
|
+
enumerable: true,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async enable(options: any = {}) : Promise<this> {
|
|
60
|
+
this.attachToContainer()
|
|
61
|
+
this.emit('enabled')
|
|
62
|
+
this.state.set('enabled', true)
|
|
63
|
+
|
|
64
|
+
this.container.emit('featureEnabled', this.shortcut, this)
|
|
65
|
+
|
|
66
|
+
return this
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class FeaturesRegistry extends Registry<Feature<any, any>> {
|
|
71
|
+
override scope = "features"
|
|
72
|
+
override baseClass = Feature as any
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const features = new FeaturesRegistry()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-compatible object hashing. Produces a deterministic string signature
|
|
3
|
+
* for any JavaScript value, suitable for use as a cache key or identity check.
|
|
4
|
+
*
|
|
5
|
+
* Replaces the `object-hash` npm package which has Node.js-specific dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function sortedEntries(obj: Record<string, any>): [string, any][] {
|
|
9
|
+
return Object.keys(obj).sort().map((k) => [k, obj[k]])
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function serialize(value: any, seen: Set<any>): string {
|
|
13
|
+
if (value === null) return 'null'
|
|
14
|
+
if (value === undefined) return 'undefined'
|
|
15
|
+
|
|
16
|
+
const type = typeof value
|
|
17
|
+
|
|
18
|
+
if (type === 'boolean' || type === 'number' || type === 'bigint') {
|
|
19
|
+
return `${type}:${value}`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (type === 'string') {
|
|
23
|
+
return `string:${value.length}:${value}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (type === 'symbol') {
|
|
27
|
+
return `symbol:${value.toString()}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (type === 'function') {
|
|
31
|
+
return `function:${value.name || 'anonymous'}`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Circular reference guard
|
|
35
|
+
if (seen.has(value)) return 'circular'
|
|
36
|
+
seen.add(value)
|
|
37
|
+
|
|
38
|
+
if (value instanceof Date) {
|
|
39
|
+
return `date:${value.toISOString()}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (value instanceof RegExp) {
|
|
43
|
+
return `regexp:${value.toString()}`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (value instanceof Error) {
|
|
47
|
+
return `error:${value.name}:${value.message}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (value instanceof Set) {
|
|
51
|
+
const items = Array.from(value).map((v) => serialize(v, seen)).sort()
|
|
52
|
+
return `set:[${items.join(',')}]`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (value instanceof Map) {
|
|
56
|
+
const entries = Array.from(value.entries())
|
|
57
|
+
.map(([k, v]) => `${serialize(k, seen)}=>${serialize(v, seen)}`)
|
|
58
|
+
.sort()
|
|
59
|
+
return `map:{${entries.join(',')}}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
|
|
63
|
+
const buf = value instanceof ArrayBuffer ? new Uint8Array(value) : new Uint8Array((value as any).buffer, (value as any).byteOffset, (value as any).byteLength)
|
|
64
|
+
return `buffer:${Array.from(buf).join(',')}`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
const items = value.map((v) => serialize(v, seen))
|
|
69
|
+
return `array:[${items.join(',')}]`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Plain objects — sort keys for determinism
|
|
73
|
+
if (type === 'object') {
|
|
74
|
+
const entries = sortedEntries(value)
|
|
75
|
+
.map(([k, v]) => `${k}:${serialize(v, seen)}`)
|
|
76
|
+
return `object:{${entries.join(',')}}`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return `unknown:${String(value)}`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Simple non-crypto hash (djb2 variant) that works in all JS environments.
|
|
84
|
+
*/
|
|
85
|
+
function djb2(str: string): string {
|
|
86
|
+
let h1 = 0x811c9dc5 // FNV offset basis
|
|
87
|
+
for (let i = 0; i < str.length; i++) {
|
|
88
|
+
h1 ^= str.charCodeAt(i)
|
|
89
|
+
h1 = (h1 * 0x01000193) >>> 0 // FNV prime, keep as uint32
|
|
90
|
+
}
|
|
91
|
+
return h1.toString(36)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default function hashObject(value: any): string {
|
|
95
|
+
const serialized = serialize(value, new Set())
|
|
96
|
+
return djb2(serialized)
|
|
97
|
+
}
|