pi-antigravity-rotator 2.3.0 → 2.3.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.
@@ -0,0 +1,17 @@
1
+ import type { Config } from "./types.js";
2
+
3
+ type ExposureConfig = Pick<Config, "bindHost" | "proxyPort">;
4
+
5
+ export function isLoopbackBindHost(bindHost?: string | null): boolean {
6
+ const host = (bindHost || "0.0.0.0").trim().toLowerCase();
7
+ return host === "localhost" || host === "127.0.0.1" || host === "::1";
8
+ }
9
+
10
+ export function getProxyExposureWarning(config: ExposureConfig): string | null {
11
+ const bindHost = config.bindHost || "0.0.0.0";
12
+ if (isLoopbackBindHost(bindHost)) return null;
13
+ return (
14
+ `Native and /v1 compatibility proxy routes are unauthenticated by design and are listening on ${bindHost}:${config.proxyPort}. ` +
15
+ "Restrict this port to localhost/LAN, a firewall, or a trusted reverse proxy, or set bindHost to 127.0.0.1 for local-only use."
16
+ );
17
+ }
package/src/index.ts CHANGED
@@ -24,6 +24,7 @@ import { setModelAliasesOverride } from "./types.js";
24
24
  import { writeTextFileAtomic } from "./storage.js";
25
25
  import { initDb } from "./db-store.js";
26
26
  import { stopPendingSessionReaper } from "./onboarding.js";
27
+ import { getProxyExposureWarning } from "./exposure.js";
27
28
 
