mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 22:55:24 +00:00
feat: turn session recording into a plugin (B13)
This commit is contained in:
1 parent
a276f6898c
commit
0af2c94983
147 files changed
+25915
-2074
No files matched your search
@@ -118,7 +118,11 @@ Plugins own their tables. A plugin declares them with `defineTable()` from
|
||||
`@termix/plugin-sdk/db` and ships migrations for `sqlite`, `postgres` and
|
||||
`mysql` under `migrations/<dialect>/NNNN_name.sql`. Table names are prefixed
|
||||
`p_<id with - as _>_`, which `defineTable` adds. `ctx.kv` is for small
|
||||
key/value state. Core `schema.ts` ends up holding only core tables.
|
||||
key/value state, and `ctx.files.dataDir()` (**B13**, needs `files:own`) is a
|
||||
per-plugin folder under `DATA_DIR`, created on first call, for state too
|
||||
large for `ctx.kv` (session recordings, uploaded files); a plugin lays out
|
||||
its own subdirectories underneath it. Core `schema.ts` ends up holding only
|
||||
core tables.
|
||||
|
||||
**One table object, three DDL emitters.** The sqlite-core definitions already
|
||||
encode correctly on every engine at query time, which is why core's own 44
|
||||
@@ -608,6 +612,22 @@ the asciicast formatting and the 300 ms batched flush (issue #1049) and hands
|
||||
the provider one `append` per batch. Service names need a dot, so "tmux" and
|
||||
"recordings" became `tmux.sessions` and `recordings.writer`.
|
||||
|
||||
**B13** is the session-recording plugin, which provides `recordings.writer`.
|
||||
It adopts `session_recordings`, keeping `userId` a plain column rather than
|
||||
`refUser()` (a recording is evidence about the host as much as the person,
|
||||
so it outlives the account) and clearing it on the `user.deleted` event
|
||||
instead of cascading. `open(meta)` returns null when the host's
|
||||
`enableSessionRecording` setting is off; otherwise a `RecordingSink` with
|
||||
`append(chunk)` (one call per already-batched chunk, so it never writes
|
||||
per-chunk itself), `persist(summary)` and `discard()` for a session that
|
||||
recorded nothing. `createFinished(input)` is the second half of the
|
||||
contract, for a caller that already wrote its own recording to disk and only
|
||||
needs the database row: remote-desktop's guacd recordings use it instead of
|
||||
importing the old repository directly, and without the plugin enabled a
|
||||
guacd recording still lands on disk but gets no row. Recording files live
|
||||
under `ctx.files.dataDir()/session_logs/<user>/<session>.cast`. Retention
|
||||
(`retentionDays`, an admin setting) runs a sweep at boot and every 24 hours.
|
||||
|
||||
A service call runs a permission check for its actor, so a share-link guest,
|
||||
who has no user, cannot make one. Guest link resolution is therefore
|
||||
published on `ctx.registry` as `sessions.sharing.guests` instead: the token is
|
||||
@@ -1161,6 +1181,7 @@ deciding, not the mechanism.
|
||||
| `hosts:read` | low | See the host list it can already see |
|
||||
| `db:own` | low | Own tables |
|
||||
| `kv:own` | low | Small key/value state |
|
||||
| `files:own` | low | Its own files on disk |
|
||||
| `secrets:own` | low | Its own encrypted secrets |
|
||||
| `settings:read-core` | low | Read core server settings |
|
||||
| `ui:surface` | low | Contribute tabs, panels and settings |
|
||||
@@ -1190,6 +1211,7 @@ Built per plugin in `src/backend/plugins/ctx.ts` and passed to `activate`.
|
||||
| `ctx.log.*` | none | **A1** |
|
||||
| `ctx.events.emit` / `.on` | `events:core` for core topics | **A1** |
|
||||
| `ctx.kv.get/set/delete/list` | `kv:own` | **A1** |
|
||||
| `ctx.files.dataDir` | `files:own` | **B13** |
|
||||
| `ctx.registry.*` | none | **A1** |
|
||||
| `ctx.services.provide` / `.get` / `.providers` | per-service RBAC | **A1**, named **B12** |
|
||||
| `ctx.secrets.offer` / `.withdraw` / `.getShared` | per-secret RBAC | **A1** |
|
||||
@@ -1668,9 +1690,13 @@ upgrade on all three engines, and the checks in step 15 are green.
|
||||
- `src/backend/database/routes/delete-user-data.ts` and the user/host
|
||||
repositories: explicit deletes of the table. A `refUser`/`refHost` column
|
||||
cascades on every engine, because the foreign key survives the rename.
|
||||
Prefer that cascade. A table that cannot carry such a column needs a
|
||||
core "user deleted" or "host deleted" event on `ctx.events`, added to
|
||||
the SDK the step 3 way; none exists yet because no plugin needed one.
|
||||
Prefer that cascade. A table that cannot carry such a column (evidence
|
||||
that has to outlive the account, like a recording) subscribes to
|
||||
`ctx.events.on("user.deleted", ...)` or `"host.deleted"` instead, added
|
||||
to the SDK the step 3 way; **B13** added `user.deleted`
|
||||
(`TOPICS.userDeleted` in `src/backend/plugins/events.ts`), emitted from
|
||||
`deleteUserAndRelatedData` right where the old direct repository call
|
||||
used to sit. `host.deleted` already existed (B7).
|
||||
- `sync-entities.ts`, if core synced it (step 9).
|
||||
- Anything else `grep -rn "<table_name>\|<tableConst>" src` finds, including
|
||||
core tests and `src/types`.
|
||||
|
||||
@@ -282,6 +282,40 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
|
||||
desktop toolbars now; the tab bar's share button is gone, so a host with the
|
||||
terminal toolbar switched off has no share button. A generic tab-menu slot
|
||||
would bring it back if that matters. Owner: D1, if wanted.
|
||||
- **B13 (session-recording):** `enable_session_logging` is still a live
|
||||
`ssh_data` column; only the copy-into-plugin-settings migration shipped
|
||||
(`session-recording-settings-migration.ts`). The legacy default was `true`;
|
||||
the plugin's enable switch defaults to `false` like every other plugin's, so
|
||||
the migration copies every host's value rather than only the ones that
|
||||
turned it off - a fresh host created after 2.9.0 defaults to recording off,
|
||||
a real behaviour change from the old column default, made once here rather
|
||||
than silently. `ssh-terminal/src/backend/terminal-socket.ts` still reads
|
||||
`resolvedHostData?.enableSessionLogging ?? hostConfig.enableSessionLogging`
|
||||
off the resolved SSH host instead of `ctx.settings.getHost`, and
|
||||
`plugins/remote-desktop/src/backend/routes.ts`'s
|
||||
`host.enableSessionLogging !== false` check, `HostEditorData.ts`,
|
||||
`HostManagerData.ts`, `quick-connect-host.ts` and the export/import sample
|
||||
all still read the column directly. The General tab's checkbox was removed
|
||||
from `HostEditor.tsx` (the schema-driven Plugins group replaces it); the
|
||||
data plumbing above was not switched over. Dropping the column in lockstep
|
||||
(`schema.ts`, `db/index.ts`, a drizzle migration per dialect,
|
||||
`schema:generate`) needs every one of those switched first. Owner: a
|
||||
dedicated follow-up step, or D0.
|
||||
- **B13 (session-recording):** `remote-desktop`'s guacd recordings now insert
|
||||
through the optional `recordings.writer` service's `createFinished(input)`
|
||||
(added alongside `open()`/`RecordingSink` for ssh-terminal's incremental
|
||||
writer) instead of importing `createCurrentSessionRecordingRepository`
|
||||
directly. Without the session-recording plugin enabled, a guacd recording
|
||||
is still written to disk but gets no `session_recordings` row and so never
|
||||
appears in the list - a warning is logged. Owner: none needed unless a
|
||||
second consumer of `recordings.writer` appears.
|
||||
- **B13 (session-recording):** `accessId` (the legacy `access_id` column,
|
||||
referencing the now-core-only `host_access` table) is carried in the
|
||||
adopted table's shape as a plain unreferenced integer, since the SDK's
|
||||
`defineTable` has no builder for a foreign key into an arbitrary core
|
||||
table beyond `refUser()`/`refHost()`. Nothing has ever populated it (true
|
||||
in core before this step too). Owner: none needed unless a future step
|
||||
wants to actually use it.
|
||||
|
||||
## Manual checks after 2.9.0
|
||||
|
||||
@@ -295,3 +329,8 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
|
||||
window, a user share joined from Active Connections, revoke and "end for
|
||||
everyone", and a collab room with two members, control hand-off, a guest
|
||||
link, rotate and end.
|
||||
- Session recording: an SSH session recorded end to end (playback matches what
|
||||
happened, including a resize mid-session), a host with recording switched
|
||||
off records nothing, retention prunes an old recording on schedule, a guacd
|
||||
RDP/VNC/Telnet recording still gets a row and plays back, and deleting a
|
||||
user anonymizes their recordings instead of removing them.
|
||||
@@ -74,6 +74,19 @@ export interface PluginKeyValue {
|
||||
list: () => Promise<string[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A folder on disk this plugin owns, for state too large for ctx.kv (session
|
||||
* recordings, uploaded files). Requires files:own.
|
||||
*/
|
||||
export interface PluginFiles {
|
||||
/**
|
||||
* The plugin's own folder under DATA_DIR, created if it does not exist yet.
|
||||
* Always the same path for this plugin; a plugin lays out its own
|
||||
* subdirectories underneath it.
|
||||
*/
|
||||
dataDir: () => Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The plugin's own tables.
|
||||
*
|
||||
@@ -1147,6 +1160,7 @@ export interface PluginContext {
|
||||
readonly log: PluginLogger;
|
||||
readonly events: PluginEvents;
|
||||
readonly kv: PluginKeyValue;
|
||||
readonly files: PluginFiles;
|
||||
readonly db: PluginDatabase;
|
||||
readonly sync: PluginSync;
|
||||
readonly registry: PluginRegistry;
|
||||
|
||||
@@ -60,6 +60,7 @@ export const CAPABILITY_CATALOG: readonly CapabilityInfo[] = [
|
||||
entry("hosts:read", "low"),
|
||||
entry("db:own", "low"),
|
||||
entry("kv:own", "low"),
|
||||
entry("files:own", "low"),
|
||||
entry("secrets:own", "low"),
|
||||
entry("settings:read-core", "low"),
|
||||
entry("ui:surface", "low"),
|
||||
|
||||
@@ -300,6 +300,12 @@ export function createFakeContext(
|
||||
list: async () => [...kv.keys()],
|
||||
},
|
||||
|
||||
files: {
|
||||
// A fake path; a test that needs a real folder uses the OS temp dir
|
||||
// itself, not this double.
|
||||
dataDir: async () => `/tmp/${pluginId}`,
|
||||
},
|
||||
|
||||
db: options.db
|
||||
? {
|
||||
...options.db,
|
||||
@@ -798,6 +804,13 @@ export function createMockCtx(
|
||||
},
|
||||
},
|
||||
|
||||
files: {
|
||||
dataDir: async () => {
|
||||
require("files:own");
|
||||
return ctx.files.dataDir();
|
||||
},
|
||||
},
|
||||
|
||||
events: {
|
||||
emit: (topic, payload) => {
|
||||
if (
|
||||
|
||||
Reference in new issue
Block a user