@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
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
3
|
+
import { Feature, features } from '../feature.js'
|
|
4
|
+
import { Client } from '../../client.js'
|
|
5
|
+
import type { Registry } from '../../registry.js'
|
|
6
|
+
import type { AssetLoader } from './asset-loader.js'
|
|
7
|
+
|
|
8
|
+
export const HelpersStateSchema = FeatureStateSchema.extend({
|
|
9
|
+
discovered: z.record(z.string(), z.boolean()).default({}).describe('Which registry types have been discovered'),
|
|
10
|
+
registered: z.array(z.string()).default([]).describe('Names of project-level helpers that were discovered (type.name)'),
|
|
11
|
+
manifestLoaded: z.boolean().default(false).describe('Whether the manifest has been fetched'),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export type HelpersState = z.infer<typeof HelpersStateSchema>
|
|
15
|
+
|
|
16
|
+
export const HelpersOptionsSchema = FeatureOptionsSchema.extend({
|
|
17
|
+
manifestURL: z.string().optional().describe('URL to fetch the helpers manifest from. Defaults to /.well-known/luca.manifest.json'),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export type HelpersOptions = z.infer<typeof HelpersOptionsSchema>
|
|
21
|
+
|
|
22
|
+
export const HelpersEventsSchema = FeatureEventsSchema.extend({
|
|
23
|
+
discovered: z.tuple([
|
|
24
|
+
z.string().describe('Registry type that was discovered'),
|
|
25
|
+
z.array(z.string()).describe('Names of newly registered helpers'),
|
|
26
|
+
]).describe('Emitted after a registry type has been discovered'),
|
|
27
|
+
registered: z.tuple([
|
|
28
|
+
z.string().describe('Registry type'),
|
|
29
|
+
z.string().describe('Helper name'),
|
|
30
|
+
]).describe('Emitted when a single helper is registered'),
|
|
31
|
+
manifestLoaded: z.tuple([
|
|
32
|
+
z.any().describe('The parsed manifest object'),
|
|
33
|
+
]).describe('Emitted when the manifest is successfully fetched'),
|
|
34
|
+
manifestError: z.tuple([
|
|
35
|
+
z.any().describe('The error that occurred'),
|
|
36
|
+
]).describe('Emitted when the manifest fetch fails'),
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
type RegistryType = 'features' | 'clients'
|
|
40
|
+
|
|
41
|
+
interface ManifestEntry {
|
|
42
|
+
id: string
|
|
43
|
+
description?: string
|
|
44
|
+
url: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface Manifest {
|
|
48
|
+
features?: Record<string, ManifestEntry>
|
|
49
|
+
clients?: Record<string, ManifestEntry>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The Helpers feature discovers and loads project-level helpers from a JSON manifest
|
|
54
|
+
* served over HTTP. Scripts are injected via AssetLoader and self-register into
|
|
55
|
+
* the container's registries.
|
|
56
|
+
*
|
|
57
|
+
* This is the web equivalent of the node Helpers feature, which scans the filesystem.
|
|
58
|
+
* Instead of filesystem scanning, this feature fetches a manifest from a well-known URL
|
|
59
|
+
* and uses AssetLoader.loadScript() to inject each helper's script tag.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* const helpers = container.feature('helpers', { enable: true })
|
|
64
|
+
*
|
|
65
|
+
* // Discover all helper types from the manifest
|
|
66
|
+
* await helpers.discoverAll()
|
|
67
|
+
*
|
|
68
|
+
* // Discover a specific type
|
|
69
|
+
* await helpers.discover('features')
|
|
70
|
+
*
|
|
71
|
+
* // Unified view of all available helpers
|
|
72
|
+
* console.log(helpers.available)
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export class Helpers extends Feature<HelpersState, HelpersOptions> {
|
|
76
|
+
static override shortcut = 'features.helpers' as const
|
|
77
|
+
static override description = 'Unified gateway for discovering and registering project-level helpers via HTTP manifest'
|
|
78
|
+
static override stateSchema = HelpersStateSchema
|
|
79
|
+
static override optionsSchema = HelpersOptionsSchema
|
|
80
|
+
static override eventsSchema = HelpersEventsSchema
|
|
81
|
+
|
|
82
|
+
private _manifest: Manifest | null = null
|
|
83
|
+
|
|
84
|
+
private get registryMap(): Record<RegistryType, { registry: Registry<any> }> {
|
|
85
|
+
return {
|
|
86
|
+
features: { registry: this.container.features as any },
|
|
87
|
+
clients: { registry: (this.container as any).clients as Registry<any> },
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The URL to fetch the helpers manifest from. */
|
|
92
|
+
get manifestURL(): string {
|
|
93
|
+
return this.options.manifestURL || '/.well-known/luca.manifest.json'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Set a new manifest URL. Invalidates any cached manifest.
|
|
98
|
+
*
|
|
99
|
+
* @param url - The new URL to fetch the manifest from
|
|
100
|
+
*/
|
|
101
|
+
setManifestURL(url: string) {
|
|
102
|
+
this.options.manifestURL = url
|
|
103
|
+
this._manifest = null
|
|
104
|
+
this.state.set('manifestLoaded', false)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Returns a unified view of all available helpers across all registries.
|
|
109
|
+
* Each key is a registry type, each value is the list of helper names in that registry.
|
|
110
|
+
*/
|
|
111
|
+
get available(): Record<string, string[]> {
|
|
112
|
+
const result: Record<string, string[]> = {}
|
|
113
|
+
for (const [type, { registry }] of Object.entries(this.registryMap)) {
|
|
114
|
+
result[type] = registry.available
|
|
115
|
+
}
|
|
116
|
+
return result
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Fetch and cache the manifest JSON. Returns cached version on subsequent calls
|
|
121
|
+
* unless invalidated by setManifestURL().
|
|
122
|
+
*/
|
|
123
|
+
private async fetchManifest(): Promise<Manifest> {
|
|
124
|
+
if (this._manifest) {
|
|
125
|
+
return this._manifest
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const response = await fetch(this.manifestURL)
|
|
130
|
+
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
const err = new Error(`Manifest fetch failed: ${response.status} ${response.statusText}`)
|
|
133
|
+
this.emit('manifestError' as any, err)
|
|
134
|
+
return {}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const manifest: Manifest = await response.json()
|
|
138
|
+
this._manifest = manifest
|
|
139
|
+
this.state.set('manifestLoaded', true)
|
|
140
|
+
this.emit('manifestLoaded' as any, manifest)
|
|
141
|
+
return manifest
|
|
142
|
+
} catch (err: any) {
|
|
143
|
+
this.emit('manifestError' as any, err)
|
|
144
|
+
return {}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get the AssetLoader instance from the container.
|
|
150
|
+
*/
|
|
151
|
+
private get assetLoader(): AssetLoader {
|
|
152
|
+
return this.container.feature('assetLoader') as unknown as AssetLoader
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Discover and register helpers of the given type from the manifest.
|
|
157
|
+
*
|
|
158
|
+
* Fetches the manifest, then for each entry of the requested type,
|
|
159
|
+
* loads the script via AssetLoader and checks what got newly registered.
|
|
160
|
+
*
|
|
161
|
+
* @param type - Which type of helpers to discover ('features' or 'clients')
|
|
162
|
+
* @returns Names of helpers that were discovered and registered
|
|
163
|
+
*/
|
|
164
|
+
async discover(type: RegistryType): Promise<string[]> {
|
|
165
|
+
const discovered = this.state.get('discovered') || {}
|
|
166
|
+
|
|
167
|
+
if (discovered[type]) {
|
|
168
|
+
return []
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const manifest = await this.fetchManifest()
|
|
172
|
+
const entries = manifest[type] || {}
|
|
173
|
+
const { registry } = this.registryMap[type]
|
|
174
|
+
const newNames: string[] = []
|
|
175
|
+
|
|
176
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
177
|
+
const beforeNames = new Set(registry.available)
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
await this.assetLoader.loadScript(entry.url)
|
|
181
|
+
} catch (err: any) {
|
|
182
|
+
console.warn(`Helpers: failed to load ${type}/${name} from ${entry.url}: ${err.message}`)
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const afterNames = registry.available
|
|
187
|
+
const added = afterNames.filter((n: string) => !beforeNames.has(n))
|
|
188
|
+
|
|
189
|
+
if (added.length > 0) {
|
|
190
|
+
for (const addedName of added) {
|
|
191
|
+
newNames.push(addedName)
|
|
192
|
+
this.emit('registered' as any, type, addedName)
|
|
193
|
+
}
|
|
194
|
+
} else if (registry.has(name)) {
|
|
195
|
+
// Script may have already been registered under the expected name
|
|
196
|
+
if (!beforeNames.has(name)) {
|
|
197
|
+
newNames.push(name)
|
|
198
|
+
this.emit('registered' as any, type, name)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.state.set('discovered', { ...this.state.get('discovered'), [type]: true })
|
|
204
|
+
|
|
205
|
+
const existing = this.state.get('registered') || []
|
|
206
|
+
this.state.set('registered', [...existing, ...newNames.map(n => `${type}.${n}`)])
|
|
207
|
+
|
|
208
|
+
this.emit('discovered' as any, type, newNames)
|
|
209
|
+
|
|
210
|
+
return newNames
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Discover all helper types from the manifest.
|
|
215
|
+
*
|
|
216
|
+
* @returns Map of registry type to discovered helper names
|
|
217
|
+
*/
|
|
218
|
+
async discoverAll(): Promise<Record<string, string[]>> {
|
|
219
|
+
const results: Record<string, string[]> = {}
|
|
220
|
+
|
|
221
|
+
for (const type of ['features', 'clients'] as RegistryType[]) {
|
|
222
|
+
results[type] = await this.discover(type)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return results
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Convenience method to discover only features.
|
|
230
|
+
*/
|
|
231
|
+
async discoverFeatures(): Promise<string[]> {
|
|
232
|
+
return this.discover('features')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Convenience method to discover only clients.
|
|
237
|
+
*/
|
|
238
|
+
async discoverClients(): Promise<string[]> {
|
|
239
|
+
return this.discover('clients')
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Look up a helper class by type and name.
|
|
244
|
+
*
|
|
245
|
+
* @param type - The registry type
|
|
246
|
+
* @param name - The helper name within that registry
|
|
247
|
+
* @returns The helper constructor
|
|
248
|
+
*/
|
|
249
|
+
lookup(type: RegistryType, name: string): any {
|
|
250
|
+
const { registry } = this.registryMap[type]
|
|
251
|
+
return registry.lookup(name)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get the introspection description for a specific helper.
|
|
256
|
+
*
|
|
257
|
+
* @param type - The registry type
|
|
258
|
+
* @param name - The helper name
|
|
259
|
+
* @returns Markdown description of the helper's interface
|
|
260
|
+
*/
|
|
261
|
+
describe(type: RegistryType, name: string): string {
|
|
262
|
+
const { registry } = this.registryMap[type]
|
|
263
|
+
return registry.describe(name)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export default features.register('helpers', Helpers)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
|
|
3
|
+
import { features, Feature } from "../feature.js";
|
|
4
|
+
import type { Container, ContainerContext } from "../container.js";
|
|
5
|
+
|
|
6
|
+
export const NetworkStateSchema = FeatureStateSchema.extend({
|
|
7
|
+
offline: z.boolean().describe('Whether the browser is currently offline'),
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export const NetworkOptionsSchema = FeatureOptionsSchema.extend({})
|
|
11
|
+
|
|
12
|
+
export type NetworkState = z.infer<typeof NetworkStateSchema>
|
|
13
|
+
export type NetworkOptions = z.infer<typeof NetworkOptionsSchema>
|
|
14
|
+
|
|
15
|
+
export class Network<
|
|
16
|
+
T extends NetworkState = NetworkState,
|
|
17
|
+
K extends NetworkOptions = NetworkOptions
|
|
18
|
+
> extends Feature<T, K> {
|
|
19
|
+
static override stateSchema = NetworkStateSchema
|
|
20
|
+
static override optionsSchema = NetworkOptionsSchema
|
|
21
|
+
static override shortcut = "features.network" as const
|
|
22
|
+
|
|
23
|
+
static attach(container: Container & { network?: Network }) {
|
|
24
|
+
container.features.register("network", Network);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
constructor(options: K, context: ContainerContext) {
|
|
28
|
+
super(options, context);
|
|
29
|
+
this.state.set("offline", !navigator.onLine);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Whether the browser is currently offline. */
|
|
33
|
+
get isOffline() {
|
|
34
|
+
return this.state.get("offline") === true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Whether the browser is currently online. */
|
|
38
|
+
get isOnline() {
|
|
39
|
+
return this.state.get("offline") === false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private handleConnectionChange = () => {
|
|
43
|
+
const isOffline = !navigator.onLine;
|
|
44
|
+
this.state.set('offline', isOffline)
|
|
45
|
+
this.emit(isOffline ? "offline" : "online");
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
start() {
|
|
49
|
+
window.addEventListener("online", this.handleConnectionChange);
|
|
50
|
+
window.addEventListener("offline", this.handleConnectionChange);
|
|
51
|
+
return this
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
disable() {
|
|
55
|
+
window.removeEventListener("online", this.handleConnectionChange);
|
|
56
|
+
window.removeEventListener("offline", this.handleConnectionChange);
|
|
57
|
+
return this
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default features.register('network', Network)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
|
|
3
|
+
import { Feature, features } from "../feature.js";
|
|
4
|
+
import { Container, type ContainerContext } from "../container.js";
|
|
5
|
+
|
|
6
|
+
export const SpeechOptionsSchema = FeatureOptionsSchema.extend({
|
|
7
|
+
voice: z.string().optional().describe('The voice to use for the speech'),
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export const SpeechStateSchema = FeatureStateSchema.extend({
|
|
11
|
+
defaultVoice: z.string().describe('Name of the currently selected default voice'),
|
|
12
|
+
voices: z.array(z.any().describe('Voice object')).optional().describe('Available speech synthesis voices'),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export type SpeechOptions = z.infer<typeof SpeechOptionsSchema>
|
|
16
|
+
export type SpeechState = z.infer<typeof SpeechStateSchema>
|
|
17
|
+
|
|
18
|
+
type Voice = {
|
|
19
|
+
voiceURI: string;
|
|
20
|
+
name: string;
|
|
21
|
+
lang: string;
|
|
22
|
+
localService: boolean;
|
|
23
|
+
default: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class Speech<
|
|
27
|
+
T extends SpeechState = SpeechState,
|
|
28
|
+
K extends SpeechOptions = SpeechOptions
|
|
29
|
+
> extends Feature<T, K> {
|
|
30
|
+
|
|
31
|
+
static attach(container: Container & { speech?: Speech }) {
|
|
32
|
+
container.features.register("speech", Speech);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static override stateSchema = SpeechStateSchema
|
|
36
|
+
static override optionsSchema = SpeechOptionsSchema
|
|
37
|
+
static override shortcut = "features.speech" as const
|
|
38
|
+
|
|
39
|
+
constructor(options: K, context: ContainerContext) {
|
|
40
|
+
super(options,context)
|
|
41
|
+
|
|
42
|
+
if(options.voice) {
|
|
43
|
+
this.state.set("defaultVoice", options.voice)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.loadVoices()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Returns the array of available speech synthesis voices. */
|
|
50
|
+
get voices() {
|
|
51
|
+
return this.state.get('voices') || []
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Returns the Voice object matching the currently selected default voice name. */
|
|
55
|
+
get defaultVoice() {
|
|
56
|
+
return this.voices.find(v => v.name === this.state.get("defaultVoice"))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
loadVoices() {
|
|
60
|
+
const voices = speechSynthesis.getVoices();
|
|
61
|
+
this.state.set("voices", voices);
|
|
62
|
+
|
|
63
|
+
if (!this.state.get("defaultVoice") && voices.length > 0) {
|
|
64
|
+
const defaultVoice = voices.find(v => v.default)!
|
|
65
|
+
this.state.set("defaultVoice", defaultVoice.name);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
setDefaultVoice(name: string) {
|
|
70
|
+
const voice = this.voices.find(v => v.name === name)!
|
|
71
|
+
this.state.set("defaultVoice", voice.name);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
cancel() {
|
|
75
|
+
speechSynthesis.cancel()
|
|
76
|
+
return this
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
say(text: string, options: { voice?: Voice } = {}) {
|
|
80
|
+
const utterance = new SpeechSynthesisUtterance(text);
|
|
81
|
+
const voice = options.voice || this.defaultVoice
|
|
82
|
+
utterance.voice = voice || this.voices[0]!
|
|
83
|
+
speechSynthesis.speak(utterance);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default features.register("speech", Speech);
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
|
|
4
|
+
import { Feature, features } from '../feature.js'
|
|
5
|
+
import { WebContainer} from '../container.js'
|
|
6
|
+
|
|
7
|
+
export const WebVaultStateSchema = FeatureStateSchema.extend({
|
|
8
|
+
secret: z.string().optional().describe('Base64-encoded AES-GCM encryption secret key'),
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export const WebVaultOptionsSchema = FeatureOptionsSchema.extend({
|
|
12
|
+
secret: z.string().optional().describe('Pre-existing base64-encoded secret key to use'),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export type WebVaultState = z.infer<typeof WebVaultStateSchema>
|
|
16
|
+
export type WebVaultOptions = z.infer<typeof WebVaultOptionsSchema>
|
|
17
|
+
|
|
18
|
+
export class WebVault extends Feature<WebVaultState, WebVaultOptions> {
|
|
19
|
+
static override stateSchema = WebVaultStateSchema
|
|
20
|
+
static override optionsSchema = WebVaultOptionsSchema
|
|
21
|
+
static override shortcut = "features.vault" as const
|
|
22
|
+
|
|
23
|
+
async secret({ refresh = false, set = true } = {}) : Promise<ArrayBuffer> {
|
|
24
|
+
if (!this.state.get('secret') && this.options.secret) {
|
|
25
|
+
this.state.set('secret', this.options.secret)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!refresh && this.state.get('secret')) {
|
|
29
|
+
return base64ToArrayBuffer(this.state.get('secret')!)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const val = await generateSecretKey()
|
|
33
|
+
const asString = arrayBufferToBase64(val)
|
|
34
|
+
|
|
35
|
+
if(set && !this.state.get('secret')) {
|
|
36
|
+
this.state.set('secret', asString)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return val
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async decrypt(payload: string) {
|
|
43
|
+
const parts = payload.split("\n------\n")
|
|
44
|
+
const iv = base64ToUint8Array(parts[1]!)
|
|
45
|
+
const ciphertext = base64ToArrayBuffer(parts[0]!)
|
|
46
|
+
const secret = await this.secret()
|
|
47
|
+
|
|
48
|
+
console.log(ciphertext, secret, iv)
|
|
49
|
+
return await decrypt(ciphertext, secret, iv)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async encrypt(payload: string) {
|
|
53
|
+
const secret = await this.secret()
|
|
54
|
+
console.log("encrypting", payload, secret)
|
|
55
|
+
const { iv, ciphertext, } = await encrypt(payload, secret)
|
|
56
|
+
|
|
57
|
+
return [
|
|
58
|
+
arrayBufferToBase64(ciphertext),
|
|
59
|
+
uint8ArrayToBase64(iv)
|
|
60
|
+
].join("\n------\n")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
utils = {
|
|
64
|
+
arrayToString: arrayBufferToBase64,
|
|
65
|
+
stringToArray: base64ToArrayBuffer,
|
|
66
|
+
uintToString: uint8ArrayToBase64,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default features.register('vault', WebVault)
|
|
71
|
+
|
|
72
|
+
async function generateSecretKey(): Promise<ArrayBuffer> {
|
|
73
|
+
const key = await crypto.subtle.generateKey(
|
|
74
|
+
{ name: "AES-GCM", length: 256 },
|
|
75
|
+
true,
|
|
76
|
+
["encrypt", "decrypt"]
|
|
77
|
+
);
|
|
78
|
+
const secretKey = await crypto.subtle.exportKey("raw", key);
|
|
79
|
+
return secretKey;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function encrypt(plaintext: string, secretKey: ArrayBuffer): Promise<{ iv: Uint8Array; ciphertext: ArrayBuffer }> {
|
|
83
|
+
const encoder = new TextEncoder();
|
|
84
|
+
const encodedText = encoder.encode(plaintext);
|
|
85
|
+
const key = await crypto.subtle.importKey(
|
|
86
|
+
"raw",
|
|
87
|
+
secretKey,
|
|
88
|
+
{ name: "AES-GCM" },
|
|
89
|
+
false,
|
|
90
|
+
["encrypt", "decrypt"]
|
|
91
|
+
);
|
|
92
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
93
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
94
|
+
{ name: "AES-GCM", iv },
|
|
95
|
+
key,
|
|
96
|
+
encodedText
|
|
97
|
+
);
|
|
98
|
+
return { iv, ciphertext };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function decrypt(ciphertext: ArrayBuffer, secretKey: ArrayBuffer, iv: Uint8Array): Promise<string> {
|
|
102
|
+
const key = await crypto.subtle.importKey(
|
|
103
|
+
"raw",
|
|
104
|
+
secretKey,
|
|
105
|
+
{ name: "AES-GCM" },
|
|
106
|
+
false,
|
|
107
|
+
["encrypt", "decrypt"]
|
|
108
|
+
);
|
|
109
|
+
const plaintextArrayBuffer = await crypto.subtle.decrypt(
|
|
110
|
+
{ name: "AES-GCM", iv },
|
|
111
|
+
key,
|
|
112
|
+
ciphertext
|
|
113
|
+
);
|
|
114
|
+
const decoder = new TextDecoder();
|
|
115
|
+
const plaintext = decoder.decode(plaintextArrayBuffer);
|
|
116
|
+
return plaintext;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
|
120
|
+
const bytes = new Uint8Array(buffer);
|
|
121
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
122
|
+
let base64 = "";
|
|
123
|
+
|
|
124
|
+
for (let i = 0; i < bytes.byteLength; i += 3) {
|
|
125
|
+
const a = bytes[i];
|
|
126
|
+
const b = bytes[i + 1];
|
|
127
|
+
const c = bytes[i + 2];
|
|
128
|
+
|
|
129
|
+
const index1 = a >> 2;
|
|
130
|
+
const index2 = ((a & 0x03) << 4) | (b >> 4);
|
|
131
|
+
const index3 = isNaN(b!) ? 64 : ((b & 0x0f) << 2) | (c >> 6);
|
|
132
|
+
const index4 = isNaN(b!) || isNaN(c!) ? 64 : c & 0x3f;
|
|
133
|
+
|
|
134
|
+
base64 += chars[index1] + chars[index2] + chars[index3] + chars[index4];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return base64.replace('undefined', '==');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function uint8ArrayToBase64(u: Uint8Array): string {
|
|
141
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
142
|
+
let base64 = "";
|
|
143
|
+
|
|
144
|
+
for (let i = 0; i < u.byteLength; i += 3) {
|
|
145
|
+
const a = u[i];
|
|
146
|
+
const b = u[i + 1];
|
|
147
|
+
const c = u[i + 2];
|
|
148
|
+
|
|
149
|
+
const index1 = a >> 2;
|
|
150
|
+
const index2 = ((a & 0x03) << 4) | (b >> 4);
|
|
151
|
+
const index3 = isNaN(b!) ? 64 : ((b & 0x0f) << 2) | (c >> 6);
|
|
152
|
+
const index4 = isNaN(b!) || isNaN(c!) ? 64 : c & 0x3f;
|
|
153
|
+
|
|
154
|
+
base64 += chars[index1]! + chars[index2]! + chars[index3]! + chars[index4]!;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return base64;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function base64ToArrayBuffer(base64: string): ArrayBuffer {
|
|
161
|
+
const uint8Array = base64ToUint8Array(base64)!;
|
|
162
|
+
return uint8Array.buffer as ArrayBuffer;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function base64ToUint8Array(base64: string): Uint8Array {
|
|
166
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
167
|
+
const lookup = new Uint8Array(256);
|
|
168
|
+
|
|
169
|
+
for (let i = 0; i < chars.length; i++) {
|
|
170
|
+
lookup[chars.charCodeAt(i)] = i;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const padding = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0;
|
|
174
|
+
const length = (base64.length * 3 / 4) - padding;
|
|
175
|
+
const bytes = new Uint8Array(length);
|
|
176
|
+
|
|
177
|
+
for (let i = 0, j = 0; i < base64.length; i += 4, j += 3) {
|
|
178
|
+
const index1 = lookup[base64.charCodeAt(i)];
|
|
179
|
+
const index2 = lookup[base64.charCodeAt(i + 1)];
|
|
180
|
+
const index3 = lookup[base64.charCodeAt(i + 2)];
|
|
181
|
+
const index4 = lookup[base64.charCodeAt(i + 3)];
|
|
182
|
+
|
|
183
|
+
bytes[j] = (index1 << 2) | (index2 >> 4);
|
|
184
|
+
if (j + 1 < length) bytes[j + 1] = ((index2 & 0x0f) << 4) | (index3 >> 2);
|
|
185
|
+
if (j + 2 < length) bytes[j + 2] = ((index3 & 0x03) << 6) | index4;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return bytes;
|
|
189
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
//
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
|
|
5
|
+
import vm from '../shims/isomorphic-vm'
|
|
6
|
+
import { Feature, features } from "../feature.js";
|
|
7
|
+
import { Container } from '../../container.js';
|
|
8
|
+
|
|
9
|
+
export const VMStateSchema = FeatureStateSchema.extend({})
|
|
10
|
+
|
|
11
|
+
export const VMOptionsSchema = FeatureOptionsSchema.extend({
|
|
12
|
+
context: z.any().describe('VM context object').optional(),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export type VMState = z.infer<typeof VMStateSchema>
|
|
16
|
+
export type VMOptions = z.infer<typeof VMOptionsSchema>
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The VM features providers a virtual machine for executing JavaScript code in a sandboxed environment.
|
|
20
|
+
*
|
|
21
|
+
* The Vm feature automatically injects the container.context object into the global scope, so these things
|
|
22
|
+
* can be referenced in the code and the code can use anything provided by the container.
|
|
23
|
+
*/
|
|
24
|
+
export class VM<
|
|
25
|
+
T extends VMState = VMState,
|
|
26
|
+
K extends VMOptions = VMOptions
|
|
27
|
+
> extends Feature<T, K> {
|
|
28
|
+
|
|
29
|
+
static attach(container: Container) {
|
|
30
|
+
container.features.register('vm', VM)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static override stateSchema = VMStateSchema
|
|
34
|
+
static override optionsSchema = VMOptionsSchema
|
|
35
|
+
static override shortcut = "features.vm" as const
|
|
36
|
+
|
|
37
|
+
createScript(code: string) {
|
|
38
|
+
return new vm.Script(code)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
createContext(ctx: any = {}) {
|
|
42
|
+
return vm.createContext({
|
|
43
|
+
...this.container.context,
|
|
44
|
+
...ctx
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async run(code: string, ctx: any = {}, options : any = {}) {
|
|
49
|
+
let script = this.createScript(code)
|
|
50
|
+
|
|
51
|
+
if (options.transform) {
|
|
52
|
+
const esbuild = this.container.feature('esbuild')
|
|
53
|
+
await esbuild.start()
|
|
54
|
+
const result = await esbuild.compile(code, options.transform)
|
|
55
|
+
script = this.createScript(result.code)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const context = this.createContext(ctx)
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const result = script.runInContext({
|
|
62
|
+
...context,
|
|
63
|
+
...(options.exports && { exports: options.exports })
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
if (options.exports) {
|
|
67
|
+
return { result, exports: options.exports, context }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return result
|
|
71
|
+
} catch(error) {
|
|
72
|
+
console.error(`Error running code`, error)
|
|
73
|
+
return error
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default features.register("vm", VM);
|