Multi user platform#5
Merged
Merged
Conversation
added 10 commits
July 19, 2026 09:10
- session-store.js: SQLite-backed auth_sessions (SHA-256 SID, sliding idle TTL + absolute TTL, remember pair, throttled touch, revoke, gc) - storage.js: users gain userId/role/status; passkeys & reset codes gain userId FK; idempotent identity migration; createUser/getUserById/ listUsers/updateUserById; saveUsersStore preserves identity - server.js: sessions survive process restart; WS upgrade binds req.authSession once (no Upgrade/connect double-auth race); 5-min WS session watchdog closes 4001 on revocation; per-user password reset; change-password revokes other sessions; /api/me/sessions list+revoke; /healthz exposes instanceId; structured auth error codes - tests: session-store (11), identity-migration (8), session-restart integration (real server reboot ×2, cookie survives)
- authz.js: capability model (discover/view/use/observe/control/execute/ fileRead/fileWrite/edit/share/delete/revealSecret/administer), tiers, effective permission, grants/revokes with audit, enumeration-safe 404 - resource-service.js: owner-aware connection/proxy/sshKey/jumpHost CRUD; server-side dependency resolution; secrets stay server-side for `use` - sharing-service.js: put/list/delete shares with tier expansion - user-service.js: create/suspend/delete, force-password-reset, last-admin protection, resource transfer on delete - storage: resource_acl + audit_events tables; ownerUserId/visibility on connections/proxies/ssh_keys/jump_hosts; ownership migration to first admin - server routes: connections/proxies/keys/jumpHosts/test/remote-execute/ rdp credentials & proxy all go through requireUser + ACL; admin user management, sharing, bootstrap, audit endpoints - tests: authz unit (10), user-isolation integration (7); full suite 151 pass
…nk (Stage 3 partial)
- workspace-service.js: (userId,clientId,workspaceId) persistence, optimistic
concurrency, secret scrubbing, restore re-filters ACL and never auto-replays
- user-settings-service.js: 3-layer merge (system > user override > admin
default); only allow-listed keys writable; system-only keys blocked for users;
non-admin never receive other users' apiKey/env values
- notes-service.js: owner/ACL/revision notes with soft-delete, restore, search,
groups, import/export, 404 enumeration safety, 409 on stale revision
- deeplink-service.js: ssh/telnet/jms parser with port-validation regex
fallback, encrypted credential blob, user-bound one-time token
- server routes: /api/me/{bootstrap,settings,workspaces}, /api/notes*,
/api/deeplinks/{prepare,:token,:token/test}, /open landing page; SSH WS
connect accepts transientToken (atomic consume, never written to assets)
- UI: notes.js controller (edit/split/preview, autosave, conflict surfacing),
app.html notes workspace + transient connection modal, style.css notes + 980px
mobile collapse, app.js openConnectionModal/transient/test/save flow,
terminal.js passes transientToken through connect, open.html/open.js
hand-off page (fragment-only, login redirect, postMessage to app)
- tests: notes-workspace-unit (3), notes-deeplink integration (5) incl.
scrubState secret strip, cross-user token rejection, restore inaccessible
filter, settings key isolation; full suite 159 pass
…tage 4/5 transitional) Stage 4 - monitor flicker (FREEZE plan §2.3): - terminal.js renderStats split into ensureStatsSkeleton (build DOM + init charts once per open) + renderStats (incremental text-node + chart-data updates). No more innerHTML rebuild or Chart teardown on every stats tick. - hideInfoModal destroys charts and clears the skeleton flag so the next open starts fresh (handles disk set / theme / instance change). - IP copy buttons bound via data-copy-stat events instead of inline onclick. Stage 5/6 - WTerm viewport (FREEZE plan §3.8 transitional): - term.viewport public facade (atBottom / followEnabled / state / follow / lock / unlock) installed by patchWTermScrollBehavior. Business code can migrate off term._scrollToBottom / _isScrolledToBottom; when the @wterm/dom fork exposes real public viewport methods only this facade moves. - Full fork of @wterm/dom with public viewport API + single render commit + program-scroll suppression remains a follow-up (requires vendored fork + CI); this commit ships the decoupling layer. tests: monitor-viewport-contract (4) - asserts renderStats no longer rebuilds innerHTML/initCharts per tick, skeleton owns the build, hideInfoModal tears down, and the viewport facade exposes a stable API. Full suite 163 pass.
Go worker (zephyr-worker/, performance-critical data plane): - main.go: WebSocket server, HMAC ticket store (one-shot, TTL), session manager with ring-buffer replay, subscriber fan-out, SSH dial w/ proxy+ jump chain, PTY shell, resize, graceful teardown - session.go: browser <-> SSH session loop; browser disconnect keeps the SSH session alive, reconnect resumes with replay - upgrader.go: nhooyr.io/websocket Conn adapter - main_test.go: ring buffer (5), ticket store, session manager, token, envelope JSON round-trip - all pass Node bridge (worker-bridge.js, control plane only): - issueForConnection / issueForTransient: ACL + secret resolution in Node, one-time ticket to Go worker, browser gets ticket+wsUrl (never secrets) - listSessions / killSession with admin vs user filtering - server routes: /api/worker/ticket, /api/worker/sessions, /api/worker/ sessions/:id/kill Bug fix (pre-existing, surfaced by deeplink test): ssh2 Client emits 'post-ready' error events (e.g. "Connection lost before handshake") after connectSSHConnection resolves - unhandled 'error' crashed the server. Added a permanent safety handler that logs and suppresses post-ready errors. Test infra: TestServer now forwards server stderr to test output and kills the child on process exit / fatal signals to prevent orphaned server.js holding the port between runs. tests: worker-bridge integration (4, mock Go worker), full suite 167 pass
…ge 8)
- ai-policy.js: AiPolicyService with disabled/admin_shared/self_managed/both
modes, per-user policy override, provider ownership + model whitelist +
quota, provider sanitization (strips apiKey for non-admin), env var isolation
- ai-agent-service.js: all routes requireUser; chat/tools/run/confirm pass
ACL-aware ctx (user, authz, resourceService, aiPolicy); every tool that
touches resources re-checks capability per-call:
list_connections / list_zephyr_resources -> discover (ACL-filtered)
open_connection -> use
remote_execute -> execute (skips denied targets)
remote_read_file -> fileRead
remote_write_file -> fileWrite
connection_create/update/delete -> edit/delete + confirmation
pendingActions bound to immutable userId; non-admin status never returns
apiKey (admin sees raw key for management); memory snippets per-user
- server.js: inject authz/resourceService/aiPolicyService/handleServiceError
into registerAiRoutes deps
- tests: ai-policy integration (5) - provider key isolation, ACL-filtered
list_connections, resource list filtering, userId-bound confirmation,
policy mode validation; full suite 172 pass
- ai-agent-service.js: add note_list/note_search/note_get/note_create/ note_update/note_delete tool definitions (gated by permissions.notes); executeAiTool cases route through notesService with ctx.user ACL. Search/list return summaries (200 chars) not full content; write ops go through confirmation; all activity logged with userId. - server.js: inject notesService into registerAiRoutes deps. - tests: ai-notes-tools integration (5) - create/search/get/isolation, cross-user B cannot read/update A's notes; full suite 177 pass
…ge 9)
- terminal.html: add "笔记" button to the terminal topbar
- terminal.js: button postMessages open-notes-for-connection to the app
shell (terminal iframe doesn't own the notes UI/ACL)
- app.js: handle open-notes-for-connection -> switchView('notes') +
notesController.filterByConnection(connectionId)
- notes.js: state.connectionFilter + loadList sends connectionId param;
new filterByConnection() method
- Existing notes workspace UI (Stage 3) already covers the three-pane
desktop layout, edit/split/preview, Markdown toolbar, autosave, mobile
collapse at 980px; this commit adds the terminal-to-notes hand-off
and "current connection" filtering required by §6.4 / §9.
Go worker: - telnet.go: minimal IAC negotiation (IAC SB TTYPE + NAWS on window resize), TCP dial, bidirectional forwarding to the WebSocket, sanitize Telnet control bytes so the browser only receives printable data + CRLF - session.go: protocol TELNET routes to serveTelnetWS instead of SSH - No fake proxy/replay/tmux-emulation fallback (plan §25); Telnet stays a first-class transport Node server: - deeplink-service.js: remove telnetUnsupported marker (Telnet is live) - server.js: /api/deeplinks/:token/test does TCP reachability probe for TELNET (minimal IAC DO TTYPE) instead of hard-rejecting; SSH WS connect points Telnet to the worker ticket path; testConnection branch handles TELNET - public/app.js: remove transient Telnet block; default port 23 for TELNET in form defaults and test payload - Tests: full suite 177 pass; Go worker unit tests pass
…I + migration & CI (Stage 5/6/14) Stage 5/6 - WTerm fork (vendoring approach, no external repo needed): - public/vendor/wterm-fork/: vendored @wterm/dom 0.1.9 sources (wterm.js, renderer.js, input.js, debug.js) + @wterm/core (wasm-bridge, transport, wasm-inline) organized under core/; all imports rewritten to relative paths so the fork loads in the browser without a bundler - wterm.js: expose public viewport API - get viewport() (atBottom, maxScroll, scrollTop, rowHeight, rows, cols, scrollToBottom, follow, lock) + isAtBottom()/scrollToBottom()/followBottom()/lockBottom() aliases - terminal.js: prefers /vendor/wterm-fork/index.js, falls back to stock @wterm/dom; patchWTermScrollBehavior has a fork path that bridges the fork's public viewport API to the legacy term.viewport facade without monkey-patching private methods - tests: wterm-fork-contract (5) - public API surface, no bare @wterm/core specifier, fork-before-stock loader, fork path doesn't touch privates Stage 14 - migration completeness + CI: - .github/workflows/ci.yml: add zephyr-worker build + vet + test job - tests/stage14-migration (7): bootstrap payload, auth/me identity, all v1 API surfaces respond (no 5xx), three-layer settings merge, workspace restore ACL filter + autoReplay=false, AI policy status, healthz instanceId Full suite 189 pass
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.