28
29
  function loadConfig(): Config {
29
30
  try {
@@ -146,6 +147,16 @@ function maybeWarnAboutAdminExposure(config: Config): void {
146
147
  console.warn();
147
148
  }
148
149
 
150
+ function maybeWarnAboutProxyExposure(config: Config): void {
151
+ const warning = getProxyExposureWarning(config);
152
+ if (!warning) return;
153
+ console.warn(`WARNING: ${warning}`);
154
+ console.warn(
155
+ "WARNING: PI_ROTATOR_ADMIN_TOKEN protects dashboard/admin APIs, but not the native or /v1 proxy routes.",
156
+ );
157
+ console.warn();
158
+ }
159
+
149
160
  export async function main(): Promise<void> {
150
161
  console.log("=== Pi Antigravity Rotator ===");
151
162
  console.log();
@@ -181,6 +192,7 @@ export async function main(): Promise<void> {
181
192
  maybeShowStarNudge();
182
193
  bootstrapAdminToken();
183
194
  maybeWarnAboutAdminExposure(config);
195
+ maybeWarnAboutProxyExposure(config);
184
196
  warnIfUsingFallbackOAuthCreds();
185
197
  warnIfInsecureTelemetryEndpoint();
186
198
  setModelSpecsOverride(config.modelSpecs ?? null);
package/src/login.ts CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { createInterface } from "node:readline";
8
8
  import { addAccountToConfig, ensurePiAuthConfig, ensurePiModelsConfig, loadOrCreateAccountsConfig } from "./account-store.js";
9
- import { buildAuthUrl, discoverProject, exchangeAuthorizationCode, generatePkce, getOAuthClientConfig, getUserEmail } from "./oauth.js";
9
+ import { buildAuthUrl, discoverProject, exchangeAuthorizationCode, generatePkce, generateState, getOAuthClientConfig, getUserEmail } from "./oauth.js";
10
10
  import type { AccountConfig } from "./types.js";
11
11
  import { getAccountsPath } from "./paths.js";
12
12
 
@@ -42,7 +42,8 @@ export async function runLogin(): Promise<void> {
42
42
 
43
43
  const oauth = getOAuthClientConfig();
44
44
  const { verifier, challenge } = generatePkce();
45
- const authUrl = buildAuthUrl(verifier, challenge);
45
+ const state = generateState();
46
+ const authUrl = buildAuthUrl(state, challenge);
46
47
 
47
48
  console.log("1. Open this URL in your browser:");
48
49
  console.log();
@@ -66,7 +67,7 @@ export async function runLogin(): Promise<void> {
66
67
  process.exit(1);
67
68
  }
68
69
 
69
- if (parsed.state && parsed.state !== verifier) {
70
+ if (parsed.state !== state) {
70
71
  console.error("State mismatch - the URL does not match this login session.");
71
72
  process.exit(1);
72
73
  }
@@ -48,6 +48,7 @@ function getVersion(): string {
48
48
 
49
49
  // ── Cached notifications ─────────────────────────────────────────────
50
50
  let _notifications: AdminNotification[] = [];
51
+ let _initialPollTimer: ReturnType<typeof setTimeout> | null = null;
51
52
  let _pollTimer: ReturnType<typeof setInterval> | null = null;
52
53
 
53
54
  /**
@@ -100,18 +101,26 @@ export function startNotificationPoller(): void {
100
101
  }
101
102
 
102
103
  // Initial delayed fetch
103
- setTimeout(() => {
104
- void fetchNotifications();
105
- }, 15_000);
104
+ if (!_initialPollTimer) {
105
+ _initialPollTimer = setTimeout(() => {
106
+ _initialPollTimer = null;
107
+ void fetchNotifications();
108
+ }, 15_000);
109
+ if (_initialPollTimer.unref) {
110
+ _initialPollTimer.unref();
111
+ }
112
+ }
106
113
 
107
114
  // Periodic poll
108
- _pollTimer = setInterval(() => {
109
- void fetchNotifications();
110
- }, POLL_INTERVAL_MS);
111
-
112
- // Don't prevent process exit
113
- if (_pollTimer.unref) {
114
- _pollTimer.unref();
115
+ if (!_pollTimer) {
116
+ _pollTimer = setInterval(() => {
117
+ void fetchNotifications();
118
+ }, POLL_INTERVAL_MS);
119
+
120
+ // Don't prevent process exit
121
+ if (_pollTimer.unref) {
122
+ _pollTimer.unref();
123
+ }
115
124
  }
116
125
  }
117
126
 
@@ -119,6 +128,10 @@ export function startNotificationPoller(): void {
119
128
  * Stop notification polling.
120
129
  */
121
130
  export function stopNotificationPoller(): void {
131
+ if (_initialPollTimer) {
132
+ clearTimeout(_initialPollTimer);
133
+ _initialPollTimer = null;
134
+ }
122
135
  if (_pollTimer) {
123
136
  clearInterval(_pollTimer);
124
137
  _pollTimer = null;
package/src/onboarding.ts CHANGED
@@ -203,6 +203,7 @@ export function startHostedLogin(
203
203
  interface CliLoginSession {
204
204
  verifier: string;
205
205
  challenge: string;
206
+ oauthState: string;
206
207
  authUrl: string;
207
208
  createdAt: number;
208
209
  }
@@ -221,11 +222,13 @@ function pruneCliSessions(): void {
221
222
  export function serveCliLogin(res: ServerResponse): void {
222
223
  pruneCliSessions();
223
224
  const { verifier, challenge } = generatePkce();
224
- const authUrl = buildAuthUrl(verifier, challenge);
225
+ const oauthState = generateState();
226
+ const authUrl = buildAuthUrl(oauthState, challenge);
225
227
  const sessionId = generateState();
226
228
  cliLoginSessions.set(sessionId, {
227
229
  verifier,
228
230
  challenge,
231
+ oauthState,
229
232
  authUrl,
230
233
  createdAt: Date.now(),
231
234
  });
@@ -348,9 +351,11 @@ export async function handleCliLoginApi(
348
351
 
349
352
  // Parse the redirect URL to extract code
350
353
  let code: string | undefined;
354
+ let state: string | null;
351
355
  try {
352
356
  const url = new URL(redirectUrl.trim());
353
357
  code = url.searchParams.get("code") ?? undefined;
358
+ state = url.searchParams.get("state");
354
359
  } catch {
355
360
  res.writeHead(400, { "Content-Type": "application/json" });
356
361
  res.end(
@@ -373,6 +378,16 @@ export async function handleCliLoginApi(
373
378
  );
374
379
  return;
375
380
  }
381
+ if (state !== session.oauthState) {
382
+ res.writeHead(400, { "Content-Type": "application/json" });
383
+ res.end(
384
+ JSON.stringify({
385
+ ok: false,
386
+ error: "State mismatch - reload the login page and try again.",
387
+ }),
388
+ );
389
+ return;
390
+ }
376
391
 
377
392
  cliLoginSessions.delete(sessionId);
378
393