heyio 0.1.28 → 0.1.30
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 +34 -2
- package/dist/api/auth.js +44 -0
- package/dist/api/server.js +13 -1
- package/dist/copilot/tools.js +1 -1
- package/package.json +1 -1
- package/web-dist/assets/index-C1TNt53a.js +74 -0
- package/web-dist/assets/index-CKIUpauJ.css +1 -0
- package/web-dist/index.html +2 -2
- package/web-dist/assets/index-BLaCmNum.js +0 -31
- package/web-dist/assets/index-DV0XWqMF.css +0 -1
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
|
-
│ ├──
|
|
249
|
-
│ ├──
|
|
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
|
package/dist/api/auth.js
ADDED
|
@@ -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
|
package/dist/api/server.js
CHANGED
|
@@ -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
|
});
|
|
@@ -127,7 +139,7 @@ export async function startApiServer() {
|
|
|
127
139
|
if (existsSync(WEB_DIST)) {
|
|
128
140
|
app.use(express.static(WEB_DIST));
|
|
129
141
|
// SPA fallback — serve index.html for any non-API route
|
|
130
|
-
app.get("*", (_req, res) => {
|
|
142
|
+
app.get("/{*splat}", (_req, res) => {
|
|
131
143
|
res.sendFile(path.join(WEB_DIST, "index.html"));
|
|
132
144
|
});
|
|
133
145
|
console.log("[io] Web frontend enabled");
|
package/dist/copilot/tools.js
CHANGED
|
@@ -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()])
|