@build-astron-co/nimbus 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +628 -0
- package/bin/nimbus +38 -0
- package/package.json +80 -0
- package/src/__tests__/app.test.ts +76 -0
- package/src/__tests__/audit.test.ts +877 -0
- package/src/__tests__/circuit-breaker.test.ts +116 -0
- package/src/__tests__/cli-run.test.ts +115 -0
- package/src/__tests__/context-manager.test.ts +502 -0
- package/src/__tests__/context.test.ts +242 -0
- package/src/__tests__/enterprise.test.ts +401 -0
- package/src/__tests__/generator.test.ts +433 -0
- package/src/__tests__/hooks.test.ts +582 -0
- package/src/__tests__/init.test.ts +436 -0
- package/src/__tests__/intent-parser.test.ts +229 -0
- package/src/__tests__/llm-router.test.ts +209 -0
- package/src/__tests__/lsp.test.ts +293 -0
- package/src/__tests__/modes.test.ts +336 -0
- package/src/__tests__/permissions.test.ts +338 -0
- package/src/__tests__/serve.test.ts +275 -0
- package/src/__tests__/sessions.test.ts +227 -0
- package/src/__tests__/sharing.test.ts +288 -0
- package/src/__tests__/snapshots.test.ts +581 -0
- package/src/__tests__/state-db.test.ts +334 -0
- package/src/__tests__/stream-with-tools.test.ts +732 -0
- package/src/__tests__/subagents.test.ts +176 -0
- package/src/__tests__/system-prompt.test.ts +169 -0
- package/src/__tests__/tool-converter.test.ts +256 -0
- package/src/__tests__/tool-schemas.test.ts +397 -0
- package/src/__tests__/tools.test.ts +143 -0
- package/src/__tests__/version.test.ts +49 -0
- package/src/agent/compaction-agent.ts +227 -0
- package/src/agent/context-manager.ts +435 -0
- package/src/agent/context.ts +427 -0
- package/src/agent/deploy-preview.ts +426 -0
- package/src/agent/index.ts +68 -0
- package/src/agent/loop.ts +717 -0
- package/src/agent/modes.ts +429 -0
- package/src/agent/permissions.ts +466 -0
- package/src/agent/subagents/base.ts +116 -0
- package/src/agent/subagents/cost.ts +51 -0
- package/src/agent/subagents/explore.ts +42 -0
- package/src/agent/subagents/general.ts +54 -0
- package/src/agent/subagents/index.ts +102 -0
- package/src/agent/subagents/infra.ts +59 -0
- package/src/agent/subagents/security.ts +69 -0
- package/src/agent/system-prompt.ts +436 -0
- package/src/app.ts +122 -0
- package/src/audit/activity-log.ts +290 -0
- package/src/audit/compliance-checker.ts +540 -0
- package/src/audit/cost-tracker.ts +318 -0
- package/src/audit/index.ts +23 -0
- package/src/audit/security-scanner.ts +596 -0
- package/src/auth/guard.ts +75 -0
- package/src/auth/index.ts +56 -0
- package/src/auth/oauth.ts +455 -0
- package/src/auth/providers.ts +470 -0
- package/src/auth/sso.ts +113 -0
- package/src/auth/store.ts +505 -0
- package/src/auth/types.ts +187 -0
- package/src/build.ts +141 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/init.ts +854 -0
- package/src/cli/openapi-spec.ts +356 -0
- package/src/cli/run.ts +237 -0
- package/src/cli/serve-auth.ts +80 -0
- package/src/cli/serve.ts +462 -0
- package/src/cli/web.ts +67 -0
- package/src/cli.ts +1417 -0
- package/src/clients/core-engine-client.ts +227 -0
- package/src/clients/enterprise-client.ts +334 -0
- package/src/clients/generator-client.ts +351 -0
- package/src/clients/git-client.ts +627 -0
- package/src/clients/github-client.ts +410 -0
- package/src/clients/helm-client.ts +504 -0
- package/src/clients/index.ts +80 -0
- package/src/clients/k8s-client.ts +497 -0
- package/src/clients/llm-client.ts +161 -0
- package/src/clients/rest-client.ts +130 -0
- package/src/clients/service-discovery.ts +33 -0
- package/src/clients/terraform-client.ts +482 -0
- package/src/clients/tools-client.ts +1843 -0
- package/src/clients/ws-client.ts +115 -0
- package/src/commands/analyze/index.ts +352 -0
- package/src/commands/apply/helm.ts +473 -0
- package/src/commands/apply/index.ts +213 -0
- package/src/commands/apply/k8s.ts +454 -0
- package/src/commands/apply/terraform.ts +582 -0
- package/src/commands/ask.ts +167 -0
- package/src/commands/audit/index.ts +238 -0
- package/src/commands/auth-cloud.ts +294 -0
- package/src/commands/auth-list.ts +134 -0
- package/src/commands/auth-profile.ts +121 -0
- package/src/commands/auth-status.ts +141 -0
- package/src/commands/aws/ec2.ts +501 -0
- package/src/commands/aws/iam.ts +397 -0
- package/src/commands/aws/index.ts +133 -0
- package/src/commands/aws/lambda.ts +396 -0
- package/src/commands/aws/rds.ts +439 -0
- package/src/commands/aws/s3.ts +439 -0
- package/src/commands/aws/vpc.ts +393 -0
- package/src/commands/aws-discover.ts +649 -0
- package/src/commands/aws-terraform.ts +805 -0
- package/src/commands/azure/aks.ts +376 -0
- package/src/commands/azure/functions.ts +253 -0
- package/src/commands/azure/index.ts +116 -0
- package/src/commands/azure/storage.ts +478 -0
- package/src/commands/azure/vm.ts +355 -0
- package/src/commands/billing/index.ts +256 -0
- package/src/commands/chat.ts +314 -0
- package/src/commands/config.ts +346 -0
- package/src/commands/cost/cloud-cost-estimator.ts +266 -0
- package/src/commands/cost/estimator.ts +79 -0
- package/src/commands/cost/index.ts +594 -0
- package/src/commands/cost/parsers/terraform.ts +273 -0
- package/src/commands/cost/parsers/types.ts +25 -0
- package/src/commands/cost/pricing/aws.ts +544 -0
- package/src/commands/cost/pricing/azure.ts +499 -0
- package/src/commands/cost/pricing/gcp.ts +396 -0
- package/src/commands/cost/pricing/index.ts +40 -0
- package/src/commands/demo.ts +250 -0
- package/src/commands/doctor.ts +794 -0
- package/src/commands/drift/index.ts +439 -0
- package/src/commands/explain.ts +277 -0
- package/src/commands/feedback.ts +389 -0
- package/src/commands/fix.ts +324 -0
- package/src/commands/fs/index.ts +402 -0
- package/src/commands/gcp/compute.ts +325 -0
- package/src/commands/gcp/functions.ts +271 -0
- package/src/commands/gcp/gke.ts +438 -0
- package/src/commands/gcp/iam.ts +344 -0
- package/src/commands/gcp/index.ts +129 -0
- package/src/commands/gcp/storage.ts +284 -0
- package/src/commands/generate-helm.ts +1249 -0
- package/src/commands/generate-k8s.ts +1560 -0
- package/src/commands/generate-terraform.ts +1460 -0
- package/src/commands/gh/index.ts +863 -0
- package/src/commands/git/index.ts +1343 -0
- package/src/commands/helm/index.ts +1126 -0
- package/src/commands/help.ts +539 -0
- package/src/commands/history.ts +142 -0
- package/src/commands/import.ts +868 -0
- package/src/commands/index.ts +367 -0
- package/src/commands/init.ts +1046 -0
- package/src/commands/k8s/index.ts +1137 -0
- package/src/commands/login.ts +631 -0
- package/src/commands/logout.ts +83 -0
- package/src/commands/onboarding.ts +228 -0
- package/src/commands/plan/display.ts +279 -0
- package/src/commands/plan/index.ts +599 -0
- package/src/commands/preview.ts +452 -0
- package/src/commands/questionnaire.ts +1270 -0
- package/src/commands/resume.ts +55 -0
- package/src/commands/team/index.ts +346 -0
- package/src/commands/template.ts +232 -0
- package/src/commands/tf/index.ts +1034 -0
- package/src/commands/upgrade.ts +550 -0
- package/src/commands/usage/index.ts +134 -0
- package/src/commands/version.ts +170 -0
- package/src/compat/index.ts +2 -0
- package/src/compat/runtime.ts +12 -0
- package/src/compat/sqlite.ts +107 -0
- package/src/config/index.ts +17 -0
- package/src/config/manager.ts +530 -0
- package/src/config/safety-policy.ts +358 -0
- package/src/config/schema.ts +125 -0
- package/src/config/types.ts +527 -0
- package/src/context/context-db.ts +199 -0
- package/src/demo/index.ts +349 -0
- package/src/demo/scenarios/full-journey.ts +229 -0
- package/src/demo/scenarios/getting-started.ts +127 -0
- package/src/demo/scenarios/helm-release.ts +341 -0
- package/src/demo/scenarios/k8s-deployment.ts +194 -0
- package/src/demo/scenarios/terraform-vpc.ts +170 -0
- package/src/demo/types.ts +92 -0
- package/src/engine/cost-estimator.ts +438 -0
- package/src/engine/diagram-generator.ts +256 -0
- package/src/engine/drift-detector.ts +902 -0
- package/src/engine/executor.ts +1035 -0
- package/src/engine/index.ts +76 -0
- package/src/engine/orchestrator.ts +636 -0
- package/src/engine/planner.ts +720 -0
- package/src/engine/safety.ts +743 -0
- package/src/engine/verifier.ts +770 -0
- package/src/enterprise/audit.ts +348 -0
- package/src/enterprise/auth.ts +270 -0
- package/src/enterprise/billing.ts +822 -0
- package/src/enterprise/index.ts +17 -0
- package/src/enterprise/teams.ts +443 -0
- package/src/generator/best-practices.ts +1608 -0
- package/src/generator/helm.ts +630 -0
- package/src/generator/index.ts +37 -0
- package/src/generator/intent-parser.ts +514 -0
- package/src/generator/kubernetes.ts +976 -0
- package/src/generator/terraform.ts +1867 -0
- package/src/history/index.ts +8 -0
- package/src/history/manager.ts +322 -0
- package/src/history/types.ts +34 -0
- package/src/hooks/config.ts +432 -0
- package/src/hooks/engine.ts +391 -0
- package/src/hooks/index.ts +4 -0
- package/src/llm/auth-bridge.ts +198 -0
- package/src/llm/circuit-breaker.ts +140 -0
- package/src/llm/config-loader.ts +201 -0
- package/src/llm/cost-calculator.ts +171 -0
- package/src/llm/index.ts +8 -0
- package/src/llm/model-aliases.ts +115 -0
- package/src/llm/provider-registry.ts +63 -0
- package/src/llm/providers/anthropic.ts +433 -0
- package/src/llm/providers/bedrock.ts +477 -0
- package/src/llm/providers/google.ts +405 -0
- package/src/llm/providers/ollama.ts +767 -0
- package/src/llm/providers/openai-compatible.ts +340 -0
- package/src/llm/providers/openai.ts +328 -0
- package/src/llm/providers/openrouter.ts +338 -0
- package/src/llm/router.ts +1035 -0
- package/src/llm/types.ts +232 -0
- package/src/lsp/client.ts +298 -0
- package/src/lsp/languages.ts +116 -0
- package/src/lsp/manager.ts +278 -0
- package/src/mcp/client.ts +402 -0
- package/src/mcp/index.ts +5 -0
- package/src/mcp/manager.ts +133 -0
- package/src/nimbus.ts +214 -0
- package/src/plugins/index.ts +27 -0
- package/src/plugins/loader.ts +334 -0
- package/src/plugins/manager.ts +376 -0
- package/src/plugins/types.ts +284 -0
- package/src/scanners/cicd-scanner.ts +258 -0
- package/src/scanners/cloud-scanner.ts +466 -0
- package/src/scanners/framework-scanner.ts +469 -0
- package/src/scanners/iac-scanner.ts +388 -0
- package/src/scanners/index.ts +539 -0
- package/src/scanners/language-scanner.ts +276 -0
- package/src/scanners/package-manager-scanner.ts +277 -0
- package/src/scanners/types.ts +172 -0
- package/src/sessions/manager.ts +365 -0
- package/src/sessions/types.ts +44 -0
- package/src/sharing/sync.ts +296 -0
- package/src/sharing/viewer.ts +97 -0
- package/src/snapshots/index.ts +2 -0
- package/src/snapshots/manager.ts +530 -0
- package/src/state/artifacts.ts +147 -0
- package/src/state/audit.ts +137 -0
- package/src/state/billing.ts +240 -0
- package/src/state/checkpoints.ts +117 -0
- package/src/state/config.ts +67 -0
- package/src/state/conversations.ts +14 -0
- package/src/state/credentials.ts +154 -0
- package/src/state/db.ts +58 -0
- package/src/state/index.ts +26 -0
- package/src/state/messages.ts +115 -0
- package/src/state/projects.ts +123 -0
- package/src/state/schema.ts +236 -0
- package/src/state/sessions.ts +147 -0
- package/src/state/teams.ts +200 -0
- package/src/telemetry.ts +108 -0
- package/src/tools/aws-ops.ts +952 -0
- package/src/tools/azure-ops.ts +579 -0
- package/src/tools/file-ops.ts +593 -0
- package/src/tools/gcp-ops.ts +625 -0
- package/src/tools/git-ops.ts +773 -0
- package/src/tools/github-ops.ts +799 -0
- package/src/tools/helm-ops.ts +943 -0
- package/src/tools/index.ts +17 -0
- package/src/tools/k8s-ops.ts +819 -0
- package/src/tools/schemas/converter.ts +184 -0
- package/src/tools/schemas/devops.ts +612 -0
- package/src/tools/schemas/index.ts +73 -0
- package/src/tools/schemas/standard.ts +1144 -0
- package/src/tools/schemas/types.ts +705 -0
- package/src/tools/terraform-ops.ts +862 -0
- package/src/types/ambient.d.ts +193 -0
- package/src/types/config.ts +83 -0
- package/src/types/drift.ts +116 -0
- package/src/types/enterprise.ts +335 -0
- package/src/types/index.ts +20 -0
- package/src/types/plan.ts +44 -0
- package/src/types/request.ts +65 -0
- package/src/types/response.ts +54 -0
- package/src/types/service.ts +51 -0
- package/src/ui/App.tsx +997 -0
- package/src/ui/DeployPreview.tsx +169 -0
- package/src/ui/Header.tsx +68 -0
- package/src/ui/InputBox.tsx +350 -0
- package/src/ui/MessageList.tsx +585 -0
- package/src/ui/PermissionPrompt.tsx +151 -0
- package/src/ui/StatusBar.tsx +158 -0
- package/src/ui/ToolCallDisplay.tsx +409 -0
- package/src/ui/chat-ui.ts +853 -0
- package/src/ui/index.ts +33 -0
- package/src/ui/ink/index.ts +711 -0
- package/src/ui/streaming.ts +176 -0
- package/src/ui/types.ts +57 -0
- package/src/utils/analytics.ts +72 -0
- package/src/utils/cost-warning.ts +27 -0
- package/src/utils/env.ts +46 -0
- package/src/utils/errors.ts +69 -0
- package/src/utils/event-bus.ts +38 -0
- package/src/utils/index.ts +24 -0
- package/src/utils/logger.ts +171 -0
- package/src/utils/rate-limiter.ts +121 -0
- package/src/utils/service-auth.ts +49 -0
- package/src/utils/validation.ts +53 -0
- package/src/version.ts +4 -0
- package/src/watcher/index.ts +163 -0
- package/src/wizard/approval.ts +383 -0
- package/src/wizard/index.ts +25 -0
- package/src/wizard/prompts.ts +338 -0
- package/src/wizard/types.ts +171 -0
- package/src/wizard/ui.ts +556 -0
- package/src/wizard/wizard.ts +304 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Module - Barrel Export
|
|
3
|
+
* Exports all authentication-related functionality
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Types
|
|
7
|
+
export type {
|
|
8
|
+
LLMProviderName,
|
|
9
|
+
GitHubIdentity,
|
|
10
|
+
LLMProviderCredential,
|
|
11
|
+
AuthFile,
|
|
12
|
+
AuthIdentity,
|
|
13
|
+
AuthProviders,
|
|
14
|
+
AuthStatus,
|
|
15
|
+
LoginWizardContext,
|
|
16
|
+
ProviderInfo,
|
|
17
|
+
GitHubDeviceCodeResponse,
|
|
18
|
+
GitHubAccessTokenResponse,
|
|
19
|
+
GitHubUserResponse,
|
|
20
|
+
GitHubEmailResponse,
|
|
21
|
+
ProviderValidationResult,
|
|
22
|
+
} from './types';
|
|
23
|
+
|
|
24
|
+
// Store
|
|
25
|
+
import { AuthStore, authStore } from './store';
|
|
26
|
+
export { AuthStore, authStore };
|
|
27
|
+
|
|
28
|
+
// Providers
|
|
29
|
+
export {
|
|
30
|
+
PROVIDER_REGISTRY,
|
|
31
|
+
getProviderInfo,
|
|
32
|
+
getProviderNames,
|
|
33
|
+
getDefaultModel,
|
|
34
|
+
validateProviderApiKey,
|
|
35
|
+
} from './providers';
|
|
36
|
+
|
|
37
|
+
// OAuth
|
|
38
|
+
export {
|
|
39
|
+
GitHubDeviceFlow,
|
|
40
|
+
BrowserOAuthServer,
|
|
41
|
+
fetchGitHubUser,
|
|
42
|
+
fetchGitHubEmail,
|
|
43
|
+
completeGitHubAuth,
|
|
44
|
+
exchangeCodeForToken,
|
|
45
|
+
} from './oauth';
|
|
46
|
+
|
|
47
|
+
// Guard
|
|
48
|
+
export { requiresAuth, isAuthenticated, getAuthMessage } from './guard';
|
|
49
|
+
|
|
50
|
+
// SSO Device Flow
|
|
51
|
+
export { SSODeviceFlow, validateSSOToken } from './sso';
|
|
52
|
+
|
|
53
|
+
// Helper to get auth store instance
|
|
54
|
+
export function getAuthStore(): AuthStore {
|
|
55
|
+
return authStore;
|
|
56
|
+
}
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth Implementation
|
|
3
|
+
* Primary: Device Flow (works in SSH/headless)
|
|
4
|
+
* Fallback: Browser-based OAuth with local callback server
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
GitHubDeviceCodeResponse,
|
|
9
|
+
GitHubAccessTokenResponse,
|
|
10
|
+
GitHubUserResponse,
|
|
11
|
+
GitHubEmailResponse,
|
|
12
|
+
GitHubIdentity,
|
|
13
|
+
} from './types';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* GitHub OAuth App Client ID
|
|
17
|
+
* This is safe to commit - it's a public identifier for the OAuth app
|
|
18
|
+
* The OAuth app must be registered at github.com/settings/developers
|
|
19
|
+
*
|
|
20
|
+
* Note: Until the OAuth App is registered, the GitHub identity step
|
|
21
|
+
* will be deferred/skipped. LLM provider setup works independently.
|
|
22
|
+
*/
|
|
23
|
+
const GITHUB_CLIENT_ID = 'Ov23liPzN7sAjwDsqUcx';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Callback server port for browser-based OAuth fallback
|
|
27
|
+
*/
|
|
28
|
+
const CALLBACK_PORT = 19284;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* GitHub Device Flow OAuth
|
|
32
|
+
* Works in SSH sessions and headless environments
|
|
33
|
+
*/
|
|
34
|
+
export class GitHubDeviceFlow {
|
|
35
|
+
private clientId: string;
|
|
36
|
+
private deviceCode: string | null = null;
|
|
37
|
+
private interval: number = 5;
|
|
38
|
+
private expiresAt: number = 0;
|
|
39
|
+
|
|
40
|
+
constructor(clientId: string = GITHUB_CLIENT_ID) {
|
|
41
|
+
this.clientId = clientId;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Step 1: Request device code from GitHub
|
|
46
|
+
* Returns the user code that must be entered at github.com/login/device
|
|
47
|
+
*/
|
|
48
|
+
async requestDeviceCode(): Promise<GitHubDeviceCodeResponse> {
|
|
49
|
+
const response = await fetch('https://github.com/login/device/code', {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: {
|
|
52
|
+
Accept: 'application/json',
|
|
53
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
54
|
+
},
|
|
55
|
+
body: new URLSearchParams({
|
|
56
|
+
client_id: this.clientId,
|
|
57
|
+
scope: 'read:user user:email',
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
const text = await response.text();
|
|
63
|
+
throw new Error(`Failed to request device code: ${response.status} ${text}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const data = (await response.json()) as GitHubDeviceCodeResponse;
|
|
67
|
+
|
|
68
|
+
// Store for polling
|
|
69
|
+
this.deviceCode = data.device_code;
|
|
70
|
+
this.interval = data.interval || 5;
|
|
71
|
+
this.expiresAt = Date.now() + data.expires_in * 1000;
|
|
72
|
+
|
|
73
|
+
return data;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Step 2: Poll for access token
|
|
78
|
+
* Call this repeatedly until it returns a token or throws an error
|
|
79
|
+
*/
|
|
80
|
+
async pollForToken(): Promise<GitHubAccessTokenResponse> {
|
|
81
|
+
if (!this.deviceCode) {
|
|
82
|
+
throw new Error('Device code not requested. Call requestDeviceCode first.');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (Date.now() > this.expiresAt) {
|
|
86
|
+
throw new Error('Device code expired. Please start the login process again.');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const response = await fetch('https://github.com/login/oauth/access_token', {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: {
|
|
92
|
+
Accept: 'application/json',
|
|
93
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
94
|
+
},
|
|
95
|
+
body: new URLSearchParams({
|
|
96
|
+
client_id: this.clientId,
|
|
97
|
+
device_code: this.deviceCode,
|
|
98
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
const text = await response.text();
|
|
104
|
+
throw new Error(`Failed to poll for token: ${response.status} ${text}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return (await response.json()) as GitHubAccessTokenResponse;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get the polling interval in milliseconds
|
|
112
|
+
*/
|
|
113
|
+
getPollingInterval(): number {
|
|
114
|
+
return this.interval * 1000;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Wait for authorization by polling
|
|
119
|
+
* Returns the access token when the user completes authorization
|
|
120
|
+
*/
|
|
121
|
+
async waitForAuthorization(onPoll?: () => void, abortSignal?: AbortSignal): Promise<string> {
|
|
122
|
+
for (;;) {
|
|
123
|
+
if (abortSignal?.aborted) {
|
|
124
|
+
throw new Error('Authorization cancelled');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const result = await this.pollForToken();
|
|
128
|
+
|
|
129
|
+
if (result.access_token) {
|
|
130
|
+
return result.access_token;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (result.error === 'authorization_pending') {
|
|
134
|
+
// User hasn't authorized yet, keep polling
|
|
135
|
+
onPoll?.();
|
|
136
|
+
await this.sleep(this.getPollingInterval());
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (result.error === 'slow_down') {
|
|
141
|
+
// GitHub is asking us to slow down
|
|
142
|
+
this.interval += 5;
|
|
143
|
+
await this.sleep(this.getPollingInterval());
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (result.error === 'expired_token') {
|
|
148
|
+
throw new Error('Device code expired. Please start the login process again.');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (result.error === 'access_denied') {
|
|
152
|
+
throw new Error('Authorization was denied by the user.');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Unknown error
|
|
156
|
+
throw new Error(result.error_description || result.error || 'Unknown authorization error');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private sleep(ms: number): Promise<void> {
|
|
161
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Fetch GitHub user profile using access token
|
|
167
|
+
*/
|
|
168
|
+
export async function fetchGitHubUser(accessToken: string): Promise<GitHubUserResponse> {
|
|
169
|
+
const response = await fetch('https://api.github.com/user', {
|
|
170
|
+
headers: {
|
|
171
|
+
Authorization: `Bearer ${accessToken}`,
|
|
172
|
+
Accept: 'application/vnd.github.v3+json',
|
|
173
|
+
'User-Agent': 'Nimbus-CLI',
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (!response.ok) {
|
|
178
|
+
const text = await response.text();
|
|
179
|
+
throw new Error(`Failed to fetch user profile: ${response.status} ${text}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return (await response.json()) as GitHubUserResponse;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Fetch GitHub user's primary email
|
|
187
|
+
*/
|
|
188
|
+
export async function fetchGitHubEmail(accessToken: string): Promise<string | null> {
|
|
189
|
+
const response = await fetch('https://api.github.com/user/emails', {
|
|
190
|
+
headers: {
|
|
191
|
+
Authorization: `Bearer ${accessToken}`,
|
|
192
|
+
Accept: 'application/vnd.github.v3+json',
|
|
193
|
+
'User-Agent': 'Nimbus-CLI',
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (!response.ok) {
|
|
198
|
+
// Email access might be restricted, return null
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const emails = (await response.json()) as GitHubEmailResponse[];
|
|
203
|
+
const primaryEmail = emails.find(e => e.primary && e.verified);
|
|
204
|
+
return primaryEmail?.email || emails[0]?.email || null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Complete GitHub authentication flow
|
|
209
|
+
* Returns a GitHubIdentity object ready to store
|
|
210
|
+
*/
|
|
211
|
+
export async function completeGitHubAuth(accessToken: string): Promise<GitHubIdentity> {
|
|
212
|
+
const [user, email] = await Promise.all([
|
|
213
|
+
fetchGitHubUser(accessToken),
|
|
214
|
+
fetchGitHubEmail(accessToken),
|
|
215
|
+
]);
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
username: user.login,
|
|
219
|
+
name: user.name,
|
|
220
|
+
email: email || user.email,
|
|
221
|
+
avatarUrl: user.avatar_url,
|
|
222
|
+
accessToken,
|
|
223
|
+
authenticatedAt: new Date().toISOString(),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Browser-based OAuth callback server (fallback)
|
|
229
|
+
* Creates a temporary local server to receive the OAuth callback
|
|
230
|
+
*/
|
|
231
|
+
export class BrowserOAuthServer {
|
|
232
|
+
private server: ReturnType<typeof Bun.serve> | null = null;
|
|
233
|
+
private clientId: string;
|
|
234
|
+
private codePromise: Promise<string> | null = null;
|
|
235
|
+
private codeResolve: ((code: string) => void) | null = null;
|
|
236
|
+
private codeReject: ((error: Error) => void) | null = null;
|
|
237
|
+
|
|
238
|
+
constructor(clientId: string = GITHUB_CLIENT_ID) {
|
|
239
|
+
this.clientId = clientId;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Start the callback server and return the authorization URL
|
|
244
|
+
*/
|
|
245
|
+
async start(): Promise<string> {
|
|
246
|
+
// Create promise for receiving the code
|
|
247
|
+
this.codePromise = new Promise((resolve, reject) => {
|
|
248
|
+
this.codeResolve = resolve;
|
|
249
|
+
this.codeReject = reject;
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Start the server
|
|
253
|
+
this.server = Bun.serve({
|
|
254
|
+
port: CALLBACK_PORT,
|
|
255
|
+
fetch: request => this.handleRequest(request),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Build authorization URL
|
|
259
|
+
const params = new URLSearchParams({
|
|
260
|
+
client_id: this.clientId,
|
|
261
|
+
redirect_uri: `http://localhost:${CALLBACK_PORT}/callback`,
|
|
262
|
+
scope: 'read:user user:email',
|
|
263
|
+
state: this.generateState(),
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return `https://github.com/login/oauth/authorize?${params}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Wait for the authorization code
|
|
271
|
+
*/
|
|
272
|
+
async waitForCode(timeout: number = 300000): Promise<string> {
|
|
273
|
+
if (!this.codePromise) {
|
|
274
|
+
throw new Error('Server not started. Call start() first.');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
278
|
+
setTimeout(() => {
|
|
279
|
+
reject(new Error('Authorization timed out'));
|
|
280
|
+
}, timeout);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
return await Promise.race([this.codePromise, timeoutPromise]);
|
|
285
|
+
} finally {
|
|
286
|
+
this.stop();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Stop the callback server
|
|
292
|
+
*/
|
|
293
|
+
stop(): void {
|
|
294
|
+
if (this.server) {
|
|
295
|
+
this.server.stop();
|
|
296
|
+
this.server = null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private handleRequest(request: Request): Response {
|
|
301
|
+
const url = new URL(request.url);
|
|
302
|
+
|
|
303
|
+
if (url.pathname === '/callback') {
|
|
304
|
+
const code = url.searchParams.get('code');
|
|
305
|
+
const error = url.searchParams.get('error');
|
|
306
|
+
const errorDescription = url.searchParams.get('error_description');
|
|
307
|
+
|
|
308
|
+
if (error) {
|
|
309
|
+
this.codeReject?.(new Error(errorDescription || error));
|
|
310
|
+
return new Response(this.getErrorPage(errorDescription || error), {
|
|
311
|
+
headers: { 'Content-Type': 'text/html' },
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (code) {
|
|
316
|
+
this.codeResolve?.(code);
|
|
317
|
+
return new Response(this.getSuccessPage(), {
|
|
318
|
+
headers: { 'Content-Type': 'text/html' },
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return new Response('Bad request', { status: 400 });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return new Response('Not found', { status: 404 });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private generateState(): string {
|
|
329
|
+
const array = new Uint8Array(16);
|
|
330
|
+
crypto.getRandomValues(array);
|
|
331
|
+
return Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private getSuccessPage(): string {
|
|
335
|
+
return `
|
|
336
|
+
<!DOCTYPE html>
|
|
337
|
+
<html>
|
|
338
|
+
<head>
|
|
339
|
+
<title>Nimbus CLI - Authorization Successful</title>
|
|
340
|
+
<style>
|
|
341
|
+
body {
|
|
342
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
343
|
+
display: flex;
|
|
344
|
+
justify-content: center;
|
|
345
|
+
align-items: center;
|
|
346
|
+
height: 100vh;
|
|
347
|
+
margin: 0;
|
|
348
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
349
|
+
color: white;
|
|
350
|
+
}
|
|
351
|
+
.container {
|
|
352
|
+
text-align: center;
|
|
353
|
+
padding: 40px;
|
|
354
|
+
background: rgba(255,255,255,0.1);
|
|
355
|
+
border-radius: 16px;
|
|
356
|
+
backdrop-filter: blur(10px);
|
|
357
|
+
}
|
|
358
|
+
h1 { margin-bottom: 10px; }
|
|
359
|
+
p { opacity: 0.9; }
|
|
360
|
+
</style>
|
|
361
|
+
</head>
|
|
362
|
+
<body>
|
|
363
|
+
<div class="container">
|
|
364
|
+
<h1>✓ Authorization Successful</h1>
|
|
365
|
+
<p>You can close this window and return to the terminal.</p>
|
|
366
|
+
</div>
|
|
367
|
+
</body>
|
|
368
|
+
</html>`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private getErrorPage(error: string): string {
|
|
372
|
+
return `
|
|
373
|
+
<!DOCTYPE html>
|
|
374
|
+
<html>
|
|
375
|
+
<head>
|
|
376
|
+
<title>Nimbus CLI - Authorization Failed</title>
|
|
377
|
+
<style>
|
|
378
|
+
body {
|
|
379
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
380
|
+
display: flex;
|
|
381
|
+
justify-content: center;
|
|
382
|
+
align-items: center;
|
|
383
|
+
height: 100vh;
|
|
384
|
+
margin: 0;
|
|
385
|
+
background: linear-gradient(135deg, #f43f5e 0%, #e11d48 100%);
|
|
386
|
+
color: white;
|
|
387
|
+
}
|
|
388
|
+
.container {
|
|
389
|
+
text-align: center;
|
|
390
|
+
padding: 40px;
|
|
391
|
+
background: rgba(255,255,255,0.1);
|
|
392
|
+
border-radius: 16px;
|
|
393
|
+
backdrop-filter: blur(10px);
|
|
394
|
+
}
|
|
395
|
+
h1 { margin-bottom: 10px; }
|
|
396
|
+
p { opacity: 0.9; }
|
|
397
|
+
</style>
|
|
398
|
+
</head>
|
|
399
|
+
<body>
|
|
400
|
+
<div class="container">
|
|
401
|
+
<h1>✗ Authorization Failed</h1>
|
|
402
|
+
<p>${error}</p>
|
|
403
|
+
<p>Please close this window and try again.</p>
|
|
404
|
+
</div>
|
|
405
|
+
</body>
|
|
406
|
+
</html>`;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Exchange authorization code for access token (browser flow)
|
|
412
|
+
* Note: This requires a client secret which should not be in CLI code
|
|
413
|
+
* For true CLI-only auth, use the Device Flow instead
|
|
414
|
+
*/
|
|
415
|
+
export async function exchangeCodeForToken(
|
|
416
|
+
code: string,
|
|
417
|
+
clientId: string = GITHUB_CLIENT_ID,
|
|
418
|
+
clientSecret?: string
|
|
419
|
+
): Promise<string> {
|
|
420
|
+
if (!clientSecret) {
|
|
421
|
+
throw new Error(
|
|
422
|
+
'Browser-based OAuth requires a client secret. Use Device Flow for CLI-only auth.'
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const response = await fetch('https://github.com/login/oauth/access_token', {
|
|
427
|
+
method: 'POST',
|
|
428
|
+
headers: {
|
|
429
|
+
Accept: 'application/json',
|
|
430
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
431
|
+
},
|
|
432
|
+
body: new URLSearchParams({
|
|
433
|
+
client_id: clientId,
|
|
434
|
+
client_secret: clientSecret,
|
|
435
|
+
code,
|
|
436
|
+
}),
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
if (!response.ok) {
|
|
440
|
+
const text = await response.text();
|
|
441
|
+
throw new Error(`Failed to exchange code for token: ${response.status} ${text}`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const data = (await response.json()) as GitHubAccessTokenResponse;
|
|
445
|
+
|
|
446
|
+
if (data.error) {
|
|
447
|
+
throw new Error(data.error_description || data.error);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (!data.access_token) {
|
|
451
|
+
throw new Error('No access token in response');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return data.access_token;
|
|
455
|
+
}
|