@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.
Files changed (358) hide show
  1. package/CLAUDE.md +71 -0
  2. package/README.md +78 -0
  3. package/bun.lock +2928 -0
  4. package/bunfig.toml +3 -0
  5. package/commands/audit-docs.ts +740 -0
  6. package/commands/build-scaffolds.ts +154 -0
  7. package/commands/generate-api-docs.ts +114 -0
  8. package/commands/update-introspection.ts +67 -0
  9. package/docs/CLI.md +335 -0
  10. package/docs/README.md +88 -0
  11. package/docs/TABLE-OF-CONTENTS.md +157 -0
  12. package/docs/apis/clients/elevenlabs.md +84 -0
  13. package/docs/apis/clients/graph.md +56 -0
  14. package/docs/apis/clients/openai.md +69 -0
  15. package/docs/apis/clients/rest.md +41 -0
  16. package/docs/apis/clients/websocket.md +107 -0
  17. package/docs/apis/features/agi/assistant.md +471 -0
  18. package/docs/apis/features/agi/assistants-manager.md +154 -0
  19. package/docs/apis/features/agi/claude-code.md +602 -0
  20. package/docs/apis/features/agi/conversation-history.md +352 -0
  21. package/docs/apis/features/agi/conversation.md +333 -0
  22. package/docs/apis/features/agi/docs-reader.md +121 -0
  23. package/docs/apis/features/agi/openai-codex.md +318 -0
  24. package/docs/apis/features/agi/openapi.md +138 -0
  25. package/docs/apis/features/agi/semantic-search.md +387 -0
  26. package/docs/apis/features/agi/skills-library.md +216 -0
  27. package/docs/apis/features/node/container-link.md +133 -0
  28. package/docs/apis/features/node/content-db.md +313 -0
  29. package/docs/apis/features/node/disk-cache.md +379 -0
  30. package/docs/apis/features/node/dns.md +651 -0
  31. package/docs/apis/features/node/docker.md +705 -0
  32. package/docs/apis/features/node/downloader.md +81 -0
  33. package/docs/apis/features/node/esbuild.md +59 -0
  34. package/docs/apis/features/node/file-manager.md +182 -0
  35. package/docs/apis/features/node/fs.md +581 -0
  36. package/docs/apis/features/node/git.md +330 -0
  37. package/docs/apis/features/node/google-auth.md +174 -0
  38. package/docs/apis/features/node/google-calendar.md +187 -0
  39. package/docs/apis/features/node/google-docs.md +151 -0
  40. package/docs/apis/features/node/google-drive.md +225 -0
  41. package/docs/apis/features/node/google-sheets.md +179 -0
  42. package/docs/apis/features/node/grep.md +290 -0
  43. package/docs/apis/features/node/helpers.md +135 -0
  44. package/docs/apis/features/node/ink.md +334 -0
  45. package/docs/apis/features/node/ipc-socket.md +260 -0
  46. package/docs/apis/features/node/json-tree.md +86 -0
  47. package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
  48. package/docs/apis/features/node/networking.md +281 -0
  49. package/docs/apis/features/node/nlp.md +133 -0
  50. package/docs/apis/features/node/opener.md +97 -0
  51. package/docs/apis/features/node/os.md +118 -0
  52. package/docs/apis/features/node/package-finder.md +402 -0
  53. package/docs/apis/features/node/postgres.md +212 -0
  54. package/docs/apis/features/node/proc.md +430 -0
  55. package/docs/apis/features/node/process-manager.md +210 -0
  56. package/docs/apis/features/node/python.md +278 -0
  57. package/docs/apis/features/node/repl.md +88 -0
  58. package/docs/apis/features/node/runpod.md +673 -0
  59. package/docs/apis/features/node/secure-shell.md +169 -0
  60. package/docs/apis/features/node/semantic-search.md +401 -0
  61. package/docs/apis/features/node/sqlite.md +211 -0
  62. package/docs/apis/features/node/telegram.md +254 -0
  63. package/docs/apis/features/node/tts.md +118 -0
  64. package/docs/apis/features/node/ui.md +703 -0
  65. package/docs/apis/features/node/vault.md +64 -0
  66. package/docs/apis/features/node/vm.md +84 -0
  67. package/docs/apis/features/node/window-manager.md +337 -0
  68. package/docs/apis/features/node/yaml-tree.md +85 -0
  69. package/docs/apis/features/node/yaml.md +176 -0
  70. package/docs/apis/features/web/asset-loader.md +47 -0
  71. package/docs/apis/features/web/container-link.md +133 -0
  72. package/docs/apis/features/web/esbuild.md +59 -0
  73. package/docs/apis/features/web/helpers.md +135 -0
  74. package/docs/apis/features/web/network.md +30 -0
  75. package/docs/apis/features/web/speech.md +55 -0
  76. package/docs/apis/features/web/vault.md +64 -0
  77. package/docs/apis/features/web/vm.md +84 -0
  78. package/docs/apis/features/web/voice.md +67 -0
  79. package/docs/apis/servers/express.md +127 -0
  80. package/docs/apis/servers/mcp.md +213 -0
  81. package/docs/apis/servers/websocket.md +99 -0
  82. package/docs/documentation-audit.md +134 -0
  83. package/docs/examples/content-db.md +77 -0
  84. package/docs/examples/disk-cache.md +83 -0
  85. package/docs/examples/docker.md +101 -0
  86. package/docs/examples/downloader.md +70 -0
  87. package/docs/examples/esbuild.md +80 -0
  88. package/docs/examples/file-manager.md +82 -0
  89. package/docs/examples/fs.md +83 -0
  90. package/docs/examples/git.md +85 -0
  91. package/docs/examples/google-auth.md +88 -0
  92. package/docs/examples/google-calendar.md +94 -0
  93. package/docs/examples/google-docs.md +82 -0
  94. package/docs/examples/google-drive.md +96 -0
  95. package/docs/examples/google-sheets.md +95 -0
  96. package/docs/examples/grep.md +85 -0
  97. package/docs/examples/ink-blocks.md +75 -0
  98. package/docs/examples/ink-renderer.md +41 -0
  99. package/docs/examples/ink.md +103 -0
  100. package/docs/examples/ipc-socket.md +103 -0
  101. package/docs/examples/json-tree.md +91 -0
  102. package/docs/examples/launcher-app-command-listener.md +120 -0
  103. package/docs/examples/networking.md +58 -0
  104. package/docs/examples/nlp.md +91 -0
  105. package/docs/examples/opener.md +78 -0
  106. package/docs/examples/os.md +72 -0
  107. package/docs/examples/package-finder.md +89 -0
  108. package/docs/examples/port-exposer.md +89 -0
  109. package/docs/examples/postgres.md +91 -0
  110. package/docs/examples/proc.md +81 -0
  111. package/docs/examples/process-manager.md +79 -0
  112. package/docs/examples/python.md +91 -0
  113. package/docs/examples/repl.md +93 -0
  114. package/docs/examples/runpod.md +119 -0
  115. package/docs/examples/secure-shell.md +92 -0
  116. package/docs/examples/sqlite.md +86 -0
  117. package/docs/examples/telegram.md +77 -0
  118. package/docs/examples/tts.md +86 -0
  119. package/docs/examples/ui.md +80 -0
  120. package/docs/examples/vault.md +70 -0
  121. package/docs/examples/vm.md +86 -0
  122. package/docs/examples/window-manager.md +125 -0
  123. package/docs/examples/yaml-tree.md +93 -0
  124. package/docs/examples/yaml.md +104 -0
  125. package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
  126. package/docs/ideas/container-use-api.md +9 -0
  127. package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
  128. package/docs/ideas/feature-stacks.md +22 -0
  129. package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
  130. package/docs/ideas/mcp-design.md +9 -0
  131. package/docs/ideas/web-container-debugging-feature.md +13 -0
  132. package/docs/introspection-audit.md +49 -0
  133. package/docs/introspection.md +154 -0
  134. package/docs/mcp/readme.md +162 -0
  135. package/docs/models.ts +38 -0
  136. package/docs/philosophy.md +85 -0
  137. package/docs/principles.md +7 -0
  138. package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
  139. package/docs/prompts/mcp-test-easy-command.md +27 -0
  140. package/docs/reports/assistant-bugs.md +38 -0
  141. package/docs/reports/attach-pattern-usage.md +18 -0
  142. package/docs/reports/code-audit-results.md +391 -0
  143. package/docs/reports/introspection-audit-tasks.md +378 -0
  144. package/docs/reports/luca-mcp-improvements.md +128 -0
  145. package/docs/scaffolds/client.md +140 -0
  146. package/docs/scaffolds/command.md +106 -0
  147. package/docs/scaffolds/endpoint.md +176 -0
  148. package/docs/scaffolds/feature.md +148 -0
  149. package/docs/scaffolds/server.md +187 -0
  150. package/docs/tasks/web-container-helper-discovery.md +71 -0
  151. package/docs/todos.md +1 -0
  152. package/docs/tutorials/01-getting-started.md +106 -0
  153. package/docs/tutorials/02-container.md +210 -0
  154. package/docs/tutorials/03-scripts.md +194 -0
  155. package/docs/tutorials/04-features-overview.md +196 -0
  156. package/docs/tutorials/05-state-and-events.md +171 -0
  157. package/docs/tutorials/06-servers.md +157 -0
  158. package/docs/tutorials/07-endpoints.md +198 -0
  159. package/docs/tutorials/08-commands.md +171 -0
  160. package/docs/tutorials/09-clients.md +162 -0
  161. package/docs/tutorials/10-creating-features.md +198 -0
  162. package/docs/tutorials/11-contentbase.md +191 -0
  163. package/docs/tutorials/12-assistants.md +215 -0
  164. package/docs/tutorials/13-introspection.md +147 -0
  165. package/docs/tutorials/14-type-system.md +174 -0
  166. package/docs/tutorials/15-project-patterns.md +222 -0
  167. package/docs/tutorials/16-google-features.md +534 -0
  168. package/docs/tutorials/17-tui-blocks.md +530 -0
  169. package/docs/tutorials/18-semantic-search.md +334 -0
  170. package/index.ts +1 -0
  171. package/luca.console.ts +9 -0
  172. package/main.py +6 -0
  173. package/package.json +154 -0
  174. package/pyproject.toml +7 -0
  175. package/scripts/animations/chrome-glitch.ts +55 -0
  176. package/scripts/animations/index.ts +16 -0
  177. package/scripts/animations/neon-pulse.ts +64 -0
  178. package/scripts/animations/types.ts +6 -0
  179. package/scripts/build-web.ts +28 -0
  180. package/scripts/examples/ask-luca-expert.ts +42 -0
  181. package/scripts/examples/assistant-questions.ts +12 -0
  182. package/scripts/examples/excalidraw-expert.ts +75 -0
  183. package/scripts/examples/expert-chat.ts +0 -0
  184. package/scripts/examples/file-manager.ts +14 -0
  185. package/scripts/examples/ideas.ts +12 -0
  186. package/scripts/examples/interactive-chat.ts +20 -0
  187. package/scripts/examples/openai-tool-calls.ts +113 -0
  188. package/scripts/examples/opening-a-web-browser.ts +5 -0
  189. package/scripts/examples/telegram-bot.ts +79 -0
  190. package/scripts/examples/telegram-ink-ui.ts +302 -0
  191. package/scripts/examples/using-assistant-with-mcp.ts +560 -0
  192. package/scripts/examples/using-claude-code.ts +10 -0
  193. package/scripts/examples/using-contentdb.ts +35 -0
  194. package/scripts/examples/using-conversations.ts +35 -0
  195. package/scripts/examples/using-disk-cache.ts +10 -0
  196. package/scripts/examples/using-docker-shell.ts +75 -0
  197. package/scripts/examples/using-elevenlabs.ts +25 -0
  198. package/scripts/examples/using-google-calendar.ts +57 -0
  199. package/scripts/examples/using-google-docs.ts +74 -0
  200. package/scripts/examples/using-google-drive.ts +74 -0
  201. package/scripts/examples/using-google-sheets.ts +89 -0
  202. package/scripts/examples/using-nlp.ts +55 -0
  203. package/scripts/examples/using-ollama.ts +10 -0
  204. package/scripts/examples/using-openai-codex.ts +23 -0
  205. package/scripts/examples/using-postgres.ts +55 -0
  206. package/scripts/examples/using-runpod.ts +32 -0
  207. package/scripts/examples/using-tts.ts +40 -0
  208. package/scripts/examples/vm-loading-esm-modules.ts +16 -0
  209. package/scripts/scaffold.ts +391 -0
  210. package/scripts/scratch.ts +15 -0
  211. package/scripts/test-command-listener.ts +123 -0
  212. package/scripts/test-window-manager-lifecycle.ts +86 -0
  213. package/scripts/test-window-manager.ts +43 -0
  214. package/scripts/update-introspection-data.ts +58 -0
  215. package/src/agi/README.md +14 -0
  216. package/src/agi/container.server.ts +114 -0
  217. package/src/agi/endpoints/ask.ts +60 -0
  218. package/src/agi/endpoints/conversations/[id].ts +45 -0
  219. package/src/agi/endpoints/conversations.ts +31 -0
  220. package/src/agi/endpoints/experts.ts +37 -0
  221. package/src/agi/features/assistant.ts +767 -0
  222. package/src/agi/features/assistants-manager.ts +260 -0
  223. package/src/agi/features/claude-code.ts +1111 -0
  224. package/src/agi/features/conversation-history.ts +497 -0
  225. package/src/agi/features/conversation.ts +799 -0
  226. package/src/agi/features/openai-codex.ts +631 -0
  227. package/src/agi/features/openapi.ts +438 -0
  228. package/src/agi/features/skills-library.ts +425 -0
  229. package/src/agi/index.ts +6 -0
  230. package/src/agi/lib/token-counter.ts +122 -0
  231. package/src/browser.ts +25 -0
  232. package/src/bus.ts +100 -0
  233. package/src/cli/cli.ts +70 -0
  234. package/src/client.ts +461 -0
  235. package/src/clients/civitai/index.ts +541 -0
  236. package/src/clients/client-template.ts +41 -0
  237. package/src/clients/comfyui/index.ts +597 -0
  238. package/src/clients/elevenlabs/index.ts +291 -0
  239. package/src/clients/openai/index.ts +451 -0
  240. package/src/clients/supabase/index.ts +366 -0
  241. package/src/command.ts +164 -0
  242. package/src/commands/chat.ts +182 -0
  243. package/src/commands/console.ts +192 -0
  244. package/src/commands/describe.ts +433 -0
  245. package/src/commands/eval.ts +116 -0
  246. package/src/commands/help.ts +214 -0
  247. package/src/commands/index.ts +14 -0
  248. package/src/commands/mcp.ts +64 -0
  249. package/src/commands/prompt.ts +807 -0
  250. package/src/commands/run.ts +257 -0
  251. package/src/commands/sandbox-mcp.ts +439 -0
  252. package/src/commands/scaffold.ts +79 -0
  253. package/src/commands/serve.ts +172 -0
  254. package/src/container.ts +781 -0
  255. package/src/endpoint.ts +340 -0
  256. package/src/feature.ts +75 -0
  257. package/src/hash-object.ts +97 -0
  258. package/src/helper.ts +543 -0
  259. package/src/introspection/generated.agi.ts +23388 -0
  260. package/src/introspection/generated.node.ts +18899 -0
  261. package/src/introspection/generated.web.ts +2021 -0
  262. package/src/introspection/index.ts +256 -0
  263. package/src/introspection/scan.ts +912 -0
  264. package/src/node/container.ts +354 -0
  265. package/src/node/feature.ts +13 -0
  266. package/src/node/features/container-link.ts +558 -0
  267. package/src/node/features/content-db.ts +475 -0
  268. package/src/node/features/disk-cache.ts +382 -0
  269. package/src/node/features/dns.ts +655 -0
  270. package/src/node/features/docker.ts +912 -0
  271. package/src/node/features/downloader.ts +92 -0
  272. package/src/node/features/esbuild.ts +68 -0
  273. package/src/node/features/file-manager.ts +357 -0
  274. package/src/node/features/fs.ts +534 -0
  275. package/src/node/features/git.ts +492 -0
  276. package/src/node/features/google-auth.ts +502 -0
  277. package/src/node/features/google-calendar.ts +300 -0
  278. package/src/node/features/google-docs.ts +404 -0
  279. package/src/node/features/google-drive.ts +339 -0
  280. package/src/node/features/google-sheets.ts +279 -0
  281. package/src/node/features/grep.ts +406 -0
  282. package/src/node/features/helpers.ts +374 -0
  283. package/src/node/features/ink.ts +490 -0
  284. package/src/node/features/ipc-socket.ts +459 -0
  285. package/src/node/features/json-tree.ts +188 -0
  286. package/src/node/features/launcher-app-command-listener.ts +388 -0
  287. package/src/node/features/networking.ts +925 -0
  288. package/src/node/features/nlp.ts +211 -0
  289. package/src/node/features/opener.ts +166 -0
  290. package/src/node/features/os.ts +157 -0
  291. package/src/node/features/package-finder.ts +539 -0
  292. package/src/node/features/port-exposer.ts +342 -0
  293. package/src/node/features/postgres.ts +273 -0
  294. package/src/node/features/proc.ts +502 -0
  295. package/src/node/features/process-manager.ts +542 -0
  296. package/src/node/features/python.ts +444 -0
  297. package/src/node/features/repl.ts +194 -0
  298. package/src/node/features/runpod.ts +802 -0
  299. package/src/node/features/secure-shell.ts +248 -0
  300. package/src/node/features/semantic-search.ts +924 -0
  301. package/src/node/features/sqlite.ts +289 -0
  302. package/src/node/features/telegram.ts +342 -0
  303. package/src/node/features/tts.ts +184 -0
  304. package/src/node/features/ui.ts +857 -0
  305. package/src/node/features/vault.ts +164 -0
  306. package/src/node/features/vm.ts +312 -0
  307. package/src/node/features/window-manager.ts +804 -0
  308. package/src/node/features/yaml-tree.ts +149 -0
  309. package/src/node/features/yaml.ts +132 -0
  310. package/src/node.ts +70 -0
  311. package/src/react/index.ts +175 -0
  312. package/src/registry.ts +199 -0
  313. package/src/scaffolds/generated.ts +1613 -0
  314. package/src/scaffolds/template.ts +37 -0
  315. package/src/schemas/base.ts +255 -0
  316. package/src/server.ts +135 -0
  317. package/src/servers/express.ts +209 -0
  318. package/src/servers/mcp.ts +805 -0
  319. package/src/servers/socket.ts +120 -0
  320. package/src/state.ts +101 -0
  321. package/src/web/clients/socket.ts +82 -0
  322. package/src/web/container.ts +74 -0
  323. package/src/web/extension.ts +30 -0
  324. package/src/web/feature.ts +12 -0
  325. package/src/web/features/asset-loader.ts +64 -0
  326. package/src/web/features/container-link.ts +385 -0
  327. package/src/web/features/esbuild.ts +79 -0
  328. package/src/web/features/helpers.ts +267 -0
  329. package/src/web/features/network.ts +61 -0
  330. package/src/web/features/speech.ts +87 -0
  331. package/src/web/features/vault.ts +189 -0
  332. package/src/web/features/vm.ts +78 -0
  333. package/src/web/features/voice-recognition.ts +129 -0
  334. package/src/web/shims/isomorphic-vm.ts +149 -0
  335. package/test/bus.test.ts +134 -0
  336. package/test/clients-servers.test.ts +216 -0
  337. package/test/container-link.test.ts +274 -0
  338. package/test/features.test.ts +160 -0
  339. package/test/integration.test.ts +787 -0
  340. package/test/node-container.test.ts +121 -0
  341. package/test/rate-limit.test.ts +272 -0
  342. package/test/semantic-search.test.ts +550 -0
  343. package/test/state.test.ts +121 -0
  344. package/test-integration/assistant.test.ts +138 -0
  345. package/test-integration/assistants-manager.test.ts +123 -0
  346. package/test-integration/claude-code.test.ts +98 -0
  347. package/test-integration/conversation-history.test.ts +205 -0
  348. package/test-integration/conversation.test.ts +137 -0
  349. package/test-integration/elevenlabs.test.ts +55 -0
  350. package/test-integration/google-services.test.ts +80 -0
  351. package/test-integration/helpers.ts +89 -0
  352. package/test-integration/openai-codex.test.ts +93 -0
  353. package/test-integration/runpod.test.ts +58 -0
  354. package/test-integration/server-endpoints.test.ts +97 -0
  355. package/test-integration/skills-library.test.ts +157 -0
  356. package/test-integration/telegram.test.ts +46 -0
  357. package/tsconfig.json +58 -0
  358. 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)