@compilr-dev/sdk 0.10.41 → 0.11.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.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Shared entitlements fetch — CLI and Desktop had byte-identical copies of this
3
+ * (only the token source differed). They now both pass a `getToken` thunk; the
4
+ * `string | null` return lets Desktop wire it straight to `ensureFreshToken`
5
+ * (`() => ensureFreshToken().then(r => r?.token ?? null)`), which also fixes
6
+ * "fetch entitlements with an expired JWT".
7
+ */
8
+ import type { EntitlementResponse } from './types.js';
9
+ /**
10
+ * GET the caller's entitlements from the backend.
11
+ *
12
+ * @param getToken - Resolves the bearer token (or null if unauthenticated).
13
+ * @param apiUrl - Base URL override. Defaults to `COMPILR_API_URL` then production.
14
+ * @throws if unauthenticated or the request fails (callers fall back to cache).
15
+ */
16
+ export declare function fetchEntitlements(getToken: () => Promise<string | null>, apiUrl?: string): Promise<EntitlementResponse>;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Shared entitlements fetch — CLI and Desktop had byte-identical copies of this
3
+ * (only the token source differed). They now both pass a `getToken` thunk; the
4
+ * `string | null` return lets Desktop wire it straight to `ensureFreshToken`
5
+ * (`() => ensureFreshToken().then(r => r?.token ?? null)`), which also fixes
6
+ * "fetch entitlements with an expired JWT".
7
+ */
8
+ const DEFAULT_API_BASE_URL = 'https://compilr.dev/.netlify/functions';
9
+ /**
10
+ * GET the caller's entitlements from the backend.
11
+ *
12
+ * @param getToken - Resolves the bearer token (or null if unauthenticated).
13
+ * @param apiUrl - Base URL override. Defaults to `COMPILR_API_URL` then production.
14
+ * @throws if unauthenticated or the request fails (callers fall back to cache).
15
+ */
16
+ export async function fetchEntitlements(getToken, apiUrl) {
17
+ const token = await getToken();
18
+ if (!token)
19
+ throw new Error('Not authenticated');
20
+ const base = apiUrl ?? process.env['COMPILR_API_URL'] ?? DEFAULT_API_BASE_URL;
21
+ const resp = await fetch(`${base}/cli-entitlements`, {
22
+ method: 'GET',
23
+ headers: {
24
+ Authorization: `Bearer ${token}`,
25
+ 'Content-Type': 'application/json',
26
+ },
27
+ });
28
+ if (!resp.ok) {
29
+ throw new Error(`Entitlements fetch failed: ${String(resp.status)}`);
30
+ }
31
+ return (await resp.json());
32
+ }
@@ -6,3 +6,4 @@ export { UNLIMITED, OFFLINE_FALLBACK_LIMITS } from './types.js';
6
6
  export type { IEntitlementStore, EntitlementCacheConfig } from './cache.js';
7
7
  export { EntitlementCache } from './cache.js';
8
8
  export { DailyCounter, formatLimitMessage, formatTimeUntilReset, formatUpgradeHint, } from './gating.js';
9
+ export { fetchEntitlements } from './fetch.js';
@@ -4,3 +4,4 @@
4
4
  export { UNLIMITED, OFFLINE_FALLBACK_LIMITS } from './types.js';
5
5
  export { EntitlementCache } from './cache.js';
6
6
  export { DailyCounter, formatLimitMessage, formatTimeUntilReset, formatUpgradeHint, } from './gating.js';
