heyio 0.1.29 → 0.1.31

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/README.md CHANGED
@@ -115,6 +115,9 @@ IO stores its configuration at `~/.io/config.json`. The setup wizard (`io setup`
115
115
  | `modelTiers.medium` | `string[]` | `["claude-sonnet-4.6", "gpt-5.5", "claude-opus-4.5"]` | Models for standard tasks (features, tests, reviews) |
116
116
  | `modelTiers.low` | `string[]` | `["claude-haiku-4.5", "gpt-5.4-mini"]` | Models for simple tasks (reads, formatting, lookups) |
117
117
  | `port` | `number` | `3170` | Port for the HTTP server (API + web frontend) |
118
+ | `supabaseUrl` | `string` | — | Supabase project URL (enables web portal authentication) |
119
+ | `supabaseAnonKey` | `string` | — | Supabase anon/public API key |
120
+ | `authorizedEmail` | `string` | — | Email address allowed to access the web portal |
118
121
 
119
122
  Each `modelTiers` list is a ranked preference — IO picks the first available model at startup.
120
123
 
@@ -130,6 +133,9 @@ Each `modelTiers` list is a ranked preference — IO picks the first available m
130
133
  "selfEditEnabled": false,
131
134
  "defaultModel": "claude-sonnet-4.6",
132
135
  "port": 3170,
136
+ "supabaseUrl": "https://your-project.supabase.co",
137
+ "supabaseAnonKey": "eyJhbGciOiJIUzI1NiIs...",
138
+ "authorizedEmail": "you@example.com",
133
139
  "modelTiers": {
134
140
  "high": ["claude-opus-4.7", "claude-opus-4.6"],
135
141
  "medium": ["claude-sonnet-4.6", "gpt-5.5", "claude-opus-4.5"],
@@ -211,6 +217,29 @@ IO includes a Vue 3 web dashboard served directly from the daemon on the same po
211
217
 
212
218
  Access the web UI at `http://your-server:3170/` when running in daemon mode.
213
219
 
220
+ ### Authentication
221
+
222
+ The web portal supports optional Supabase email authentication. When enabled, users must sign in with email and password before accessing the dashboard. Only the configured `authorizedEmail` is allowed access.
223
+
224
+ **Setup:**
225
+
226
+ 1. Create a [Supabase](https://supabase.com) project (or use an existing one)
227
+ 2. Enable the **Email** auth provider in Supabase → Authentication → Providers
228
+ 3. Create your user account in Supabase → Authentication → Users
229
+ 4. Add the following to `~/.io/config.json`:
230
+
231
+ ```jsonc
232
+ {
233
+ "supabaseUrl": "https://your-project.supabase.co",
234
+ "supabaseAnonKey": "eyJhbGciOiJIUzI1NiIs...",
235
+ "authorizedEmail": "you@example.com"
236
+ }
237
+ ```
238
+
239
+ 5. Restart IO — the web portal will now require login
240
+
241
+ > **Note:** Auth is completely optional. If `supabaseUrl` is not configured, the portal runs without authentication (open access).
242
+
214
243
  ## 🏗️ Project Structure
215
244
 
216
245
  ```
@@ -241,12 +270,15 @@ src/
241
270
  ├── tui/
242
271
  │ └── index.ts # Terminal UI
243
272
  └── api/
273
+ ├── auth.ts # Supabase JWT auth middleware
244
274
  └── server.ts # Express HTTP + SSE + static frontend
245
275
 
246
276
  web/ # Vue 3 frontend (built to web-dist/)
247
277
  ├── src/
248
- │ ├── views/ # ChatView, SquadsView, SkillsView, AgentActivityView
249
- │ ├── router/ # Vue Router config
278
+ │ ├── lib/ # supabase.ts, api.ts (auth helpers)
279
+ │ ├── stores/ # Pinia stores (chat, auth)
280
+ │ ├── views/ # ChatView, SquadsView, SkillsView, AgentActivityView, LoginView
281
+ │ ├── router/ # Vue Router config + auth guard
250
282
  │ └── main.ts # App entry
251
283
  ├── vite.config.ts # Vite config (builds to ../web-dist/)
252
284
  └── package.json
@@ -0,0 +1,44 @@
1
+ import { config } from "../config.js";
2
+ /**
3
+ * Express middleware that validates Supabase JWT tokens.
4
+ * If auth is not configured (no supabaseUrl), all requests pass through.
5
+ */
6
+ export function requireAuth(req, res, next) {
7
+ // Auth not configured — pass through
8
+ if (!config.supabaseUrl || !config.supabaseAnonKey) {
9
+ next();
10
+ return;
11
+ }
12
+ const authHeader = req.headers.authorization;
13
+ const queryToken = req.query.token;
14
+ const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : undefined;
15
+ const token = bearerToken ?? queryToken;
16
+ if (!token) {
17
+ res.status(401).json({ error: "Missing or invalid authorization" });
18
+ return;
19
+ }
20
+ // Verify token by calling Supabase's /auth/v1/user endpoint
21
+ fetch(`${config.supabaseUrl}/auth/v1/user`, {
22
+ headers: {
23
+ Authorization: `Bearer ${token}`,
24
+ apikey: config.supabaseAnonKey,
25
+ },
26
+ })
27
+ .then(async (resp) => {
28
+ if (!resp.ok) {
29
+ res.status(401).json({ error: "Invalid or expired token" });
30
+ return;
31
+ }
32
+ const user = (await resp.json());
33
+ // Check authorized email if configured
34
+ if (config.authorizedEmail && user.email !== config.authorizedEmail) {
35
+ res.status(403).json({ error: "Unauthorized user" });
36
+ return;
37
+ }
38
+ next();
39
+ })
40
+ .catch(() => {
41
+ res.status(500).json({ error: "Auth verification failed" });
42
+ });
43
+ }
44
+ //# sourceMappingURL=auth.js.map
@@ -7,6 +7,7 @@ import { listSkills } from "../copilot/skills.js";
7
7
  import { listSquads, createSquad } from "../store/squads.js";
8
8
  import { getAgentInfo } from "../copilot/agents.js";
9
9
  import { IO_VERSION } from "../paths.js";
10
+ import { requireAuth } from "./auth.js";
10
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
12
  const WEB_DIST = path.resolve(__dirname, "../../web-dist");
12
13
  let messageHandler;
@@ -31,9 +32,20 @@ export async function startApiServer() {
31
32
  });
32
33
  // Build API router
33
34
  const api = express.Router();
35
+ // Public endpoints (no auth required)
34
36
  api.get("/health", (_req, res) => {
35
37
  res.json({ status: "ok" });
36
38
  });
39
+ api.get("/auth/config", (_req, res) => {
40
+ const authEnabled = !!(config.supabaseUrl && config.supabaseAnonKey);
41
+ res.json({
42
+ authEnabled,
43
+ supabaseUrl: config.supabaseUrl ?? null,
44
+ supabaseAnonKey: config.supabaseAnonKey ?? null,
45
+ });
46
+ });
47
+ // Apply auth middleware to all subsequent routes
48
+ api.use(requireAuth);
37
49
  api.get("/status", (_req, res) => {
38
50
  res.json({ version: IO_VERSION, uptime: process.uptime() });
39
51
  });
@@ -120,17 +132,20 @@ export async function startApiServer() {
120
132
  sseConnections.delete(res);
121
133
  });
122
134
  });
123
- // Mount API at /api (for frontend) and / (backward compat)
135
+ // Mount API at /api (for frontend)
124
136
  app.use("/api", api);
125
- app.use("/", api);
126
- // Serve Vue frontend if built assets exist
137
+ // Serve Vue frontend if built assets exist (before backward-compat API mount)
127
138
  if (existsSync(WEB_DIST)) {
128
139
  app.use(express.static(WEB_DIST));
129
- // SPA fallback — serve index.html for any non-API route
140
+ console.log("[io] Web frontend enabled");
141
+ }
142
+ // Backward-compat: mount API at / (after static files so HTML/CSS/JS are served first)
143
+ app.use("/", api);
144
+ // SPA fallback — serve index.html for any unmatched route
145
+ if (existsSync(WEB_DIST)) {
130
146
  app.get("/{*splat}", (_req, res) => {
131
147
  res.sendFile(path.join(WEB_DIST, "index.html"));
132
148
  });
133
- console.log("[io] Web frontend enabled");
134
149
  }
135
150
  return new Promise((resolve) => {
136
151
  app.listen(config.port, () => {
@@ -292,7 +292,7 @@ export function createTools(deps) {
292
292
  skipPermission: true,
293
293
  parameters: z.object({
294
294
  key: z
295
- .enum(["defaultModel", "telegramEnabled", "selfEditEnabled", "port"])
295
+ .enum(["defaultModel", "telegramEnabled", "selfEditEnabled", "port", "authorizedEmail"])
296
296
  .describe("Config key to update"),
297
297
  value: z
298
298
  .union([z.string(), z.number(), z.boolean()])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "heyio",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "description": "IO — a personal AI assistant built on the GitHub Copilot SDK",
5
5
  "bin": {
6
6
  "io": "dist/index.js"