@zenera/server 1.2.0 → 1.2.1

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 CHANGED
@@ -1,246 +1,158 @@
1
- # @zenera/server
1
+ <div align="center">
2
+ <img
3
+ src="https://raw.githubusercontent.com/zenera-ai/ZeneraNeo/main/docs/imgs/zenera-logo-with-media-queries.svg"
4
+ alt="Zenera"
5
+ width="24"
6
+ />
7
+ <div>Powered By Zenera</div>
8
+ <br>
9
+ <img
10
+ src="https://raw.githubusercontent.com/zenera-ai/ZeneraNeo/main/docs/imgs/banner-server-with-media-queries.svg"
11
+ alt="ZENERA SERVER"
12
+ width="918"
13
+ />
2
14
 
3
- A project's chats over HTTP, for every user its host vouches for: the server
4
- the agent image runs. Each turn opens the project with
5
- [`@zenera/engine`](../engine), streams its events as
6
- [AG-UI](https://docs.ag-ui.com), and runs the agents' commands in a sandbox —
7
- under bubblewrap inside the agent image, with podman anywhere else, as `zen run`.
15
+ [![npm](https://img.shields.io/npm/v/@zenera/server.svg)](https://www.npmjs.com/package/@zenera/server)
16
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/zenera-ai/ZeneraNeo/blob/main/LICENSE)
17
+ [![Node](https://img.shields.io/badge/node-%E2%89%A524-brightgreen.svg)](https://nodejs.org)
18
+
19
+ [Quickstart](#quickstart) · [Secure setup](#secure-setup) · [Commands](#commands) · [Options](#common-options) · [HTTP API](#http-api)
20
+
21
+ </div>
22
+
23
+ # `@zenera/server`
24
+
25
+ Serve a ZeneraNeo project's chats over HTTP. Each user has private sessions and
26
+ memory; agent commands run in Podman sandboxes, or under bwrap in an agent container.
27
+ Requires Node.js 24+ and the [Zenera CLI](../cli/README.md).
28
+
29
+ ## Quickstart
30
+
31
+ From a configured project:
8
32
 
9
33
  ```sh
10
- zen server start --insecure # try it here: tokens read, not checked
11
- zen server token --user ann # a token such a server takes
12
- zen server start --jwt-public-key app.pub.pem # behind zenera-app
13
- zen server start --container --insecure # in the project's agent image, under bwrap
14
- zen server check # could it start here?
15
- zen server status # a running one's report
34
+ zen server start --insecure --user ann
16
35
  ```
17
36
 
18
- With `--insecure`, startup prints a ready-to-open HTTP link to the bundled
19
- React chat page served at `/chat`. It includes the server address and an unsigned JWT in the
20
- fragment (not a query parameter); the page removes the fragment and keeps the
21
- token only in memory. The default browser user is `local`; use `--user ann` to
22
- test another user's sessions. The same works with `--container`: the outer CLI
23
- prints the host's published address and port. HTTP links preserve the fragment
24
- when opened from a terminal on macOS, unlike file-document links.
25
- The fragment ends with `&open=1` so terminal link detection preserves the unsigned
26
- JWT's trailing dot.
27
- The page route is public only in insecure mode; API requests still require a JWT.
28
- Secure mode does not expose the page. It remains a single compiled HTML asset,
29
- with no frontend dependencies or separate UI server needed at runtime.
30
-
31
- Startup uses an aligned, colored summary for the project, HTTP address, auth,
32
- sandbox, state directory, image and browser user. Container builds show one
33
- updating, width-limited line with a spinner and elapsed build time by default,
34
- even during quiet steps; piped logs retain only the final
35
- progress line. Warnings and errors remain visible in both cases.
36
- Use `zen server start --container --verbose` for every build line, grouped by
37
- stage and colored by severity. This applies to both the sandbox base image and
38
- the agent image. Colors honor `NO_COLOR` and normal terminal detection.
39
- The image reports listener readiness without repeating the host's summary or
40
- insecure warning; container startup and HTTP readiness are distinct messages.
41
-
42
- The HTML contains its JavaScript, React components and CSS; it needs no frontend
43
- server or CDN. It reuses ZeneraLite's chat messages, composer, tool cards and
44
- AG-UI reducer. It lists sessions, loads history, streams turns, follows active
45
- runs, cancels runs and deletes sessions. Connected-account actions still need
46
- zenera-app. Reopen the printed link after a page reload, or paste a dev JWT into
47
- the connection form: tokens are deliberately not saved in browser storage.
48
-
49
- Local files send `Origin: null`. Only insecure servers allow that origin and
50
- its GET/POST/DELETE preflights with Authorization and JSON headers; actual API
51
- requests still require a token. Secure servers do not enable this CORS access.
52
- A browser may ask you to allow local-network access.
53
-
54
- ```ts
55
- import { createServer } from '@zenera/server';
56
-
57
- const server = createServer({
58
- projectId: 'p1',
59
- projectDir: '/project',
60
- root: '/state',
61
- auth: {
62
- mode: 'jwt',
63
- publicKey: process.env.ZEN_SERVER_JWT_PUBLIC_KEY!,
64
- issuer: 'zenera-app',
65
- audience: 'zenera-runtime',
66
- },
67
- });
68
- await server.listen({ port: 3000, host: '0.0.0.0' });
69
- ```
37
+ Open the printed HTTP `/chat` link to list sessions, send messages, follow active
38
+ runs, cancel them, or delete a session. `--user` selects the development JWT's
39
+ subject, not a server-wide user. The default is `local`.
40
+
41
+ **Insecure mode is for local testing only:** anyone who can reach the server can
42
+ impersonate any user. API requests still need a JWT, but signatures are not checked.
43
+ The test page is available only in this mode. Its token stays in memory; after a
44
+ reload, reopen the printed link or enter a development token again.
70
45
 
71
- ## Who is calling: a JWT per request
72
-
73
- Every request but `/healthz` carries `Authorization: Bearer <jwt>`, signed by the
74
- host (zenera-app) for that one request with its Ed25519 key. The server holds
75
- only the public key (`--jwt-public-key <file>` or `$ZEN_SERVER_JWT_PUBLIC_KEY`),
76
- so neither it nor a sandboxed command that reaches its port can make one.
77
-
78
- | Claim | Checked |
79
- | ----------------------- | --------------------------------------------------------------------------- |
80
- | alg | `EdDSA` only |
81
- | `iss` | `zenera-app` (`--jwt-issuer`) |
82
- | `aud` | `zenera-runtime` (`--jwt-audience`) |
83
- | `iat` | at most 2 minutes old, 30 s of clock skew |
84
- | `prj` | this server's project id |
85
- | `req` | `"<METHOD> <url>"` of this very request: a token seen in transit is no good |
86
- | `sub` | the user, required |
87
- | `thr` | when there, must be the thread in the URL |
88
- | `role`, `email`, `name` | when there, handed to the run as the user's (`zenera` library) |
89
-
90
- Logging people in and deciding who may use which project stay in front of the
91
- server (zenera-app); the server only trusts what it signed.
92
-
93
- **`--insecure`** is for trying a server on your machine: tokens are read, not
94
- checked (any signature or none, any age, any project or request), but `sub` is
95
- still required — `zen server token --user ann` prints one. It listens on
96
- loopback only, and is refused inside the agent image unless whoever runs the
97
- container says so (`ZEN_SERVER_INSECURE_LOCAL=1`, which `--container` sets while
98
- publishing the port on loopback only).
99
-
100
- ## Root: every user in a folder of their own
46
+ To run the server in the project's agent image:
101
47
 
48
+ ```sh
49
+ zen server start --container --insecure --user ann
50
+ zen server start --container --insecure --user ann --verbose # full build logs
102
51
  ```
103
- <root>/server.json {version, projectId}: this server's
104
- <root>/users/<user>/memory/ the user's graph, shared by their chats
105
- <root>/users/<user>/sessions/<thread>/.data/ the session's state (its conversation)
106
- <root>/users/<user>/sessions/<thread>/runs/ the engine's record of each run
107
- <root>/users/<user>/sessions/<thread>/workspace/ what the agent reads and writes
108
- <root>/users/<user>/sessions/<thread>/agui/ each run's AG-UI events
52
+
53
+ Podman builds and caches the image. Default build output shows a spinner, elapsed
54
+ time, and the latest line; warnings remain visible. Use `--rebuild` to rebuild.
55
+ Press Ctrl+C to stop gracefully and save active runs.
56
+
57
+ ## Secure setup
58
+
59
+ A trusted caller, usually Zenera-app, authenticates users, checks project access,
60
+ and signs a short-lived JWT for each runtime request. The runtime holds **only
61
+ the public key**.
62
+
63
+ For a standalone caller, generate an Ed25519 pair outside the project directory:
64
+
65
+ ```sh
66
+ zen server keygen --out "$HOME/runtime-auth"
67
+ zen server start --jwt-public-key "$HOME/runtime-auth.public.pem"
109
68
  ```
110
69
 
111
- - `--root` / `$ZEN_SERVER_ROOT`, else `~/.zenera/neo/servers/<project>`; `/state`
112
- in the agent image. `server.json` makes it one project's: a root of another
113
- project, or of a newer layout, is refused.
114
- - `<user>` is `sub` as it is when it is a safe path segment (a UUID lower-cased),
115
- else `u-<sha256>`. A thread is one path segment (letters, digits, `_`, `-`).
116
- - **Memory seed**: the first time a user needs a memory, it is copied from the
117
- seed — the project's own (`memory:` in agents.yaml), `--memory-seed <dir>`, or
118
- none (`--no-memory-seed`, everyone starts empty). From then on it is theirs:
119
- their chats share it, other users never see it, and the seed is not read again.
120
- The host CLI captures the initial seed at startup, including committed SQLite
121
- WAL data and attached files. Later seed changes only affect the next server
122
- start; existing user memories are never replaced.
123
- With `--container`, this snapshot is mounted at `/seed:ro` in rollback-journal
124
- mode, so SQLite can read it without creating WAL/SHM files on the read-only
125
- project mount. The temporary snapshot is removed after the server stops
126
- (also on failed startup); private user memories remain in the server root.
127
- Snapshot file permissions allow image UID 10001 to read it even with rootful
128
- podman; the private server root still controls host access.
129
- If the container cannot be stopped, the CLI reports that error and retains
130
- its snapshot rather than removing memory that the container may still need.
131
-
132
- ## Routes
133
-
134
- | Route | What |
135
- | ------------------------------------------------ | ------------------------------------------------------------ |
136
- | `GET /healthz` | For probes; no token |
137
- | `GET /api/status` | Versions, auth, sandbox (and why not), image identity, root |
138
- | `GET /api/activity` | Whether a run is going, for idle stop |
139
- | `GET /api/sessions` | The caller's sessions, newest activity first |
140
- | `GET /api/sessions/:tid` | The chat so far, as AG-UI events |
141
- | `POST /api/sessions/:tid/runs` | A turn (or AG-UI `resume` entries), streamed as SSE |
142
- | `GET /api/sessions/:tid/runs/:runId/events` | Follow a run, or replay one that ended |
143
- | `POST /api/sessions/:tid/runs/:runId/cancel` | Stop it |
144
- | `DELETE /api/sessions/:tid` | The thread and its files |
145
- | `/api/sessions/:tid/{runs,…/trace,memory}` debug | With `debug: true` only: recorded runs, traces, memory graph |
146
-
147
- Every session route is the caller's own: `:tid` is looked up under their folder.
148
- The list returns `{ sessions: [{ id, title, createdAt, updatedAt, running }] }`;
149
- `running` is `{ runId }` or `null`, and titles come from the engine's session
150
- metadata (falling back to the ID). A thread-scoped token lists only its thread.
151
- Listing does not create sessions or memory. A new session is still created on
152
- its first turn.
153
-
154
- A run belongs to the server, not the request: it goes on when the caller leaves
155
- and can be followed again. At most `maxRuns` (`ZEN_RUNTIME_MAX_RUNS`, 4) run at
156
- once, `maxUserRuns` (`ZEN_RUNTIME_MAX_USER_RUNS`, 2) per user. An
157
- `x-zenera-mcp-token` header on a run is handed to the run's MCP plugin
158
- (`ZEN_PLUGINS`, `ZEN_MCP_BROKER_URL`).
159
-
160
- ## Sandbox
161
-
162
- Chosen once, at start:
163
-
164
- - **podman** — anywhere but the agent image, as `zen run`: a container per
165
- session on this machine. `zen server start` readies podman as `zen run` does,
166
- and a turn pulls or builds the image it needs.
167
- - **bwrap** — inside the agent image, which ships `/opt/zenera/bin/bwrap` and is
168
- itself the container: each command under bubblewrap. Commands see the image
169
- (what the project's `sandbox/Dockerfile` installed, with its `ENV`), their own
170
- workspace and home, and the `zenera` library; never the root (other chats,
171
- other users, memories), the seed, the zen home, the project (its `.env`), the
172
- server's install, `/state` or `/run/secrets`.
173
-
174
- `--sandbox` or `$ZEN_SERVER_SANDBOX` overrides the choice; `createServer` takes
175
- `sandbox: 'podman' | 'bwrap' | factory` and an optional `preflight`. A sandbox
176
- plugin in `ZEN_PLUGINS` never replaces it.
177
-
178
- The `zenera` library (Python and Node: `identity()`, `require_identity()`) is one
179
- shared read-only directory mounted at `/opt/zenera/runtime` for every session —
180
- built into the agent image, or written once per content under the zen home.
181
-
182
- `zen server start` refuses to listen when commands have nowhere to run (podman
183
- not answering, bwrap unable to isolate), when the image was built for another
184
- project environment (`ZEN_IMAGE_BUILD` / `ZEN_IMAGE_BASE`), or when the root is
185
- another project's.
186
-
187
- ## `--container`
188
-
189
- `zen server start --container` runs the server in the project's agent image on
190
- this machine, with podman:
191
-
192
- 1. The project's sandbox image is readied as for `zen run` (`sandbox/Dockerfile`).
193
- 2. The agent image is built on it: this zen's packages (`npm pack` of the ones
194
- installed), Node in `/opt/zenera`, a static bwrap, the `zenera` library, the
195
- sandbox image's `ENV` (so commands get it under bwrap). Tagged
196
- `localhost/zenera-agent:<hash>`; built again only when something changed, or
197
- with `--rebuild`.
198
- 3. `podman run`: the project read-only at `/project`, the root at `/state`, the
199
- port published on `--host` (loopback by default), the auth flags and model
200
- keys and provider settings passed through by environment name, credential files
201
- mounted as private Podman secrets, logs followed;
202
- Ctrl-C stops it.
203
-
204
- The server runs as numeric UID/GID **10001**, with `HOME=/home/zenera`, not
205
- as root or as the sandbox base image's `USER`. Commands inherit that UID unless
206
- the project's sandbox config explicitly selects another user. Rootless podman
207
- uses `--userns keep-id:uid=10001,gid=10001`: existing host-owned state stays
208
- writable, without changing its ownership. With rootful podman, the host must
209
- provide a state directory writable by UID/GID 10001; ownership is not changed
210
- automatically. Project and seed mounts must also be readable by that user.
211
- The container drops all capabilities and disables privilege escalation;
212
- bwrap uses unprivileged user namespaces.
213
-
214
- For Kubernetes, set `runAsNonRoot: true`, `runAsUser: 10001`,
215
- `runAsGroup: 10001`, `fsGroup: 10001` for the state PVC, and
216
- `allowPrivilegeEscalation: false` with all capabilities dropped. The node's
217
- security policy must allow bwrap's unprivileged user namespaces; startup
218
- reports a failed probe rather than running commands without isolation.
219
-
220
- Model keys come from the keyring (`zen key …`) and the environment, as for any
221
- run, including the project's `.env`; in the agent image, the environment.
222
- Vertex project, location and quota project, and the Bifrost endpoint, are forwarded
223
- alongside keys. Explicit `GOOGLE_APPLICATION_CREDENTIALS` takes precedence over
224
- ambient gcloud application-default credentials.
225
-
226
- File credentials use ephemeral Podman secrets readable only by UID 10001. The
227
- server stages them in its private container home before opening engines, allowing
228
- bwrap to bind container-owned files when the project's keys policy permits it.
229
- They are not embedded in the image, argv, workspaces or persistent user state.
230
- Secrets are removed after confirmed shutdown or failed startup; a failed stop
231
- retains them and the memory seed until the container can be stopped. Failed secret
232
- cleanup is reported with a manual removal command.
233
-
234
- ## Rebuilding the standalone page
235
-
236
- The source and build live in ZeneraLite's `services/zenera-app`. Run there:
70
+ `keygen` creates `<prefix>.private.pem` (PKCS#8, owner-only `0600` permissions)
71
+ and `<prefix>.public.pem` (SPKI). The parent directory must already exist.
72
+ Existing files are never overwritten; a failed write removes newly created files.
73
+ Keep the private key with the trusted caller, out of source control and runtime
74
+ containers. Zenera-app already manages its own signing identity; it does not need
75
+ this command.
76
+
77
+ Generating keys does **not** sign requests. Secure callers send an
78
+ `Authorization: Bearer <jwt>` header containing an EdDSA-signed token with:
79
+
80
+ - `iss` / `aud`: defaults `zenera-app` / `zenera-runtime`.
81
+ - `sub`: the authenticated user; `prj`: this server's project ID.
82
+ - `req`: the exact `"<METHOD> <path-and-query>"` of the request.
83
+ - `iat` / `exp`: issue time and expiry; maximum age 2 minutes, 30 seconds clock tolerance.
84
+
85
+ Use HTTPS at the ingress or a protected internal connection. A token cannot
86
+ authorize a different endpoint, but can be replayed for the same request until
87
+ it expires. The server does not perform user login or project-membership checks.
88
+
89
+ ## Commands
90
+
91
+ | Command | Use |
92
+ | ---------------------------------- | --------------------------------------------------------------- |
93
+ | `zen server start` | Start a project's runtime. |
94
+ | `zen server check` | Check whether it can start in the current environment. |
95
+ | `zen server status` | Inspect a running runtime; use `--token <jwt>` for secure mode. |
96
+ | `zen server keygen --out <prefix>` | Generate private/public signing keys. |
97
+ | `zen server token --user <id>` | Print an **unsigned** development JWT for insecure mode. |
98
+
99
+ Test an API without the browser:
237
100
 
238
101
  ```sh
239
- npm run build:chat -- --output /path/to/ZeneraNeo/packages/cli/assets/chat.html
102
+ TOKEN=$(zen server token --user ann)
103
+ curl http://127.0.0.1:3000/api/sessions \
104
+ -H "Authorization: Bearer $TOKEN"
240
105
  ```
241
106
 
242
- Without `--output`, the result is `dist/chat.html`. If a sibling `../ZeneraNeo`
243
- checkout exists next to ZeneraLite, the build also copies the result into its
244
- `packages/cli/assets/chat.html` automatically, including when `--output` is used.
245
- Without that sibling, only the requested output is written. The CLI package includes
246
- the compiled asset; ZeneraNeo needs no frontend build dependencies.
107
+ `token` also accepts `--email`, `--role`, `--name`, and `--thread`. It neither
108
+ creates an account nor signs a secure token.
109
+
110
+ ## Common options
111
+
112
+ | Option | Use / default |
113
+ | --------------------------------- | ---------------------------------------------------------------------- |
114
+ | `--project <name-or-dir>` | Select a project; otherwise use the current project. |
115
+ | `--port <n>` / `--host <addr>` | `3000` / `127.0.0.1`. Insecure mode is loopback-only. |
116
+ | `--root <dir>` | Persistent user state; `~/.zenera/neo/servers/<project>` on the host. |
117
+ | `--memory-seed <dir>` | Initial memory for new users; defaults to project memory. |
118
+ | `--no-memory-seed` | Start new users with empty memory. |
119
+ | `--jwt-public-key <file>` | Public PEM for secure mode; alternatively `ZEN_SERVER_JWT_PUBLIC_KEY`. |
120
+ | `--jwt-issuer` / `--jwt-audience` | Override the expected signer / audience. |
121
+ | `--container` | Run the server in the project image; commands use bwrap. |
122
+ | `--verbose` / `--rebuild` | Full container build logs / rebuild images. |
123
+ | `--debug` | Enable run, trace, and memory inspection routes. |
124
+
125
+ Run `zen server --help` for all options. Sessions and memory live under
126
+ `<root>/users/<user>/`; memory is private per user and shared across their sessions.
127
+ New users receive the memory seed captured at startup. Existing user memory is
128
+ never replaced by a restart.
129
+
130
+ Model credentials come from `zen key`, the environment, and the project's `.env`.
131
+ Container mode forwards provider settings, including Vertex and Bifrost, and
132
+ credential files. The gateway must be reachable from the container; host
133
+ `localhost` is not container `localhost`.
134
+ Set `sandbox.keys: false` to withhold credentials from agent-written commands,
135
+ while allowing the engine to use them for model calls.
136
+
137
+ ## HTTP API
138
+
139
+ Session routes operate only on the JWT subject's own sessions.
140
+
141
+ | Route | Use |
142
+ | ---------------------------------------------- | ---------------------------------------------- |
143
+ | `GET /healthz` | Public health probe. |
144
+ | `GET /chat` | Public test page, insecure mode only. |
145
+ | `GET /api/status` / `GET /api/activity` | Runtime status / active runs. |
146
+ | `GET /api/sessions` / `GET /api/sessions/:tid` | List sessions / load history. |
147
+ | `POST /api/sessions/:tid/runs` | Start a turn, streaming AG-UI events over SSE. |
148
+ | `GET /api/sessions/:tid/runs/:runId/events` | Follow or replay a run. |
149
+ | `POST /api/sessions/:tid/runs/:runId/cancel` | Cancel a run. |
150
+ | `DELETE /api/sessions/:tid` | Delete a session and its files. |
151
+
152
+ Runs continue after a client disconnects and can be followed again. Defaults:
153
+ 4 concurrent runs total, 2 per user (`ZEN_RUNTIME_MAX_RUNS`,
154
+ `ZEN_RUNTIME_MAX_USER_RUNS`).
155
+
156
+ For deployment and sandbox details, see [agent-image architecture](../../docs/agent-image.md).
157
+ To rebuild the bundled test page, run `npm run build:chat` from ZeneraLite's root;
158
+ it automatically copies the page into a sibling ZeneraNeo checkout when present.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenera/server",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "One user's chats with a project over HTTP: the server in the agent image, commands isolated with bubblewrap. Every user its host vouches for with a signed JWT, each in a folder of their own.",
5
5
  "keywords": [
6
6
  "agents",
@@ -45,9 +45,9 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@ag-ui/core": "^1.0.3",
48
- "@zenera/engine": "^1.2.0",
49
- "@zenera/neo": "^1.2.0",
50
- "@zenera/sandbox": "^1.2.0",
48
+ "@zenera/engine": "^1.2.1",
49
+ "@zenera/neo": "^1.2.1",
50
+ "@zenera/sandbox": "^1.2.1",
51
51
  "express": "^5.3.0",
52
52
  "jose": "^6.2.13"
53
53
  },
package/dist/state.d.ts DELETED
@@ -1,24 +0,0 @@
1
- /** 1: data/{sessions,runs,memory}, one user's. */
2
- export declare const STATE_VERSION = 1;
3
- export interface StateInfo {
4
- version: number;
5
- createdAt: string;
6
- /** whose state it is */
7
- userId: string;
8
- projectId: string;
9
- }
10
- export declare class StateError extends Error {
11
- name: string;
12
- }
13
- /**
14
- * The state directory's description, checked against who opens it: the same project and the
15
- * same user. `create: false` only reads (a check must not claim a directory): a missing file
16
- * is then `undefined`.
17
- */
18
- export declare function openState(dir: string, owner: {
19
- userId: string;
20
- projectId: string;
21
- }, opts?: {
22
- create?: boolean;
23
- }): StateInfo | undefined;
24
- //# sourceMappingURL=state.d.ts.map
package/dist/state.js DELETED
@@ -1,55 +0,0 @@
1
- // The state directory's own description: which layout it is in, so a server never reads (or
2
- // writes over) a layout it does not know. Written on first start; a newer one is refused.
3
- // STATE_VERSION - the layout this server writes
4
- // openState - create or check <state>/state.json, answering what it says
5
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
6
- import { join } from 'node:path';
7
- /** 1: data/{sessions,runs,memory}, one user's. */
8
- export const STATE_VERSION = 1;
9
- export class StateError extends Error {
10
- name = 'StateError';
11
- }
12
- /**
13
- * The state directory's description, checked against who opens it: the same project and the
14
- * same user. `create: false` only reads (a check must not claim a directory): a missing file
15
- * is then `undefined`.
16
- */
17
- export function openState(dir, owner, opts = {}) {
18
- const file = join(dir, 'state.json');
19
- if (!existsSync(file)) {
20
- if (opts.create === false)
21
- return undefined;
22
- // The user's chats: nobody else on the machine reads them.
23
- mkdirSync(dir, { recursive: true, mode: 0o700 });
24
- const info = {
25
- version: STATE_VERSION,
26
- createdAt: new Date().toISOString(),
27
- userId: owner.userId,
28
- projectId: owner.projectId,
29
- };
30
- writeFileSync(`${file}.tmp`, `${JSON.stringify(info, null, 2)}\n`);
31
- renameSync(`${file}.tmp`, file);
32
- return info;
33
- }
34
- let info;
35
- try {
36
- info = JSON.parse(readFileSync(file, 'utf8'));
37
- }
38
- catch (err) {
39
- throw new StateError(`${file} is not readable: ${err instanceof Error ? err.message : err}`);
40
- }
41
- if (!Number.isInteger(info.version) || info.version < 1) {
42
- throw new StateError(`${file} names no layout version`);
43
- }
44
- if (info.version > STATE_VERSION) {
45
- throw new StateError(`${dir} is in layout ${info.version}, written by a newer server; this one knows ${STATE_VERSION}`);
46
- }
47
- if (info.projectId !== owner.projectId) {
48
- throw new StateError(`${dir} belongs to project ${info.projectId}, not ${owner.projectId}`);
49
- }
50
- if (info.userId !== owner.userId) {
51
- throw new StateError(`${dir} belongs to user ${info.userId}, not ${owner.userId}`);
52
- }
53
- return info;
54
- }
55
- //# sourceMappingURL=state.js.map
package/dist/token.d.ts DELETED
@@ -1,14 +0,0 @@
1
- export declare const TOKEN_ENV = "ZEN_SERVER_TOKEN";
2
- export declare const MIN_TOKEN_LENGTH = 32;
3
- export interface ServerToken {
4
- token: string;
5
- /** where it came from: the environment, or the file */
6
- from: 'env' | string;
7
- }
8
- /** Where a project's server for a user keeps its token. Both are single path segments here. */
9
- export declare function tokenFile(project: string, user: string): string;
10
- export declare function serverToken(file: string, opts?: {
11
- create?: boolean;
12
- env?: NodeJS.ProcessEnv;
13
- }): ServerToken | undefined;
14
- //# sourceMappingURL=token.d.ts.map
package/dist/token.js DELETED
@@ -1,30 +0,0 @@
1
- // The server's bearer token, when its host gave none: one per project and user, in the zen home
2
- // (which no sandboxed command can see), readable by its owner only.
3
- // tokenFile - <zen home>/servers/<project>/<user>.token
4
- // serverToken - $ZEN_SERVER_TOKEN, else the file, else a new one written there (`create`)
5
- import { randomBytes } from 'node:crypto';
6
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
7
- import { home } from '@zenera/engine';
8
- import { dirname, join } from 'node:path';
9
- export const TOKEN_ENV = 'ZEN_SERVER_TOKEN';
10
- export const MIN_TOKEN_LENGTH = 32;
11
- /** Where a project's server for a user keeps its token. Both are single path segments here. */
12
- export function tokenFile(project, user) {
13
- const segment = (s) => s.replace(/[^\w.@-]/g, '_');
14
- return join(home(), 'servers', segment(project), `${segment(user)}.token`);
15
- }
16
- export function serverToken(file, opts = {}) {
17
- const given = (opts.env ?? process.env)[TOKEN_ENV]?.trim();
18
- if (given)
19
- return { token: given, from: 'env' };
20
- if (existsSync(file))
21
- return { token: readFileSync(file, 'utf8').trim(), from: file };
22
- if (!opts.create)
23
- return undefined;
24
- mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
25
- const token = randomBytes(32).toString('base64url');
26
- writeFileSync(file, `${token}\n`, { mode: 0o600, flag: 'wx' });
27
- chmodSync(file, 0o600);
28
- return { token, from: file };
29
- }
30
- //# sourceMappingURL=token.js.map