7
+ export { fetchEntitlements } from './fetch.js';
@@ -0,0 +1,49 @@
1
+ /**
2
+ * HTTP client for the compilr.dev CLI auth backend (Netlify Functions).
3
+ *
4
+ * Moved from the CLI's `src/auth/api-client.ts` so CLI and Desktop share one
5
+ * implementation (Desktop previously re-implemented these calls inline). All
6
+ * endpoints accept an optional `AuthEndpoints` override; by default they use
7
+ * `COMPILR_API_URL` / `COMPILR_WEB_URL` (falling back to production).
8
+ *
9
+ * Pure network module — no fs, no electron. Uses global `fetch` (Node ≥ 20).
10
+ */
11
+ import type { RefreshTokenResponse, ProfileData, HeartbeatData, DeviceCodeResponse, DeviceTokenResponse, DeviceTokenError, AuthEndpoints } from './auth-types.js';
12
+ /**
13
+ * Refresh an access token using a refresh token. The server-side function
14
+ * handles Supabase internally.
15
+ */
16
+ export declare function refreshToken(refreshTokenValue: string, endpoints?: AuthEndpoints): Promise<RefreshTokenResponse>;
17
+ /** Send a session heartbeat (telemetry). */
18
+ export declare function sendHeartbeat(accessToken: string, data: HeartbeatData, endpoints?: AuthEndpoints): Promise<{
19
+ success: boolean;
20
+ telemetry_enabled?: boolean;
21
+ error?: string;
22
+ }>;
23
+ /** Fetch the user's profile. */
24
+ export declare function getProfile(accessToken: string, endpoints?: AuthEndpoints): Promise<{
25
+ success: boolean;
26
+ profile?: ProfileData;
27
+ error?: string;
28
+ }>;
29
+ /** Update profile settings. */
30
+ export declare function updateProfile(accessToken: string, updates: Partial<Pick<ProfileData, 'telemetry_enabled' | 'default_provider' | 'default_model' | 'default_tier' | 'theme'>>, endpoints?: AuthEndpoints): Promise<{
31
+ success: boolean;
32
+ profile?: ProfileData;
33
+ error?: string;
34
+ }>;
35
+ /** Request a device code to start the browser-based auth flow. */
36
+ export declare function requestDeviceCode(endpoints?: AuthEndpoints): Promise<{
37
+ success: boolean;
38
+ data?: DeviceCodeResponse;
39
+ error?: string;
40
+ }>;
41
+ /** Poll for the device token after the user authorizes in the browser. */
42
+ export declare function pollDeviceToken(deviceCode: string, endpoints?: AuthEndpoints): Promise<{
43
+ success: boolean;
44
+ data?: DeviceTokenResponse;
45
+ error?: DeviceTokenError;
46
+ networkError?: string;
47
+ }>;
48
+ /** Build the device-flow authorization URL (optionally pre-filling the code). */
49
+ export declare function getAuthorizationUrl(userCode?: string, endpoints?: AuthEndpoints): string;
@@ -0,0 +1,201 @@
1
+ /**
2
+ * HTTP client for the compilr.dev CLI auth backend (Netlify Functions).
3
+ *
4
+ * Moved from the CLI's `src/auth/api-client.ts` so CLI and Desktop share one
5
+ * implementation (Desktop previously re-implemented these calls inline). All
6
+ * endpoints accept an optional `AuthEndpoints` override; by default they use
7
+ * `COMPILR_API_URL` / `COMPILR_WEB_URL` (falling back to production).
8
+ *
9
+ * Pure network module — no fs, no electron. Uses global `fetch` (Node ≥ 20).
10
+ */
11
+ import * as os from 'os';
12
+ const DEFAULT_API_BASE_URL = 'https://compilr.dev/.netlify/functions';
13
+ const DEFAULT_WEB_BASE_URL = 'https://compilr.dev';
14
+ function apiBase(endpoints) {
15
+ return endpoints?.baseUrl ?? process.env.COMPILR_API_URL ?? DEFAULT_API_BASE_URL;
16
+ }
17
+ function webBase(endpoints) {
18
+ return endpoints?.webUrl ?? process.env.COMPILR_WEB_URL ?? DEFAULT_WEB_BASE_URL;
19
+ }
20
+ // =============================================================================
21
+ // Token Refresh
22
+ // =============================================================================
23
+ /**
24
+ * Refresh an access token using a refresh token. The server-side function
25
+ * handles Supabase internally.
26
+ */
27
+ export async function refreshToken(refreshTokenValue, endpoints) {
28
+ try {
29
+ const response = await fetch(`${apiBase(endpoints)}/cli-refresh-token`, {
30
+ method: 'POST',
31
+ headers: { 'Content-Type': 'application/json' },
32
+ body: JSON.stringify({ refresh_token: refreshTokenValue }),
33
+ });
34
+ const contentType = response.headers.get('content-type') ?? '';
35
+ if (!contentType.includes('application/json')) {
36
+ return {
37
+ success: false,
38
+ error: `Server returned non-JSON response (${String(response.status)})`,
39
+ };
40
+ }
41
+ const result = (await response.json());
42
+ if (!response.ok) {
43
+ return { success: false, error: result.error ?? `HTTP ${String(response.status)}` };
44
+ }
45
+ return {
46
+ success: true,
47
+ access_token: result.access_token,
48
+ refresh_token: result.refresh_token,
49
+ expires_in: result.expires_in,
50
+ user: result.user,
51
+ };
52
+ }
53
+ catch (error) {
54
+ return { success: false, error: error instanceof Error ? error.message : 'Network error' };
55
+ }
56
+ }
57
+ // =============================================================================
58
+ // Profile + Heartbeat
59
+ // =============================================================================
60
+ /** Send a session heartbeat (telemetry). */
61
+ export async function sendHeartbeat(accessToken, data, endpoints) {
62
+ try {
63
+ const response = await fetch(`${apiBase(endpoints)}/cli-heartbeat`, {
64
+ method: 'POST',
65
+ headers: {
66
+ Authorization: `Bearer ${accessToken}`,
67
+ 'Content-Type': 'application/json',
68
+ },
69
+ body: JSON.stringify(data),
70
+ });
71
+ const contentType = response.headers.get('content-type') ?? '';
72
+ if (!contentType.includes('application/json')) {
73
+ return {
74
+ success: false,
75
+ error: `Server returned non-JSON response (${String(response.status)})`,
76
+ };
77
+ }
78
+ const result = (await response.json());
79
+ if (!response.ok) {
80
+ return { success: false, error: result.error ?? `HTTP ${String(response.status)}` };
81
+ }
82
+ return { success: result.success ?? true, telemetry_enabled: result.telemetry_enabled };
83
+ }
84
+ catch (error) {
85
+ return { success: false, error: error instanceof Error ? error.message : 'Network error' };
86
+ }
87
+ }
88
+ /** Fetch the user's profile. */
89
+ export async function getProfile(accessToken, endpoints) {
90
+ try {
91
+ const response = await fetch(`${apiBase(endpoints)}/cli-profile`, {
92
+ method: 'GET',
93
+ headers: { Authorization: `Bearer ${accessToken}` },
94
+ });
95
+ const contentType = response.headers.get('content-type') ?? '';
96
+ if (!contentType.includes('application/json')) {
97
+ return {
98
+ success: false,
99
+ error: `Server returned non-JSON response (${String(response.status)})`,
100
+ };
101
+ }
102
+ const result = (await response.json());
103
+ if (!response.ok) {
104
+ return { success: false, error: result.error ?? `HTTP ${String(response.status)}` };
105
+ }
106
+ return { success: true, profile: result };
107
+ }
108
+ catch (error) {
109
+ return { success: false, error: error instanceof Error ? error.message : 'Network error' };
110
+ }
111
+ }
112
+ /** Update profile settings. */
113
+ export async function updateProfile(accessToken, updates, endpoints) {
114
+ try {
115
+ const response = await fetch(`${apiBase(endpoints)}/cli-profile`, {
116
+ method: 'PATCH',
117
+ headers: {
118
+ Authorization: `Bearer ${accessToken}`,
119
+ 'Content-Type': 'application/json',
120
+ },
121
+ body: JSON.stringify(updates),
122
+ });
123
+ const contentType = response.headers.get('content-type') ?? '';
124
+ if (!contentType.includes('application/json')) {
125
+ return {
126
+ success: false,
127
+ error: `Server returned non-JSON response (${String(response.status)})`,
128
+ };
129
+ }
130
+ const result = (await response.json());
131
+ if (!response.ok) {
132
+ return { success: false, error: result.error ?? `HTTP ${String(response.status)}` };
133
+ }
134
+ return { success: result.success ?? true, profile: result.profile };
135
+ }
136
+ catch (error) {
137
+ return { success: false, error: error instanceof Error ? error.message : 'Network error' };
138
+ }
139
+ }
140
+ // =============================================================================
141
+ // Device Flow
142
+ // =============================================================================
143
+ /** Request a device code to start the browser-based auth flow. */
144
+ export async function requestDeviceCode(endpoints) {
145
+ try {
146
+ const url = `${apiBase(endpoints)}/cli-device-code`;
147
+ const response = await fetch(url, {
148
+ method: 'POST',
149
+ headers: { 'Content-Type': 'application/json' },
150
+ });
151
+ const contentType = response.headers.get('content-type') ?? '';
152
+ if (!contentType.includes('application/json')) {
153
+ return {
154
+ success: false,
155
+ error: `Non-JSON response (${String(response.status)}) from: ${url}`,
156
+ };
157
+ }
158
+ if (!response.ok) {
159
+ const result = (await response.json());
160
+ return { success: false, error: result.error ?? `HTTP ${String(response.status)}` };
161
+ }
162
+ const data = (await response.json());
163
+ return { success: true, data };
164
+ }
165
+ catch (error) {
166
+ return { success: false, error: error instanceof Error ? error.message : 'Network error' };
167
+ }
168
+ }
169
+ /** Poll for the device token after the user authorizes in the browser. */
170
+ export async function pollDeviceToken(deviceCode, endpoints) {
171
+ try {
172
+ const response = await fetch(`${apiBase(endpoints)}/cli-device-token`, {
173
+ method: 'POST',
174
+ headers: { 'Content-Type': 'application/json' },
175
+ body: JSON.stringify({ device_code: deviceCode, hostname: os.hostname() }),
176
+ });
177
+ const contentType = response.headers.get('content-type') ?? '';
178
+ if (!contentType.includes('application/json')) {
179
+ return {
180
+ success: false,
181
+ networkError: `Server returned non-JSON response (${String(response.status)})`,
182
+ };
183
+ }
184
+ const result = (await response.json());
185
+ if (!response.ok) {
186
+ return { success: false, error: result };
187
+ }
188
+ return { success: true, data: result };
189
+ }
190
+ catch (error) {
191
+ return {
192
+ success: false,
193
+ networkError: error instanceof Error ? error.message : 'Network error',
194
+ };
195
+ }
196
+ }
197
+ /** Build the device-flow authorization URL (optionally pre-filling the code). */
198
+ export function getAuthorizationUrl(userCode, endpoints) {
199
+ const url = `${webBase(endpoints)}/cli/authorize`;
200
+ return userCode ? `${url}?code=${userCode}` : url;
201
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Persistent storage for auth credentials (`~/.compilr-dev/auth.json`).
3
+ *
4
+ * Moved from the CLI's `src/auth/storage.ts` so CLI and Desktop read/write the
5
+ * shared file identically (Desktop previously parsed auth.json inline in several
6
+ * places). The on-disk shape is unchanged.
7
+ *
8
+ * Security: the file is written 0600 (owner read/write only).
9
+ *
10
+ * All functions accept an optional `dir` (the `.compilr-dev` directory) for
11
+ * tests and for hosts whose home path differs from `os.homedir()`. It defaults
12
+ * to `~/.compilr-dev`.
13
+ */
14
+ import type { AuthData, AuthSession } from './auth-types.js';
15
+ /**
16
+ * Load auth data from disk. Returns null if the file is missing, unparseable,
17
+ * or missing required fields.
18
+ */
19
+ export declare function loadAuthData(dir?: string): AuthData | null;
20
+ /** Save auth data to disk with restrictive (0600) permissions. */
21
+ export declare function saveAuthData(data: AuthData, dir?: string): void;
22
+ /** Remove stored credentials (logout). */
23
+ export declare function clearAuthData(dir?: string): void;
24
+ /**
25
+ * Replace just the session tokens (after a refresh), preserving the rest of the
26
+ * file. Returns false if there's no existing auth data.
27
+ */
28
+ export declare function updateSession(session: AuthSession, dir?: string): boolean;
29
+ /** Quick existence check without loading/parsing. */
30
+ export declare function hasAuthData(dir?: string): boolean;
31
+ /**
32
+ * Returns a warning string if the auth file has group/other-accessible
33
+ * permissions, or null if it's secure / absent.
34
+ */
35
+ export declare function checkAuthFilePermissions(dir?: string): string | null;
36
+ export { loadAuthData as readAuthFile, saveAuthData as writeAuthFile };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Persistent storage for auth credentials (`~/.compilr-dev/auth.json`).
3
+ *
4
+ * Moved from the CLI's `src/auth/storage.ts` so CLI and Desktop read/write the
5
+ * shared file identically (Desktop previously parsed auth.json inline in several
6
+ * places). The on-disk shape is unchanged.
7
+ *
8
+ * Security: the file is written 0600 (owner read/write only).
9
+ *
10
+ * All functions accept an optional `dir` (the `.compilr-dev` directory) for
11
+ * tests and for hosts whose home path differs from `os.homedir()`. It defaults
12
+ * to `~/.compilr-dev`.
13
+ */
14
+ import * as fs from 'fs';
15
+ import * as path from 'path';
16
+ import * as os from 'os';
17
+ function configDir(dir) {
18
+ return dir ?? path.join(os.homedir(), '.compilr-dev');
19
+ }
20
+ function authFile(dir) {
21
+ return path.join(configDir(dir), 'auth.json');
22
+ }
23
+ /**
24
+ * Load auth data from disk. Returns null if the file is missing, unparseable,
25
+ * or missing required fields.
26
+ */
27
+ export function loadAuthData(dir) {
28
+ try {
29
+ const file = authFile(dir);
30
+ if (!fs.existsSync(file))
31
+ return null;
32
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
33
+ if (typeof parsed.version !== 'number' ||
34
+ typeof parsed.user !== 'object' ||
35
+ !parsed.user ||
36
+ typeof parsed.session !== 'object' ||
37
+ !parsed.session) {
38
+ return null;
39
+ }
40
+ return parsed;
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ /** Save auth data to disk with restrictive (0600) permissions. */
47
+ export function saveAuthData(data, dir) {
48
+ fs.mkdirSync(configDir(dir), { recursive: true });
49
+ const file = authFile(dir);
50
+ fs.writeFileSync(file, JSON.stringify(data, null, 2), { encoding: 'utf-8', mode: 0o600 });
51
+ try {
52
+ fs.chmodSync(file, 0o600);
53
+ }
54
+ catch {
55
+ // chmod may be unsupported (e.g. Windows) — ignore.
56
+ }
57
+ }
58
+ /** Remove stored credentials (logout). */
59
+ export function clearAuthData(dir) {
60
+ try {
61
+ const file = authFile(dir);
62
+ if (fs.existsSync(file))
63
+ fs.unlinkSync(file);
64
+ }
65
+ catch {
66
+ // ignore
67
+ }
68
+ }
69
+ /**
70
+ * Replace just the session tokens (after a refresh), preserving the rest of the
71
+ * file. Returns false if there's no existing auth data.
72
+ */
73
+ export function updateSession(session, dir) {
74
+ const data = loadAuthData(dir);
75
+ if (!data)
76
+ return false;
77
+ data.session = session;
78
+ saveAuthData(data, dir);
79
+ return true;
80
+ }
81
+ /** Quick existence check without loading/parsing. */
82
+ export function hasAuthData(dir) {
83
+ return fs.existsSync(authFile(dir));
84
+ }
85
+ /**
86
+ * Returns a warning string if the auth file has group/other-accessible
87
+ * permissions, or null if it's secure / absent.
88
+ */
89
+ export function checkAuthFilePermissions(dir) {
90
+ try {
91
+ const file = authFile(dir);
92
+ if (!fs.existsSync(file))
93
+ return null;
94
+ const mode = fs.statSync(file).mode & 0o777;
95
+ if (mode & 0o077) {
96
+ return `Warning: Auth file has insecure permissions (${mode.toString(8)}). Consider running: chmod 600 ${file}`;
97
+ }
98
+ return null;
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ }
104
+ // Spec §5.2 names — aliases of the canonical load/save.
105
+ export { loadAuthData as readAuthFile, saveAuthData as writeAuthFile };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Shared auth types for the host integration layer.
3
+ *
4
+ * Covers both the on-disk credential shape (`~/.compilr-dev/auth.json`) and the
5
+ * response shapes of the compilr.dev CLI backend (Netlify Functions). CLI and
6
+ * Desktop both speak this protocol; these types are the single source of truth
7
+ * (Desktop previously re-declared them inline with `as` casts).
8
+ *
9
+ * USER authentication only — users still bring their own LLM API keys.
10
+ */
11
+ export type AccountType = 'free' | 'pro' | 'team' | 'enterprise';
12
+ export interface AuthUser {
13
+ id: string;
14
+ email: string;
15
+ createdAt: string;
16
+ }
17
+ export interface AuthSession {
18
+ accessToken: string;
19
+ refreshToken: string;
20
+ /** ISO timestamp of JWT expiry. */
21
+ expiresAt: string;
22
+ /** Long-lived API token (tok_xxx). When present it's preferred over the JWT. */
23
+ apiToken?: string;
24
+ }
25
+ export interface AuthData {
26
+ version: number;
27
+ user: AuthUser;
28
+ session: AuthSession;
29
+ }
30
+ export interface RefreshTokenResponse {
31
+ success: boolean;
32
+ access_token?: string;
33
+ refresh_token?: string;
34
+ expires_in?: number;
35
+ user?: {
36
+ id: string;
37
+ email: string;
38
+ };
39
+ error?: string;
40
+ }
41
+ export interface ProfileData {
42
+ user_id: string;
43
+ email: string;
44
+ account_type: AccountType;
45
+ telemetry_enabled: boolean;
46
+ default_provider: string;
47
+ default_model: string;
48
+ default_tier: string;
49
+ theme: string;
50
+ total_sessions: number;
51
+ total_messages: number;
52
+ first_session_at: string | null;
53
+ last_session_at: string | null;
54
+ created_at: string;
55
+ }
56
+ export interface HeartbeatData {
57
+ session_id: string;
58
+ cli_version: string;
59
+ os: string;
60
+ node_version: string;
61
+ project_id?: number;
62
+ total_messages?: number;
63
+ total_tokens_in?: number;
64
+ total_tokens_out?: number;
65
+ commands_used?: Record<string, number>;
66
+ agents_used?: string[];
67
+ tools_used?: string[];
68
+ llm_provider?: string;
69
+ llm_model?: string;
70
+ }
71
+ export interface DeviceCodeResponse {
72
+ device_code: string;
73
+ user_code: string;
74
+ verification_uri: string;
75
+ verification_uri_complete: string;
76
+ expires_in: number;
77
+ interval: number;
78
+ }
79
+ export interface DeviceTokenResponse {
80
+ access_token: string;
81
+ refresh_token: string;
82
+ token_type: string;
83
+ expires_in: number;
84
+ /** Long-lived CLI token (tok_xxx) — persistent auth. */
85
+ api_token?: string;
86
+ user: {
87
+ id: string;
88
+ email: string;
89
+ };
90
+ }
91
+ export interface DeviceTokenError {
92
+ error: 'authorization_pending' | 'slow_down' | 'expired_token' | 'access_denied' | 'invalid_grant' | 'server_error';
93
+ error_description: string;
94
+ }
95
+ /**
96
+ * Optional endpoint overrides. Both default to the production URLs (or the
97
+ * `COMPILR_API_URL` / `COMPILR_WEB_URL` env vars). Pass explicit values in tests
98
+ * or to target a branch preview.
99
+ */
100
+ export interface AuthEndpoints {
101
+ /** Base URL for Netlify Functions, e.g. https://compilr.dev/.netlify/functions */
102
+ baseUrl?: string;
103
+ /** Web base URL for the device-flow authorization page, e.g. https://compilr.dev */
104
+ webUrl?: string;
105
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Shared auth types for the host integration layer.
3
+ *
4
+ * Covers both the on-disk credential shape (`~/.compilr-dev/auth.json`) and the
5
+ * response shapes of the compilr.dev CLI backend (Netlify Functions). CLI and
6
+ * Desktop both speak this protocol; these types are the single source of truth
7
+ * (Desktop previously re-declared them inline with `as` casts).
8
+ *
9
+ * USER authentication only — users still bring their own LLM API keys.
10
+ */
11
+ export {};
@@ -0,0 +1,44 @@
1
+ /**
2
+ * runDeviceFlow — browser-based (OAuth device flow) login state machine.
3
+ *
4
+ * Ported from the CLI's AuthManager.loginWithDeviceFlow so CLI and Desktop share
5
+ * one implementation. Desktop previously drove the poll loop from the renderer
6
+ * one fetch at a time and therefore lacked the `slow_down` backoff this owns.
7
+ *
8
+ * On success the tokens are persisted to `~/.compilr-dev/auth.json`. Fetching the
9
+ * user profile (account type, etc.) is left to the host — call `getProfile`
10
+ * afterward if needed.
11
+ */
12
+ import type { AuthEndpoints } from './auth-types.js';
13
+ export type DeviceFlowStatus = 'polling' | 'authorized' | 'expired' | 'denied';
14
+ export interface DeviceFlowCallbacks {
15
+ /** Called once the user code + authorization URL are ready (show / open it). */
16
+ onCodeReady(userCode: string, url: string): void;
17
+ /** Optional progress updates as polling proceeds. */
18
+ onStatusUpdate?(status: DeviceFlowStatus): void;
19
+ }
20
+ export interface LoginResult {
21
+ success: boolean;
22
+ user?: {
23
+ id: string;
24
+ email: string;
25
+ };
26
+ error?: string;
27
+ }
28
+ export interface RunDeviceFlowOptions {
29
+ /** Override the `.compilr-dev` directory (tests / non-standard homes). */
30
+ dir?: string;
31
+ /** Override backend endpoints (tests / branch previews). */
32
+ endpoints?: AuthEndpoints;
33
+ }
34
+ /**
35
+ * Next poll interval after a `slow_down` response: +5s, capped at 30s.
36
+ * Exported for unit testing the backoff that Desktop was missing.
37
+ */
38
+ export declare function nextPollInterval(currentMs: number): number;
39
+ /**
40
+ * Run the full device-flow login: request a code, surface it via `onCodeReady`,
41
+ * then poll until authorized, denied, expired, or aborted. Persists tokens on
42
+ * success.
43
+ */
44
+ export declare function runDeviceFlow(callbacks: DeviceFlowCallbacks, signal?: AbortSignal, opts?: RunDeviceFlowOptions): Promise<LoginResult>;