@saluzi/saluzi-edu 0.2.30 → 0.2.32

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saluzi/saluzi-edu",
3
- "version": "0.2.30",
3
+ "version": "0.2.32",
4
4
  "description": "Saluzi CLI - interactive AI coding assistant in the terminal",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "@agentclientprotocol/sdk": "^0.19.0",
69
- "@saluzi/acp-link": "^0.1.4",
69
+ "@saluzi/acp-link": "^0.1.5",
70
70
  "@saluzi/codegraph": "^0.2.4",
71
71
  "@saluzi/sa-agent-tools": "^1.0.0",
72
72
  "@saluzi/sa-builtin-tools": "^1.0.1",
@@ -1,4 +1,5 @@
1
1
  import { Hono } from 'hono'
2
+ import { getCookie } from 'hono/cookie'
2
3
  import { randomUUID } from 'node:crypto'
3
4
  import type { Context } from 'hono'
4
5
  import type { UpgradeWebSocket } from 'hono/ws'
@@ -14,6 +15,8 @@ import {
14
15
  } from '../../auth/middleware'
15
16
  import { validateApiKey } from '../../auth/api-key'
16
17
  import { resolveToken } from '../../auth/token'
18
+ import { resolveSessionToken } from '../../auth/session'
19
+ import { getDb } from '../../db/sqlite'
17
20
  import {
18
21
  handleAcpWsOpen,
19
22
  handleAcpWsMessage,
@@ -55,9 +58,40 @@ function toAcpAgentResponse(env: ReturnType<typeof storeGetEnvironment> & {}) {
55
58
  }
56
59
  }
57
60
 
61
+ /**
62
+ * Resolve a Web UI user's token (SQLite session token from cookie or
63
+ * Bearer) to a userId. Returns null if the token is invalid or absent.
64
+ *
65
+ * Web UI users authenticate via rct_* SQLite session tokens stored in the
66
+ * rcs_access cookie (or Bearer for API calls). ACP read/relay endpoints
67
+ * must accept these in addition to admin API keys, otherwise non-admin
68
+ * users can list ACP sessions (via /web/sessions with sessionAuth) but
69
+ * can't open them (ACP endpoints reject with 401 "unauthorized").
70
+ */
71
+ function resolveWebUserToken(c: Context): string | null {
72
+ // Cookie (rcs_access) — primary Web UI auth path for WebSocket upgrades
73
+ const cookieToken = getCookie(c, 'rcs_access')
74
+ // Bearer — for REST calls from Web UI (Authorization header)
75
+ const bearerToken = extractBearerToken(c)
76
+ const token = cookieToken || bearerToken
77
+ if (!token) return null
78
+ // Try in-memory token (legacy issueToken flow)
79
+ const username = resolveToken(token)
80
+ if (username) return username
81
+ // Try SQLite session token
82
+ try {
83
+ return resolveSessionToken(token, getDb())
84
+ } catch {
85
+ return null
86
+ }
87
+ }
88
+
58
89
  function hasAcpReadAuth(c: Context): boolean {
59
- const token = extractBearerToken(c)
60
- return !!token && validateApiKey(token)
90
+ // Admin API key (RCS_API_KEYS)
91
+ const bearer = extractBearerToken(c)
92
+ if (bearer && validateApiKey(bearer)) return true
93
+ // Web UI user (SQLite session token or in-memory token)
94
+ return !!resolveWebUserToken(c)
61
95
  }
62
96
 
63
97
  /**
@@ -68,10 +102,30 @@ function hasAcpReadAuth(c: Context): boolean {
68
102
  */
69
103
  export function hasAcpRelayAuth(c: Context): boolean {
70
104
  const token = extractWebSocketAuthToken(c)
71
- if (!token) return false
72
- if (validateApiKey(token)) return true
73
- // Try session token resolution (in-memory issueToken tokens)
74
- return !!resolveToken(token)
105
+ if (token) {
106
+ if (validateApiKey(token)) return true
107
+ // Try in-memory token resolution (existing issueToken flow)
108
+ if (resolveToken(token)) return true
109
+ // Try SQLite session token (rct_* from Bearer or Sec-WebSocket-Protocol)
110
+ try {
111
+ if (resolveSessionToken(token, getDb())) return true
112
+ } catch {
113
+ // DB unavailable — fall through to cookie check
114
+ }
115
+ }
116
+ // Cookie-based session token (rcs_access) — browser WebSocket upgrades
117
+ // automatically send cookies, so this works without client-side token
118
+ // injection.
119
+ const cookieToken = getCookie(c, 'rcs_access')
120
+ if (cookieToken) {
121
+ if (resolveToken(cookieToken)) return true
122
+ try {
123
+ if (resolveSessionToken(cookieToken, getDb())) return true
124
+ } catch {
125
+ return false
126
+ }
127
+ }
128
+ return false
75
129
  }
76
130
 
77
131
  function acpReadUnauthorized(c: Context) {