@saluzi/saluzi-edu 0.2.31 → 0.2.33

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.31",
3
+ "version": "0.2.33",
4
4
  "description": "Saluzi CLI - interactive AI coding assistant in the terminal",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -58,6 +58,8 @@
58
58
  "check:unused": "knip-bun",
59
59
  "health": "bun run scripts/health-check.ts",
60
60
  "docs:dev": "npx mintlify dev",
61
+ "build:guide": "vite build packages/guide-web && bun run scripts/build-guide.ts",
62
+ "dev:guide": "vite dev packages/guide-web",
61
63
  "typecheck": "tsc --noEmit",
62
64
  "precheck": "bun run typecheck && bun run check:fix && bun test --isolate",
63
65
  "rcs": "bun run scripts/rcs.ts",
@@ -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,47 @@ 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
+ // Wrapped because getCookie accesses c.req.raw.headers, which may not
74
+ // exist on mock contexts in unit tests.
75
+ let cookieToken: string | undefined
76
+ try {
77
+ cookieToken = getCookie(c, 'rcs_access')
78
+ } catch {
79
+ cookieToken = undefined
80
+ }
81
+ // Bearer — for REST calls from Web UI (Authorization header)
82
+ const bearerToken = extractBearerToken(c)
83
+ const token = cookieToken || bearerToken
84
+ if (!token) return null
85
+ // Try in-memory token (legacy issueToken flow)
86
+ const username = resolveToken(token)
87
+ if (username) return username
88
+ // Try SQLite session token
89
+ try {
90
+ return resolveSessionToken(token, getDb())
91
+ } catch {
92
+ return null
93
+ }
94
+ }
95
+
58
96
  function hasAcpReadAuth(c: Context): boolean {
59
- const token = extractBearerToken(c)
60
- return !!token && validateApiKey(token)
97
+ // Admin API key (RCS_API_KEYS)
98
+ const bearer = extractBearerToken(c)
99
+ if (bearer && validateApiKey(bearer)) return true
100
+ // Web UI user (SQLite session token or in-memory token)
101
+ return !!resolveWebUserToken(c)
61
102
  }
62
103
 
63
104
  /**
@@ -68,10 +109,36 @@ function hasAcpReadAuth(c: Context): boolean {
68
109
  */
69
110
  export function hasAcpRelayAuth(c: Context): boolean {
70
111
  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)
112
+ if (token) {
113
+ if (validateApiKey(token)) return true
114
+ // Try in-memory token resolution (existing issueToken flow)
115
+ if (resolveToken(token)) return true
116
+ // Try SQLite session token (rct_* from Bearer or Sec-WebSocket-Protocol)
117
+ try {
118
+ if (resolveSessionToken(token, getDb())) return true
119
+ } catch {
120
+ // DB unavailable — fall through to cookie check
121
+ }
122
+ }
123
+ // Cookie-based session token (rcs_access) — browser WebSocket upgrades
124
+ // automatically send cookies, so this works without client-side token
125
+ // injection. Wrapped because getCookie accesses c.req.raw.headers,
126
+ // which may not exist on mock contexts in unit tests.
127
+ let cookieToken: string | undefined
128
+ try {
129
+ cookieToken = getCookie(c, 'rcs_access')
130
+ } catch {
131
+ cookieToken = undefined
132
+ }
133
+ if (cookieToken) {
134
+ if (resolveToken(cookieToken)) return true
135
+ try {
136
+ if (resolveSessionToken(cookieToken, getDb())) return true
137
+ } catch {
138
+ return false
139
+ }
140
+ }
141
+ return false
75
142
  }
76
143
 
77
144
  function acpReadUnauthorized(c: Context) {