@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,502 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
3
|
+
import { Feature, features } from '../feature.js'
|
|
4
|
+
import { google } from 'googleapis'
|
|
5
|
+
import type { OAuth2Client } from 'google-auth-library'
|
|
6
|
+
|
|
7
|
+
export const GoogleAuthStateSchema = FeatureStateSchema.extend({
|
|
8
|
+
authMode: z.enum(['oauth2', 'service-account', 'none']).default('none')
|
|
9
|
+
.describe('Current authentication mode'),
|
|
10
|
+
isAuthenticated: z.boolean().default(false)
|
|
11
|
+
.describe('Whether valid credentials are currently available'),
|
|
12
|
+
email: z.string().optional()
|
|
13
|
+
.describe('Authenticated user or service account email'),
|
|
14
|
+
scopes: z.array(z.string()).default([])
|
|
15
|
+
.describe('OAuth2 scopes that have been authorized'),
|
|
16
|
+
tokenExpiry: z.string().optional()
|
|
17
|
+
.describe('ISO timestamp when the current access token expires'),
|
|
18
|
+
lastError: z.string().optional()
|
|
19
|
+
.describe('Last authentication error message'),
|
|
20
|
+
})
|
|
21
|
+
export type GoogleAuthState = z.infer<typeof GoogleAuthStateSchema>
|
|
22
|
+
|
|
23
|
+
export const GoogleAuthOptionsSchema = FeatureOptionsSchema.extend({
|
|
24
|
+
mode: z.enum(['oauth2', 'service-account']).optional()
|
|
25
|
+
.describe('Authentication mode. Auto-detected if serviceAccountKeyPath is set'),
|
|
26
|
+
clientId: z.string().optional()
|
|
27
|
+
.describe('OAuth2 client ID (falls back to GOOGLE_CLIENT_ID env var)'),
|
|
28
|
+
clientSecret: z.string().optional()
|
|
29
|
+
.describe('OAuth2 client secret (falls back to GOOGLE_CLIENT_SECRET env var)'),
|
|
30
|
+
serviceAccountKeyPath: z.string().optional()
|
|
31
|
+
.describe('Path to service account JSON key file (falls back to GOOGLE_SERVICE_ACCOUNT_KEY env var)'),
|
|
32
|
+
serviceAccountKey: z.record(z.string(), z.any()).optional()
|
|
33
|
+
.describe('Service account key as a parsed JSON object (alternative to file path)'),
|
|
34
|
+
scopes: z.array(z.string()).optional()
|
|
35
|
+
.describe('OAuth2 scopes to request'),
|
|
36
|
+
redirectPort: z.number().optional()
|
|
37
|
+
.describe('Port for OAuth2 callback server (falls back to GOOGLE_OAUTH_REDIRECT_PORT env var, then 3000)'),
|
|
38
|
+
tokenCacheKey: z.string().optional()
|
|
39
|
+
.describe('DiskCache key for storing OAuth2 refresh token'),
|
|
40
|
+
})
|
|
41
|
+
export type GoogleAuthOptions = z.infer<typeof GoogleAuthOptionsSchema>
|
|
42
|
+
|
|
43
|
+
export const GoogleAuthEventsSchema = FeatureEventsSchema.extend({
|
|
44
|
+
authenticated: z.tuple([z.object({
|
|
45
|
+
mode: z.string().describe('Auth mode used'),
|
|
46
|
+
email: z.string().optional().describe('User or service account email'),
|
|
47
|
+
})]).describe('Authentication successful'),
|
|
48
|
+
tokenRefreshed: z.tuple([]).describe('Access token was refreshed'),
|
|
49
|
+
authorizationRequired: z.tuple([z.string().describe('Authorization URL to visit')])
|
|
50
|
+
.describe('User must visit this URL to authorize'),
|
|
51
|
+
error: z.tuple([z.any().describe('The error')]).describe('Authentication error occurred'),
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Google authentication feature supporting OAuth2 browser flow and service account auth.
|
|
56
|
+
*
|
|
57
|
+
* Handles the complete OAuth2 lifecycle: authorization URL generation, local callback server,
|
|
58
|
+
* token exchange, refresh token storage (via diskCache), and automatic token refresh.
|
|
59
|
+
* Also supports non-interactive service account authentication via JSON key files.
|
|
60
|
+
*
|
|
61
|
+
* Other Google features (drive, sheets, calendar, docs) depend on this feature
|
|
62
|
+
* and access it lazily via `container.feature('googleAuth')`.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* // OAuth2 flow — opens browser for consent
|
|
67
|
+
* const auth = container.feature('googleAuth', {
|
|
68
|
+
* clientId: 'your-client-id.apps.googleusercontent.com',
|
|
69
|
+
* clientSecret: 'your-secret',
|
|
70
|
+
* scopes: ['https://www.googleapis.com/auth/drive.readonly'],
|
|
71
|
+
* })
|
|
72
|
+
* await auth.authorize()
|
|
73
|
+
*
|
|
74
|
+
* // Service account flow — no browser needed
|
|
75
|
+
* const auth = container.feature('googleAuth', {
|
|
76
|
+
* serviceAccountKeyPath: '/path/to/key.json',
|
|
77
|
+
* scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'],
|
|
78
|
+
* })
|
|
79
|
+
* await auth.authenticateServiceAccount()
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export class GoogleAuth extends Feature<GoogleAuthState, GoogleAuthOptions> {
|
|
83
|
+
static override shortcut = 'features.googleAuth' as const
|
|
84
|
+
static override envVars = [
|
|
85
|
+
'GOOGLE_CLIENT_ID',
|
|
86
|
+
'GOOGLE_CLIENT_SECRET',
|
|
87
|
+
'GOOGLE_SERVICE_ACCOUNT_KEY',
|
|
88
|
+
'GOOGLE_OAUTH_REDIRECT_PORT',
|
|
89
|
+
]
|
|
90
|
+
static override stateSchema = GoogleAuthStateSchema
|
|
91
|
+
static override optionsSchema = GoogleAuthOptionsSchema
|
|
92
|
+
static override eventsSchema = GoogleAuthEventsSchema
|
|
93
|
+
|
|
94
|
+
private _oauth2Client?: OAuth2Client
|
|
95
|
+
private _redirectUri?: string
|
|
96
|
+
|
|
97
|
+
override get initialState(): GoogleAuthState {
|
|
98
|
+
return {
|
|
99
|
+
...super.initialState,
|
|
100
|
+
authMode: 'none',
|
|
101
|
+
isAuthenticated: false,
|
|
102
|
+
scopes: [],
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** OAuth2 client ID from options or GOOGLE_CLIENT_ID env var. */
|
|
107
|
+
get clientId(): string {
|
|
108
|
+
const id = this.options.clientId || process.env.GOOGLE_CLIENT_ID
|
|
109
|
+
if (!id) throw new Error('Google client ID required. Set options.clientId or GOOGLE_CLIENT_ID env var.')
|
|
110
|
+
return id
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** OAuth2 client secret from options or GOOGLE_CLIENT_SECRET env var. */
|
|
114
|
+
get clientSecret(): string {
|
|
115
|
+
const secret = this.options.clientSecret || process.env.GOOGLE_CLIENT_SECRET
|
|
116
|
+
if (!secret) throw new Error('Google client secret required. Set options.clientSecret or GOOGLE_CLIENT_SECRET env var.')
|
|
117
|
+
return secret
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Resolved authentication mode based on options. */
|
|
121
|
+
get authMode(): 'oauth2' | 'service-account' {
|
|
122
|
+
if (this.options.mode) return this.options.mode
|
|
123
|
+
if (this.options.serviceAccountKeyPath || this.options.serviceAccountKey || process.env.GOOGLE_SERVICE_ACCOUNT_KEY) {
|
|
124
|
+
return 'service-account'
|
|
125
|
+
}
|
|
126
|
+
return 'oauth2'
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Whether valid credentials are currently available. */
|
|
130
|
+
get isAuthenticated(): boolean {
|
|
131
|
+
return this.state.get('isAuthenticated') || false
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Default scopes covering Drive, Sheets, Calendar, and Docs read access. */
|
|
135
|
+
get defaultScopes(): string[] {
|
|
136
|
+
return [
|
|
137
|
+
'https://www.googleapis.com/auth/drive.readonly',
|
|
138
|
+
'https://www.googleapis.com/auth/spreadsheets.readonly',
|
|
139
|
+
'https://www.googleapis.com/auth/calendar.readonly',
|
|
140
|
+
'https://www.googleapis.com/auth/documents.readonly',
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Resolved redirect port from options, GOOGLE_OAUTH_REDIRECT_PORT env var, or default 3000. */
|
|
145
|
+
get redirectPort(): number {
|
|
146
|
+
return this.options.redirectPort
|
|
147
|
+
|| (process.env.GOOGLE_OAUTH_REDIRECT_PORT ? parseInt(process.env.GOOGLE_OAUTH_REDIRECT_PORT, 10) : undefined)
|
|
148
|
+
|| 3000
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** DiskCache key used for storing the refresh token. */
|
|
152
|
+
get tokenCacheKey(): string {
|
|
153
|
+
return this.options.tokenCacheKey || `google-auth:refresh:${this.clientId}`
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get the OAuth2Client instance, creating it lazily.
|
|
158
|
+
* After authentication, this client has valid credentials set.
|
|
159
|
+
*
|
|
160
|
+
* @returns The OAuth2Client instance
|
|
161
|
+
*/
|
|
162
|
+
getOAuth2Client(): OAuth2Client {
|
|
163
|
+
if (this._oauth2Client) return this._oauth2Client
|
|
164
|
+
|
|
165
|
+
const redirectUri = this._redirectUri || `http://localhost:${this.redirectPort}/oauth2callback`
|
|
166
|
+
this._oauth2Client = new google.auth.OAuth2(this.clientId, this.clientSecret, redirectUri)
|
|
167
|
+
return this._oauth2Client
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get the authenticated auth client for passing to googleapis service constructors.
|
|
172
|
+
* Handles token refresh automatically for OAuth2. For service accounts, returns
|
|
173
|
+
* the JWT auth client.
|
|
174
|
+
*
|
|
175
|
+
* @returns An auth client suitable for `google.drive({ version: 'v3', auth })`
|
|
176
|
+
*/
|
|
177
|
+
async getAuthClient(): Promise<OAuth2Client | ReturnType<typeof google.auth.fromJSON>> {
|
|
178
|
+
if (!this.isAuthenticated) {
|
|
179
|
+
// Try restoring from cache first
|
|
180
|
+
const restored = await this.tryRestoreTokens()
|
|
181
|
+
if (!restored) {
|
|
182
|
+
throw new Error('Not authenticated. Call authorize() or authenticateServiceAccount() first.')
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (this.state.get('authMode') === 'service-account') {
|
|
187
|
+
const key = this.getServiceAccountKey()
|
|
188
|
+
const auth = google.auth.fromJSON(key) as any
|
|
189
|
+
auth.scopes = this.options.scopes || this.defaultScopes
|
|
190
|
+
return auth
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const client = this.getOAuth2Client()
|
|
194
|
+
|
|
195
|
+
// Check if token needs refresh
|
|
196
|
+
const expiry = this.state.get('tokenExpiry')
|
|
197
|
+
if (expiry && new Date(expiry).getTime() < Date.now() + 60_000) {
|
|
198
|
+
try {
|
|
199
|
+
const { credentials } = await client.refreshAccessToken()
|
|
200
|
+
client.setCredentials(credentials)
|
|
201
|
+
if (credentials.expiry_date) {
|
|
202
|
+
this.setState({ tokenExpiry: new Date(credentials.expiry_date).toISOString() })
|
|
203
|
+
}
|
|
204
|
+
this.emit('tokenRefreshed')
|
|
205
|
+
} catch (err: any) {
|
|
206
|
+
this.setState({ lastError: err.message, isAuthenticated: false })
|
|
207
|
+
this.emit('error', err)
|
|
208
|
+
throw err
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return client
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Start the OAuth2 authorization flow.
|
|
217
|
+
*
|
|
218
|
+
* 1. Spins up a temporary Express callback server on a free port
|
|
219
|
+
* 2. Generates the Google authorization URL
|
|
220
|
+
* 3. Opens the browser to the consent page
|
|
221
|
+
* 4. Waits for the callback with the authorization code
|
|
222
|
+
* 5. Exchanges the code for access + refresh tokens
|
|
223
|
+
* 6. Stores the refresh token in diskCache
|
|
224
|
+
* 7. Shuts down the callback server
|
|
225
|
+
*
|
|
226
|
+
* @param scopes - OAuth2 scopes to request (defaults to options.scopes or defaultScopes)
|
|
227
|
+
*/
|
|
228
|
+
async authorize(scopes?: string[]): Promise<this> {
|
|
229
|
+
const requestedScopes = scopes || this.options.scopes || this.defaultScopes
|
|
230
|
+
const port = this.redirectPort
|
|
231
|
+
const redirectUri = `http://localhost:${port}/oauth2callback`
|
|
232
|
+
this._redirectUri = redirectUri
|
|
233
|
+
|
|
234
|
+
const oauth2Client = new google.auth.OAuth2(this.clientId, this.clientSecret, redirectUri)
|
|
235
|
+
this._oauth2Client = oauth2Client
|
|
236
|
+
|
|
237
|
+
const authUrl = oauth2Client.generateAuthUrl({
|
|
238
|
+
access_type: 'offline',
|
|
239
|
+
scope: requestedScopes,
|
|
240
|
+
prompt: 'consent',
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
this.emit('authorizationRequired', authUrl)
|
|
244
|
+
|
|
245
|
+
// Create a promise that resolves when the callback is received
|
|
246
|
+
const codePromise = new Promise<string>((resolve, reject) => {
|
|
247
|
+
const server = Bun.serve({
|
|
248
|
+
port,
|
|
249
|
+
fetch(req) {
|
|
250
|
+
const url = new URL(req.url)
|
|
251
|
+
if (url.pathname === '/oauth2callback') {
|
|
252
|
+
const code = url.searchParams.get('code')
|
|
253
|
+
const error = url.searchParams.get('error')
|
|
254
|
+
|
|
255
|
+
if (error) {
|
|
256
|
+
reject(new Error(`OAuth2 authorization denied: ${error}`))
|
|
257
|
+
return new Response(
|
|
258
|
+
'<html><body><h2>Authorization denied.</h2><p>You can close this window.</p></body></html>',
|
|
259
|
+
{ headers: { 'Content-Type': 'text/html' } }
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (code) {
|
|
264
|
+
resolve(code)
|
|
265
|
+
return new Response(
|
|
266
|
+
'<html><body><h2>Authorization successful!</h2><p>You can close this window.</p></body></html>',
|
|
267
|
+
{ headers: { 'Content-Type': 'text/html' } }
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
reject(new Error('No authorization code received'))
|
|
272
|
+
return new Response('Missing code', { status: 400 })
|
|
273
|
+
}
|
|
274
|
+
return new Response('Not found', { status: 404 })
|
|
275
|
+
},
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
// Store server reference for cleanup
|
|
279
|
+
;(this as any)._callbackServer = server
|
|
280
|
+
|
|
281
|
+
// Timeout after 5 minutes
|
|
282
|
+
setTimeout(() => {
|
|
283
|
+
reject(new Error('OAuth2 authorization timed out (5 minutes)'))
|
|
284
|
+
}, 5 * 60 * 1000)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
// Open the browser
|
|
288
|
+
try {
|
|
289
|
+
const opener = this.container.feature('opener')
|
|
290
|
+
await opener.open(authUrl)
|
|
291
|
+
} catch {
|
|
292
|
+
// If opener fails, log the URL for manual opening
|
|
293
|
+
console.log(`\nOpen this URL in your browser to authorize:\n${authUrl}\n`)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const code = await codePromise
|
|
298
|
+
const { tokens } = await oauth2Client.getToken(code)
|
|
299
|
+
oauth2Client.setCredentials(tokens)
|
|
300
|
+
|
|
301
|
+
// Store refresh token
|
|
302
|
+
if (tokens.refresh_token) {
|
|
303
|
+
await this.storeRefreshToken(tokens.refresh_token)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Fetch user info
|
|
307
|
+
let email: string | undefined
|
|
308
|
+
try {
|
|
309
|
+
const oauth2 = google.oauth2({ version: 'v2', auth: oauth2Client })
|
|
310
|
+
const userInfo = await oauth2.userinfo.get()
|
|
311
|
+
email = userInfo.data.email || undefined
|
|
312
|
+
} catch {
|
|
313
|
+
// Non-critical — email is optional
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
this.setState({
|
|
317
|
+
authMode: 'oauth2',
|
|
318
|
+
isAuthenticated: true,
|
|
319
|
+
email,
|
|
320
|
+
scopes: requestedScopes,
|
|
321
|
+
tokenExpiry: tokens.expiry_date ? new Date(tokens.expiry_date).toISOString() : undefined,
|
|
322
|
+
lastError: undefined,
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
this.emit('authenticated', { mode: 'oauth2', email })
|
|
326
|
+
} catch (err: any) {
|
|
327
|
+
this.setState({ lastError: err.message })
|
|
328
|
+
this.emit('error', err)
|
|
329
|
+
throw err
|
|
330
|
+
} finally {
|
|
331
|
+
// Shut down callback server
|
|
332
|
+
const server = (this as any)._callbackServer
|
|
333
|
+
if (server) {
|
|
334
|
+
server.stop()
|
|
335
|
+
delete (this as any)._callbackServer
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return this
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Authenticate using a service account JSON key file.
|
|
344
|
+
* Reads the key from options.serviceAccountKeyPath, options.serviceAccountKey,
|
|
345
|
+
* or the GOOGLE_SERVICE_ACCOUNT_KEY env var.
|
|
346
|
+
*
|
|
347
|
+
* @returns This feature instance for chaining
|
|
348
|
+
*/
|
|
349
|
+
async authenticateServiceAccount(): Promise<this> {
|
|
350
|
+
try {
|
|
351
|
+
const key = this.getServiceAccountKey()
|
|
352
|
+
const scopes = this.options.scopes || this.defaultScopes
|
|
353
|
+
const auth = google.auth.fromJSON(key) as any
|
|
354
|
+
auth.scopes = scopes
|
|
355
|
+
|
|
356
|
+
// Test the auth by getting an access token
|
|
357
|
+
const token = await auth.getAccessToken()
|
|
358
|
+
if (!token) {
|
|
359
|
+
throw new Error('Failed to obtain access token from service account')
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
this.setState({
|
|
363
|
+
authMode: 'service-account',
|
|
364
|
+
isAuthenticated: true,
|
|
365
|
+
email: key.client_email,
|
|
366
|
+
scopes,
|
|
367
|
+
lastError: undefined,
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
this.emit('authenticated', { mode: 'service-account', email: key.client_email })
|
|
371
|
+
} catch (err: any) {
|
|
372
|
+
this.setState({ lastError: err.message })
|
|
373
|
+
this.emit('error', err)
|
|
374
|
+
throw err
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return this
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Attempt to restore authentication from a cached refresh token.
|
|
382
|
+
* Called automatically by getAuthClient() if not yet authenticated.
|
|
383
|
+
*
|
|
384
|
+
* @returns true if tokens were restored successfully
|
|
385
|
+
*/
|
|
386
|
+
async tryRestoreTokens(): Promise<boolean> {
|
|
387
|
+
if (this.authMode === 'service-account') {
|
|
388
|
+
try {
|
|
389
|
+
await this.authenticateServiceAccount()
|
|
390
|
+
return true
|
|
391
|
+
} catch {
|
|
392
|
+
return false
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
const refreshToken = await this.loadRefreshToken()
|
|
398
|
+
if (!refreshToken) return false
|
|
399
|
+
|
|
400
|
+
const oauth2Client = this.getOAuth2Client()
|
|
401
|
+
oauth2Client.setCredentials({ refresh_token: refreshToken })
|
|
402
|
+
|
|
403
|
+
const { credentials } = await oauth2Client.refreshAccessToken()
|
|
404
|
+
oauth2Client.setCredentials(credentials)
|
|
405
|
+
|
|
406
|
+
let email: string | undefined
|
|
407
|
+
try {
|
|
408
|
+
const oauth2 = google.oauth2({ version: 'v2', auth: oauth2Client })
|
|
409
|
+
const userInfo = await oauth2.userinfo.get()
|
|
410
|
+
email = userInfo.data.email || undefined
|
|
411
|
+
} catch {
|
|
412
|
+
// Non-critical
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
this.setState({
|
|
416
|
+
authMode: 'oauth2',
|
|
417
|
+
isAuthenticated: true,
|
|
418
|
+
email,
|
|
419
|
+
tokenExpiry: credentials.expiry_date ? new Date(credentials.expiry_date).toISOString() : undefined,
|
|
420
|
+
lastError: undefined,
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
this.emit('authenticated', { mode: 'oauth2', email })
|
|
424
|
+
return true
|
|
425
|
+
} catch {
|
|
426
|
+
return false
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Revoke the current credentials and clear cached tokens.
|
|
432
|
+
*
|
|
433
|
+
* @returns This feature instance for chaining
|
|
434
|
+
*/
|
|
435
|
+
async revoke(): Promise<this> {
|
|
436
|
+
try {
|
|
437
|
+
if (this.state.get('authMode') === 'oauth2' && this._oauth2Client) {
|
|
438
|
+
await this._oauth2Client.revokeCredentials()
|
|
439
|
+
}
|
|
440
|
+
} catch {
|
|
441
|
+
// Best effort revocation
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Clear cached refresh token
|
|
445
|
+
try {
|
|
446
|
+
const cache = this.container.feature('diskCache')
|
|
447
|
+
await cache.rm(this.tokenCacheKey)
|
|
448
|
+
} catch {
|
|
449
|
+
// Cache may not exist
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
this._oauth2Client = undefined
|
|
453
|
+
this.setState({
|
|
454
|
+
authMode: 'none',
|
|
455
|
+
isAuthenticated: false,
|
|
456
|
+
email: undefined,
|
|
457
|
+
scopes: [],
|
|
458
|
+
tokenExpiry: undefined,
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
return this
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** Store a refresh token in diskCache. */
|
|
465
|
+
private async storeRefreshToken(token: string): Promise<void> {
|
|
466
|
+
const cache = this.container.feature('diskCache')
|
|
467
|
+
await cache.set(this.tokenCacheKey, token)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Load a refresh token from diskCache. */
|
|
471
|
+
private async loadRefreshToken(): Promise<string | null> {
|
|
472
|
+
try {
|
|
473
|
+
const cache = this.container.feature('diskCache')
|
|
474
|
+
const exists = await cache.has(this.tokenCacheKey)
|
|
475
|
+
if (!exists) return null
|
|
476
|
+
return await cache.get(this.tokenCacheKey)
|
|
477
|
+
} catch {
|
|
478
|
+
return null
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Resolve the service account key from options or env var. */
|
|
483
|
+
private getServiceAccountKey(): any {
|
|
484
|
+
if (this.options.serviceAccountKey) return this.options.serviceAccountKey
|
|
485
|
+
|
|
486
|
+
const keyPath = this.options.serviceAccountKeyPath || process.env.GOOGLE_SERVICE_ACCOUNT_KEY
|
|
487
|
+
if (!keyPath) {
|
|
488
|
+
throw new Error('Service account key required. Set options.serviceAccountKeyPath, options.serviceAccountKey, or GOOGLE_SERVICE_ACCOUNT_KEY env var.')
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const resolved = this.container.paths.resolve(keyPath)
|
|
492
|
+
return this.container.fs.readJson(resolved)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
declare module '../../feature' {
|
|
497
|
+
interface AvailableFeatures {
|
|
498
|
+
googleAuth: typeof GoogleAuth
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export default features.register('googleAuth', GoogleAuth)
|