vde-open 0.1.8 → 0.1.10

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
@@ -28,7 +28,7 @@ To install from a clone of this repository (Node.js 24.21.0 is pinned in `mise.t
28
28
  pnpm install --frozen-lockfile
29
29
  pnpm build
30
30
  pnpm test:pack # builds artifacts/vde-open-<version>.tgz and verifies an install in a separate directory
31
- bun add -g "$PWD/artifacts/vde-open-0.1.8.tgz" # bun add -g needs an absolute path
31
+ bun add -g "$PWD/artifacts/vde-open-0.1.10.tgz" # bun add -g needs an absolute path
32
32
  ```
33
33
 
34
34
  ## `vde-open` and `vo`
@@ -45,7 +45,7 @@ Installing never overwrites or removes an existing `vo` (another tool's command,
45
45
  ```bash
46
46
  vo open README.md docs/design.md # open documents (starts the daemon if needed)
47
47
  vo open docs -w # open a directory and follow new documents
48
- vo ui # open the management UI (one-time URL)
48
+ vo ui # open the management UI (no authentication)
49
49
  vo list --json # list open documents
50
50
  vo search "authentication design" --json # search open documents
51
51
  vo read <documentId> --section sec_0003 --json # read a section
@@ -53,6 +53,8 @@ vo close docs/design.md # remove from the list (the file is not
53
53
  vo daemon stop # stop the daemon
54
54
  ```
55
55
 
56
+ The management UI does not require authentication. The URL printed by `vo ui --print-url` can be copied to another browser or tab on the same machine while the daemon is running. URLs that select a document work directly too. Both listeners bind to `127.0.0.1`; viewing from another machine is not supported.
57
+
56
58
  For how agents search, read, and ask questions, see [docs/agent-usage.md](docs/agent-usage.md).
57
59
 
58
60
  ## Agent skill
@@ -80,7 +82,13 @@ ln -s ~/.bun/install/global/node_modules/vde-open/skills/vde-open ~/.codex/skill
80
82
  - Scripts run only in HTML opened with `--html-mode interactive`. Use it only with HTML that you or the agent prepared and trust: scripts can load only registered files and cannot reach the management UI, the management API, or other files, but this does not block every outbound request (navigation inside the view, for example). After the daemon restarts, the HTML is static until you allow scripts again in the management UI.
81
83
  - "Differences from the original document" in the management UI lists what the view changed, why, and what to do.
82
84
 
83
- ## Markdown display limits
85
+ ## Markdown display
86
+
87
+ Use "Color palette" in the management header to choose Standard (the default), GitHub, Gruvbox, Catppuccin, or GitHub High Contrast. Light, Dark, and Match OS setting are separate controls. Both preferences are saved in the browser. The palette applies to the management UI and Markdown, including code highlighting; HTML keeps its own styling and PDFs keep their print styling.
88
+
89
+ "Wide view" in the document header expands Markdown to the available width. Press it again to return to the standard width. The width preference is saved in the browser. Tables keep their column widths and scroll horizontally when they do not fit. To scroll with the keyboard, focus the table and press the left or right arrow key.
90
+
91
+ ### Display limits
84
92
 
85
93
  Markdown is rendered with TanStack Markdown 1.0.0. It is not fully compatible with CommonMark or GFM.
86
94
 
@@ -125,7 +133,7 @@ The person answers in the answer panel of the management UI. The answers are sub
125
133
  | Symptom | What to do |
126
134
  |---|---|
127
135
  | `vo` runs a different command | Use `vde-open`, or check the order of `PATH` |
128
- | The management UI asks you to open it again from the CLI | Open a new URL with `vo ui` (each URL works once; after the daemon restarts, earlier windows stop working) |
136
+ | An earlier URL stops working after the daemon restarts | Open the current URL with `vo ui` (the port may change on restart) |
129
137
  | Exit code 8 (cannot connect to or start the daemon) | Check with `vo daemon status`, and look for leftover files with `vo doctor` |
130
138
  | Images or CSS are not shown | Open "Differences from the original document" and register them with `--assets-root` or `--asset` |
131
139
  | Search does not find a document | Check with `vo list --json` that the document is open and its `searchState` is `ready` |
@@ -137,7 +145,7 @@ The person answers in the answer panel of the management UI. The answers are sub
137
145
  |---|---|
138
146
  | Linux and macOS on CI (GitHub Actions `ubuntu-latest` and `macos-latest`, Node.js 24.21.0) | Verified (format, lint, typecheck, unit/integration, build, pack, e2e; `.github/workflows/ci.yml`) |
139
147
  | Windows on CI (`windows-latest`, Node.js 24.21.0) | Verified: build, pack smoke (install, both bins, IPC, starting and stopping the daemon, JSON output, the UI and workers), and the daemon and document integration tests. The other unit and integration tests and the e2e tests are not run on Windows |
140
- | Browsers | Chromium (Playwright's Chrome Headless Shell): the full suite is verified. Firefox and WebKit (Playwright 1.63.0): the view isolation, CSP, HTML bridge, and authentication tests (`pnpm test:e2e:cross`) are verified. Other UI interactions in Firefox and WebKit (search, answer panel, narrow screens, a list of 1,000 documents) are not verified |
148
+ | Browsers | Chromium (Playwright's Chrome Headless Shell): the full suite is verified. Firefox and WebKit (Playwright 1.63.0): the view isolation, CSP, HTML bridge, and direct URL tests (`pnpm test:e2e:cross`) are verified. Other UI interactions in Firefox and WebKit (search, answer panel, narrow screens, a list of 1,000 documents) are not verified |
141
149
  | PDF export | macOS (development machine): verified with Google Chrome 154 and Microsoft Edge 154. Linux on CI: verified with the runner's Google Chrome (e2e). Windows: printing with a real browser is not verified (the pack smoke test checks rendering the print document) |
142
150
 
143
151
  More details: [docs/performance.md](docs/performance.md) (measurements), [docs/architecture.md](docs/architecture.md) and [docs/security-model.md](docs/security-model.md) (design). Development records (in Japanese): [docs/implementation-status.md](docs/implementation-status.md), [docs/dependency-validation.md](docs/dependency-validation.md), and [docs/adr/](docs/adr/).
@@ -156,7 +164,7 @@ cd ../..
156
164
  pnpm build:watch
157
165
  ```
158
166
 
159
- `build:watch` rebuilds when the sources, the build configuration, or the packaged files change; a failed build keeps the last successful output. The next linked CLI invocation uses the new build, but the watcher does not restart your daemon: run `vo daemon restart`, then `vo ui`. Restarting keeps registered documents and saved answers, but changes the UI address and invalidates earlier browser sessions and interactive HTML permissions.
167
+ `build:watch` rebuilds when the sources, the build configuration, or the packaged files change; a failed build keeps the last successful output. The next linked CLI invocation uses the new build, but the watcher does not restart your daemon: run `vo daemon restart`, then `vo ui`. Restarting keeps registered documents and saved answers, but changes the UI address and invalidates earlier render grants and interactive HTML permissions.
160
168
 
161
169
  For frequent UI changes, use `pnpm dev` instead. It runs the daemon from source with a separate `.dev-home` and serves the UI with Vite HMR. It does not rebuild the linked CLI or restart the backend when its source changes. To run CLI commands against it, set `VDE_OPEN_HOME` to the absolute path of `.dev-home` (for example, `VDE_OPEN_HOME="$PWD/.dev-home" vo list` from the repository root).
162
170
 
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { C as decodeStateFile, D as daemonEntryPath, E as resolveStateRoot, O as referencedBlobs, S as renameWithRetry, T as resolveRuntimeLocation, a as DAEMON_LOCK_NAME, b as proofMatches, c as readIpcKey, d as inspectOwner, f as readCurrentLock, g as encodeFrame, h as FrameDecoder, i as requirePrivateDirectoryIfExists, k as version, l as readPointer, m as systemProcessProbe, n as ensurePrivateDirectory, o as POINTER_FILE, p as releaseLock, r as inspectPrivateDirectory, s as START_LOCK_NAME, t as startDaemon, u as acquireLock, v as computeProof, w as currentPathEnvironment, x as removeWithRetry, y as createNonce } from "./main-001DhaNk.js";
3
- import { A as successEnvelope, C as VdeError, D as ExitCode, E as isVdeError, O as envelopeSchema, T as isErrorCode, Y as LIMITS, k as errorEnvelope, w as exitCodeForError } from "./search-index-B7DqaWh6.js";
2
+ import { C as decodeStateFile, D as daemonEntryPath, E as resolveStateRoot, O as referencedBlobs, S as renameWithRetry, T as resolveRuntimeLocation, a as DAEMON_LOCK_NAME, b as proofMatches, c as readIpcKey, d as inspectOwner, f as readCurrentLock, g as encodeFrame, h as FrameDecoder, i as requirePrivateDirectoryIfExists, k as version, l as readPointer, m as systemProcessProbe, n as ensurePrivateDirectory, o as POINTER_FILE, p as releaseLock, r as inspectPrivateDirectory, s as START_LOCK_NAME, t as startDaemon, u as acquireLock, v as computeProof, w as currentPathEnvironment, x as removeWithRetry, y as createNonce } from "./main-BI-4X6_3.js";
3
+ import { A as successEnvelope, C as VdeError, D as ExitCode, E as isVdeError, O as envelopeSchema, T as isErrorCode, Y as LIMITS, k as errorEnvelope, w as exitCodeForError } from "./search-index-NiN4l6Hg.js";
4
4
  import fs, { fstatSync } from "node:fs";
5
5
  import { lstat, readFile, readdir, stat as stat$1, writeFile } from "node:fs/promises";
6
6
  import path, { join, resolve } from "node:path";
@@ -3815,23 +3815,22 @@ async function runCli(rawArgv, context) {
3815
3815
  }
3816
3816
  };
3817
3817
  const openBrowser = async (connection) => {
3818
- const { data } = unwrap(await connection.request("ui.bootstrap", {}));
3819
- if (await browser.open(data.bootstrapUrl)) return [];
3818
+ const { data } = unwrap(await connection.request("daemon.status", {}));
3819
+ if (await browser.open(data.uiUrl ?? "")) return [];
3820
3820
  return [{
3821
3821
  code: "W_BROWSER_OPEN_FAILED",
3822
3822
  message: "The browser could not be opened. Open the URL shown by `vde-open ui --print-url` in your browser.",
3823
3823
  details: { uiUrl: data.uiUrl }
3824
3824
  }];
3825
3825
  };
3826
- const showUi = async (printUrl) => {
3826
+ const showUi = async (printUrl, json = false) => {
3827
3827
  const connection = await control.ensure();
3828
3828
  try {
3829
3829
  if (printUrl) {
3830
- const { data } = unwrap(await connection.request("ui.bootstrap", {}));
3831
- context.stderr("This URL contains a one-time secret valid for 60 seconds. Do not share it.\n");
3832
- context.stdout(`${data.bootstrapUrl}\n`);
3830
+ const { data } = unwrap(await connection.request("daemon.status", {}));
3831
+ if (!json) context.stdout(`${data.uiUrl ?? ""}\n`);
3833
3832
  return { data: {
3834
- uiUrl: data.uiUrl,
3833
+ uiUrl: data.uiUrl ?? "",
3835
3834
  opened: false
3836
3835
  } };
3837
3836
  }
@@ -4051,12 +4050,8 @@ async function runCli(rawArgv, context) {
4051
4050
  watchCommand.command("remove").description("Remove a watch rule. Open documents stay open").argument("<watchId>", "Watch rule ID").option("--json", "Output the result as JSON", false).action(async (watchId, options) => {
4052
4051
  await execute("watch.remove", options.json, () => viaDaemon("watch.remove", { watchId }), renderRules);
4053
4052
  });
4054
- program.command("ui").description("Open the management UI in the browser").option("--print-url", "Print a one-time URL instead of opening the browser", false).option("--json", "Output the result as JSON", false).action(async (options) => {
4055
- if (options.printUrl && options.json) {
4056
- fail(new VdeError("E_INVALID_ARGUMENT", "--print-url cannot be combined with --json because its output contains a secret."), true);
4057
- return;
4058
- }
4059
- await execute("ui", options.json, () => showUi(options.printUrl), () => "");
4053
+ program.command("ui").description("Open the management UI in the browser").option("--print-url", "Print the UI URL instead of opening the browser", false).option("--json", "Output the result as JSON", false).action(async (options) => {
4054
+ await execute("ui", options.json, () => showUi(options.printUrl, options.json), () => "");
4060
4055
  });
4061
4056
  const renderStatus = (status) => status.state === "running" ? `running pid=${String(status.pid)} open documents: ${String(status.openDocuments)} ${status.uiUrl ?? ""}` : "stopped";
4062
4057
  const daemon = program.command("daemon").description("Start, inspect, and stop the daemon");
package/dist/daemon.js CHANGED
@@ -1,5 +1,5 @@
1
- import { k as version, t as startDaemon, w as currentPathEnvironment } from "./main-001DhaNk.js";
2
- import { E as isVdeError } from "./search-index-B7DqaWh6.js";
1
+ import { k as version, t as startDaemon, w as currentPathEnvironment } from "./main-BI-4X6_3.js";
2
+ import { E as isVdeError } from "./search-index-NiN4l6Hg.js";
3
3
  //#region src/daemon.ts
4
4
  function notifyParent(message) {
5
5
  if (!process.send) return;
@@ -1,5 +1,5 @@
1
1
  import { C as roleAllowed, N as __commonJSMin, P as __toESM, S as isValidLogicalPath, _ as classifyLink, b as encodeLogicalPath, d as analyzeDocument, g as assetTypeOf, u as ParseLimitError, v as classifyReference, x as hasHiddenSegment, y as dirnameOfLogicalPath } from "./html-static-BDV6MgA7.js";
2
- import { $ as literal, A as successEnvelope, B as readParamsSchema, C as VdeError, D as ExitCode, E as isVdeError, F as htmlModeChangeParamsSchema, G as sha256Schema, H as reorderParamsSchema, I as htmlModeSchema, J as watchRuleSchema, K as sourceKindSchema, L as linkOpenParamsSchema, M as closeParamsSchema, N as documentFormatSchema, P as documentIdSchema, Q as boolean, R as listParamsSchema, S as validateAnswers, U as revisionSchema, V as renderGrantParamsSchema, W as searchParamsSchema, X as _enum, Y as LIMITS, Z as array, _ as feedbackWaitParamsSchema, a as uniqueTokens, at as canonicalJson, b as submissionSchema, c as feedbackAckParamsSchema, d as feedbackDraftParamsSchema, et as number, f as feedbackForgetParamsSchema, g as feedbackSubmitParamsSchema, h as feedbackStatusSchema, i as codePointLength, it as ZodError, j as assetRecordSchema, k as errorEnvelope, l as feedbackCancelParamsSchema, m as feedbackListParamsSchema, n as batchesOf, nt as strictObject, o as answersSchema, p as feedbackIdParamsSchema, q as sourceStateSchema, r as partsOf, rt as string, s as cancellationSchema, tt as record, u as feedbackCreateParamsSchema, v as questionnaireSchema, w as exitCodeForError, x as utf8Length, y as requestIdSchema, z as openParamsSchema } from "./search-index-B7DqaWh6.js";
2
+ import { $ as literal, A as successEnvelope, B as readParamsSchema, C as VdeError, D as ExitCode, E as isVdeError, F as htmlModeChangeParamsSchema, G as sha256Schema, H as reorderParamsSchema, I as htmlModeSchema, J as watchRuleSchema, K as sourceKindSchema, L as linkOpenParamsSchema, M as closeParamsSchema, N as documentFormatSchema, P as documentIdSchema, Q as boolean, R as listParamsSchema, S as validateAnswers, U as revisionSchema, V as renderGrantParamsSchema, W as searchParamsSchema, X as _enum, Y as LIMITS, Z as array, _ as feedbackWaitParamsSchema, a as uniqueTokens, at as canonicalJson, b as submissionSchema, c as feedbackAckParamsSchema, d as feedbackDraftParamsSchema, et as number, f as feedbackForgetParamsSchema, g as feedbackSubmitParamsSchema, h as feedbackStatusSchema, i as codePointLength, it as ZodError, j as assetRecordSchema, k as errorEnvelope, l as feedbackCancelParamsSchema, m as feedbackListParamsSchema, n as batchesOf, nt as strictObject, o as answersSchema, p as feedbackIdParamsSchema, q as sourceStateSchema, r as partsOf, rt as string, s as cancellationSchema, tt as record, u as feedbackCreateParamsSchema, v as questionnaireSchema, w as exitCodeForError, x as utf8Length, y as requestIdSchema, z as openParamsSchema } from "./search-index-NiN4l6Hg.js";
3
3
  import { n as scanReferences, r as extractTitle } from "./render-BX3arVZj.js";
4
4
  import { n as measureHeap, t as collectGarbage } from "./heap-C3qHm7SE.js";
5
5
  import { accessSync, constants, existsSync, stat, statSync, unwatchFile, watch, watchFile } from "node:fs";
@@ -154,7 +154,7 @@ function parseStrictJson(text) {
154
154
  }
155
155
  //#endregion
156
156
  //#region package.json
157
- var version = "0.1.8";
157
+ var version = "0.1.10";
158
158
  //#endregion
159
159
  //#region src/documents/cursor.ts
160
160
  function invalidCursor(reason) {
@@ -6900,12 +6900,9 @@ function decodeUtf8(bytes) {
6900
6900
  }
6901
6901
  }
6902
6902
  function createRenderService(options) {
6903
- const { store, documents, sessions, parse } = options;
6903
+ const { store, documents, parse } = options;
6904
6904
  const grants = /* @__PURE__ */ new Map();
6905
6905
  const snapshots = /* @__PURE__ */ new Map();
6906
- sessions.onAnyRevoke((sessionId) => {
6907
- for (const [key, grant] of grants) if (grant.sessionId === sessionId) grants.delete(key);
6908
- });
6909
6906
  const snapshotOf = async (documentId, revision, mode = "static", withSdk = false) => {
6910
6907
  const { record, entry } = documents.describeRevision(documentId, revision);
6911
6908
  const cacheKey = `${entry.revision}\n${entry.documentLogicalPath}\n${mode}\n${String(withSdk)}`;
@@ -7004,14 +7001,13 @@ function createRenderService(options) {
7004
7001
  const liveGrant = (key) => {
7005
7002
  const grant = grants.get(key);
7006
7003
  if (!grant) return null;
7007
- const record = documents.state.documents[grant.documentId];
7008
- if (!sessions.isActiveId(grant.sessionId) || !record?.isOpen || documents.openEpoch(grant.documentId) !== grant.openEpoch || grant.mode === "interactive" && documents.interactiveGeneration(grant.documentId) !== grant.permission) {
7004
+ if (!documents.state.documents[grant.documentId]?.isOpen || documents.openEpoch(grant.documentId) !== grant.openEpoch || grant.mode === "interactive" && documents.interactiveGeneration(grant.documentId) !== grant.permission) {
7009
7005
  grants.delete(key);
7010
7006
  return null;
7011
7007
  }
7012
7008
  return grant;
7013
7009
  };
7014
- const issue = async (sessionId, params, context) => {
7010
+ const issue = async (params, context) => {
7015
7011
  const interactive = params.mode === "interactive";
7016
7012
  const permissionOf = () => {
7017
7013
  if (!interactive) return null;
@@ -7054,7 +7050,6 @@ function createRenderService(options) {
7054
7050
  }
7055
7051
  const key = randomBytes(32).toString("base64url");
7056
7052
  grants.set(key, {
7057
- sessionId,
7058
7053
  documentId: record.documentId,
7059
7054
  openEpoch,
7060
7055
  revision: entry.revision,
@@ -7064,8 +7059,11 @@ function createRenderService(options) {
7064
7059
  bridge,
7065
7060
  missing: /* @__PURE__ */ new Set()
7066
7061
  });
7067
- const owned = [...grants].filter(([, grant]) => grant.sessionId === sessionId);
7068
- for (const [oldKey] of owned.slice(0, Math.max(0, owned.length - LIMITS.renderGrantsPerSession))) grants.delete(oldKey);
7062
+ while (grants.size > LIMITS.renderGrants) {
7063
+ const oldest = grants.keys().next().value;
7064
+ if (oldest === void 0) break;
7065
+ grants.delete(oldest);
7066
+ }
7069
7067
  const filesBaseUrl = `${options.previewOrigin()}/r/${key}/files/`;
7070
7068
  return {
7071
7069
  grant: key,
@@ -7100,14 +7098,14 @@ function createRenderService(options) {
7100
7098
  snapshots: snapshots.size
7101
7099
  };
7102
7100
  },
7103
- async createGrant(sessionId, rawParams, context) {
7101
+ async createGrant(rawParams, context) {
7104
7102
  const params = renderGrantParamsSchema.parse(rawParams);
7105
- return issue(sessionId, {
7103
+ return issue({
7106
7104
  ...params,
7107
7105
  requestId: null
7108
7106
  }, context);
7109
7107
  },
7110
- async createGrantForRequest(sessionId, requestId, context) {
7108
+ async createGrantForRequest(requestId, context) {
7111
7109
  const request = Object.hasOwn(store.payload.feedbackRequests, requestId) ? store.payload.feedbackRequests[requestId] : void 0;
7112
7110
  if (!request) throw new VdeError("E_REQUEST_NOT_FOUND", "The question was not found.", { requestId });
7113
7111
  if (request.status !== "pending") throw new VdeError("E_REQUEST_NOT_PENDING", "The question is not pending.", {
@@ -7115,16 +7113,16 @@ function createRenderService(options) {
7115
7113
  status: request.status
7116
7114
  });
7117
7115
  const interactive = request.renderMode === "interactive" && documents.interactiveAllowed(request.documentId);
7118
- return issue(sessionId, {
7116
+ return issue({
7119
7117
  documentId: request.documentId,
7120
7118
  revision: request.revision,
7121
7119
  mode: interactive ? "interactive" : "static",
7122
7120
  requestId: interactive ? request.requestId : null
7123
7121
  }, context);
7124
7122
  },
7125
- bridgeOf(sessionId, key) {
7123
+ bridgeOf(key) {
7126
7124
  const grant = liveGrant(key);
7127
- if (!grant?.bridge || grant.sessionId !== sessionId) return null;
7125
+ if (!grant?.bridge) return null;
7128
7126
  if (store.payload.feedbackRequests[grant.bridge.requestId]?.status !== "pending") return null;
7129
7127
  return {
7130
7128
  requestId: grant.bridge.requestId,
@@ -7132,9 +7130,9 @@ function createRenderService(options) {
7132
7130
  revision: grant.revision
7133
7131
  };
7134
7132
  },
7135
- release(sessionId, keys) {
7133
+ release(keys) {
7136
7134
  let released = 0;
7137
- for (const key of keys) if (grants.get(key)?.sessionId === sessionId && grants.delete(key)) released += 1;
7135
+ for (const key of keys) if (grants.delete(key)) released += 1;
7138
7136
  return released;
7139
7137
  },
7140
7138
  async resolve(key, logicalPath) {
@@ -7162,9 +7160,9 @@ function createRenderService(options) {
7162
7160
  mode: grant.mode
7163
7161
  };
7164
7162
  },
7165
- missingOf(sessionId, key) {
7163
+ missingOf(key) {
7166
7164
  const grant = liveGrant(key);
7167
- if (!grant || grant.sessionId !== sessionId) return [];
7165
+ if (!grant) return [];
7168
7166
  return [...grant.missing];
7169
7167
  },
7170
7168
  pruneClosed() {
@@ -11016,7 +11014,7 @@ function createEventQueue(options) {
11016
11014
  if (pending === 0) progressAt = now();
11017
11015
  pending += 1;
11018
11016
  writes = writes.then(() => {
11019
- if (stopped || !options.beforeWrite()) return void 0;
11017
+ if (stopped) return void 0;
11020
11018
  return write();
11021
11019
  }).then(() => {
11022
11020
  progressAt = now();
@@ -11216,27 +11214,9 @@ async function startManagementServer(deps, options = {}) {
11216
11214
  if (Number(c.req.header("content-length") ?? "0") > 0 && !(c.req.header("content-type") ?? "").startsWith("application/json")) return fail(c, new VdeError("E_INVALID_ARGUMENT", "The body must be sent as JSON."));
11217
11215
  }
11218
11216
  if (deps.isStopping()) return fail(c, new VdeError("E_DAEMON_STOPPING", "The daemon is stopping."));
11219
- if (c.req.path === `${API_PREFIX}/sessions/bootstrap` && method === "POST") return next();
11220
- const authorization = c.req.header("authorization") ?? "";
11221
- const token = authorization.startsWith("Bearer ") ? authorization.slice(7) : "";
11222
- if (token === "" || !deps.sessions.authenticate(token)) return fail(c, new VdeError("E_UNAUTHORIZED", "Authentication is required. Open again from the CLI."));
11223
- c.set("token", token);
11224
11217
  return next();
11225
11218
  });
11226
11219
  const api = new Hono();
11227
- api.post("/sessions/bootstrap", async (c) => {
11228
- const body = strictObject({ ticket: string().min(1).max(256) }).parse(await c.req.json());
11229
- const token = deps.sessions.exchange(body.ticket);
11230
- if (!token) return fail(c, new VdeError("E_UNAUTHORIZED", "This URL has already been used or has expired."));
11231
- return c.json(successEnvelope({
11232
- token,
11233
- idleTimeoutSeconds: LIMITS.sessionIdleMs / 1e3
11234
- }, { command: "sessions.bootstrap" }));
11235
- });
11236
- api.delete("/session", (c) => {
11237
- deps.sessions.revoke(c.get("token"));
11238
- return c.json(successEnvelope({ revoked: true }, { command: "session.delete" }));
11239
- });
11240
11220
  api.get("/status", (c) => {
11241
11221
  const state = deps.documents.state;
11242
11222
  const status = {
@@ -11322,28 +11302,27 @@ async function startManagementServer(deps, options = {}) {
11322
11302
  });
11323
11303
  api.post("/feedback/:id/render-grants", async (c) => {
11324
11304
  const requestId = requestIdSchema.parse(c.req.param("id"));
11325
- const result = await deps.render.createGrantForRequest(deps.sessions.idOf(c.get("token")), requestId, { origin: c.req.header("origin") ?? origin });
11305
+ const result = await deps.render.createGrantForRequest(requestId, { origin: c.req.header("origin") ?? origin });
11326
11306
  return c.json(successEnvelope(result, {
11327
11307
  command: "feedback.render-grant",
11328
11308
  catalogVersion: deps.documents.state.catalogVersion
11329
11309
  }));
11330
11310
  });
11331
- const bridgeOf = (token, grant) => {
11332
- const found = typeof grant === "string" ? deps.render.bridgeOf(deps.sessions.idOf(token), grant) : null;
11311
+ const bridgeOf = (grant) => {
11312
+ const found = typeof grant === "string" ? deps.render.bridgeOf(grant) : null;
11333
11313
  if (!found) throw new VdeError("E_RENDER_GRANT_INVALID", "The render grant has expired, so draft answers from the HTML are not accepted.");
11334
11314
  return found;
11335
11315
  };
11336
11316
  api.post("/render-grants/bridge/ready", async (c) => {
11337
11317
  const body = strictObject({ grant: string().min(1).max(128) }).parse(await c.req.json());
11338
- const { requestId } = bridgeOf(c.get("token"), body.grant);
11318
+ const { requestId } = bridgeOf(body.grant);
11339
11319
  return ok(c, "render-grants.bridge-ready", deps.feedback.getForUi(requestId));
11340
11320
  });
11341
11321
  api.put("/render-grants/bridge/draft", async (c) => {
11342
11322
  const { grant, ...draft } = parseAnswersBody(await c.req.text());
11343
- const token = c.get("token");
11344
- const { requestId } = bridgeOf(token, grant);
11323
+ const { requestId } = bridgeOf(grant);
11345
11324
  return ok(c, "render-grants.bridge-draft", await deps.feedback.updateDraft(requestId, draft, { authorize: () => {
11346
- if (bridgeOf(token, grant).requestId !== requestId) throw new VdeError("E_RENDER_GRANT_INVALID", "The render grant has changed.");
11325
+ if (bridgeOf(grant).requestId !== requestId) throw new VdeError("E_RENDER_GRANT_INVALID", "The render grant has changed.");
11347
11326
  } }));
11348
11327
  });
11349
11328
  api.post("/feedback/:id/submit", async (c) => ok(c, "feedback.submit", await deps.feedback.submit(requestIdSchema.parse(c.req.param("id")), await c.req.json())));
@@ -11356,7 +11335,7 @@ async function startManagementServer(deps, options = {}) {
11356
11335
  revision: string().optional(),
11357
11336
  mode: string().optional()
11358
11337
  }).parse(await c.req.json());
11359
- const result = await deps.render.createGrant(deps.sessions.idOf(c.get("token")), {
11338
+ const result = await deps.render.createGrant({
11360
11339
  documentId: c.req.param("id"),
11361
11340
  ...body
11362
11341
  }, { origin: c.req.header("origin") ?? origin });
@@ -11367,7 +11346,7 @@ async function startManagementServer(deps, options = {}) {
11367
11346
  });
11368
11347
  api.post("/render-grants/missing", async (c) => {
11369
11348
  const body = strictObject({ grant: string().min(1).max(128) }).parse(await c.req.json());
11370
- const missing = deps.render.missingOf(deps.sessions.idOf(c.get("token")), body.grant);
11349
+ const missing = deps.render.missingOf(body.grant);
11371
11350
  return c.json(successEnvelope({ missing }, { command: "render-grants.missing" }));
11372
11351
  });
11373
11352
  api.post("/documents/:id/html-mode", async (c) => {
@@ -11379,7 +11358,7 @@ async function startManagementServer(deps, options = {}) {
11379
11358
  });
11380
11359
  api.post("/render-grants/release", async (c) => {
11381
11360
  const body = strictObject({ grants: array(string().min(1).max(128)).max(256) }).parse(await c.req.json());
11382
- const released = deps.render.release(deps.sessions.idOf(c.get("token")), body.grants);
11361
+ const released = deps.render.release(body.grants);
11383
11362
  return c.json(successEnvelope({ released }, { command: "render-grants.release" }));
11384
11363
  });
11385
11364
  api.post("/documents/:id/links/:linkId/open", async (c) => {
@@ -11408,7 +11387,6 @@ async function startManagementServer(deps, options = {}) {
11408
11387
  closed = true;
11409
11388
  wake();
11410
11389
  };
11411
- const token = c.get("token");
11412
11390
  const queue = createEventQueue({
11413
11391
  writeEvent: (event) => stream.writeSSE({
11414
11392
  event: event.type,
@@ -11416,11 +11394,6 @@ async function startManagementServer(deps, options = {}) {
11416
11394
  data: JSON.stringify(event)
11417
11395
  }),
11418
11396
  writeHeartbeat: () => stream.write(": heartbeat\n\n"),
11419
- beforeWrite: () => {
11420
- if (deps.sessions.isActive(token)) return true;
11421
- drop();
11422
- return false;
11423
- },
11424
11397
  onError: finish
11425
11398
  });
11426
11399
  const drop = () => {
@@ -11431,7 +11404,6 @@ async function startManagementServer(deps, options = {}) {
11431
11404
  };
11432
11405
  streams.set(finish, queue);
11433
11406
  stream.onAbort(finish);
11434
- const stopWatchingSession = deps.sessions.onRevoke(token, drop);
11435
11407
  const send = (event) => {
11436
11408
  if (!closed) queue.send(event);
11437
11409
  };
@@ -11443,10 +11415,6 @@ async function startManagementServer(deps, options = {}) {
11443
11415
  });
11444
11416
  const unsubscribe = deps.events.subscribe(send);
11445
11417
  const heartbeat = setInterval(() => {
11446
- if (!deps.sessions.isActive(token)) {
11447
- drop();
11448
- return;
11449
- }
11450
11418
  if (queue.stalledFor() >= (deps.stallMs ?? LIMITS.sseStallMs)) {
11451
11419
  drop();
11452
11420
  return;
@@ -11457,7 +11425,6 @@ async function startManagementServer(deps, options = {}) {
11457
11425
  await done;
11458
11426
  } finally {
11459
11427
  clearInterval(heartbeat);
11460
- stopWatchingSession();
11461
11428
  unsubscribe();
11462
11429
  await queue.settled();
11463
11430
  streams.delete(finish);
@@ -11864,99 +11831,6 @@ async function startIpcServer(options) {
11864
11831
  };
11865
11832
  }
11866
11833
  //#endregion
11867
- //#region src/server/session-service.ts
11868
- function digest(secret) {
11869
- return createHash("sha256").update(secret).digest("hex");
11870
- }
11871
- function newSecret() {
11872
- return randomBytes(32).toString("base64url");
11873
- }
11874
- function createSessionService(now = Date.now) {
11875
- const tickets = /* @__PURE__ */ new Map();
11876
- const sessions = /* @__PURE__ */ new Map();
11877
- const revokeListeners = /* @__PURE__ */ new Map();
11878
- const anyRevokeListeners = /* @__PURE__ */ new Set();
11879
- const drop = (key) => {
11880
- if (!sessions.delete(key)) return;
11881
- const listeners = revokeListeners.get(key);
11882
- revokeListeners.delete(key);
11883
- if (listeners) for (const listener of listeners) listener();
11884
- for (const listener of anyRevokeListeners) listener(key);
11885
- };
11886
- const isActiveKey = (key) => {
11887
- const lastUsedAt = sessions.get(key);
11888
- if (lastUsedAt === void 0) return false;
11889
- if (lastUsedAt + LIMITS.sessionIdleMs < now()) {
11890
- drop(key);
11891
- return false;
11892
- }
11893
- return true;
11894
- };
11895
- const sweep = () => {
11896
- const current = now();
11897
- for (const [key, expiresAt] of tickets) if (expiresAt < current) tickets.delete(key);
11898
- for (const [key, lastUsedAt] of sessions) if (lastUsedAt + LIMITS.sessionIdleMs < current) drop(key);
11899
- };
11900
- return {
11901
- retainedCounts() {
11902
- let listeners = 0;
11903
- for (const set of revokeListeners.values()) listeners += set.size;
11904
- return {
11905
- tickets: tickets.size,
11906
- sessions: sessions.size,
11907
- revokeListeners: listeners,
11908
- anyRevokeListeners: anyRevokeListeners.size
11909
- };
11910
- },
11911
- createBootstrapTicket() {
11912
- sweep();
11913
- const ticket = newSecret();
11914
- tickets.set(digest(ticket), now() + LIMITS.bootstrapTicketTtlMs);
11915
- return ticket;
11916
- },
11917
- exchange(ticket) {
11918
- sweep();
11919
- const key = digest(ticket);
11920
- const expiresAt = tickets.get(key);
11921
- tickets.delete(key);
11922
- if (expiresAt === void 0 || expiresAt < now()) return null;
11923
- const token = newSecret();
11924
- sessions.set(digest(token), now());
11925
- return token;
11926
- },
11927
- authenticate(token) {
11928
- const key = digest(token);
11929
- const lastUsedAt = sessions.get(key);
11930
- if (lastUsedAt === void 0) return false;
11931
- if (lastUsedAt + LIMITS.sessionIdleMs < now()) {
11932
- drop(key);
11933
- return false;
11934
- }
11935
- sessions.set(key, now());
11936
- return true;
11937
- },
11938
- isActive: (token) => isActiveKey(digest(token)),
11939
- idOf: digest,
11940
- isActiveId: isActiveKey,
11941
- onAnyRevoke(listener) {
11942
- anyRevokeListeners.add(listener);
11943
- },
11944
- revoke(token) {
11945
- drop(digest(token));
11946
- },
11947
- onRevoke(token, listener) {
11948
- const key = digest(token);
11949
- const listeners = revokeListeners.get(key) ?? /* @__PURE__ */ new Set();
11950
- revokeListeners.set(key, listeners);
11951
- listeners.add(listener);
11952
- return () => {
11953
- listeners.delete(listener);
11954
- if (listeners.size === 0 && revokeListeners.get(key) === listeners) revokeListeners.delete(key);
11955
- };
11956
- }
11957
- };
11958
- }
11959
- //#endregion
11960
11834
  //#region ../../node_modules/.pnpm/readdirp@5.1.1/node_modules/readdirp/index.js
11961
11835
  const EntryTypes = {
11962
11836
  FILE_TYPE: "files",
@@ -14342,7 +14216,6 @@ async function startDaemon(options) {
14342
14216
  emit
14343
14217
  });
14344
14218
  const answering = feedback;
14345
- const sessions = createSessionService();
14346
14219
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
14347
14220
  const devOrigin = isSourceRun ? environment.env["VDE_OPEN_DEV_UI_ORIGIN"] ?? null : null;
14348
14221
  let managementOrigin = null;
@@ -14350,7 +14223,6 @@ async function startDaemon(options) {
14350
14223
  const render = createRenderService({
14351
14224
  store: openedStore,
14352
14225
  documents,
14353
- sessions,
14354
14226
  parse: parser,
14355
14227
  previewOrigin: () => previewOrigin,
14356
14228
  onMissing: (documentId) => emit({
@@ -14379,7 +14251,6 @@ async function startDaemon(options) {
14379
14251
  daemonId,
14380
14252
  version,
14381
14253
  documents,
14382
- sessions,
14383
14254
  events,
14384
14255
  render,
14385
14256
  search: searching,
@@ -14441,8 +14312,7 @@ async function startDaemon(options) {
14441
14312
  documents: documents.retainedCounts(),
14442
14313
  search: search?.retainedCounts() ?? {},
14443
14314
  render: render.retainedCounts(),
14444
- feedback: answering.retainedCounts(),
14445
- sessions: sessions.retainedCounts()
14315
+ feedback: answering.retainedCounts()
14446
14316
  },
14447
14317
  workers,
14448
14318
  activeResources,
@@ -14486,11 +14356,7 @@ async function startDaemon(options) {
14486
14356
  const result = await documents.removeWatchRule(params);
14487
14357
  watching.sync();
14488
14358
  return result;
14489
- },
14490
- "ui.bootstrap": () => ({ data: {
14491
- bootstrapUrl: `${uiUrl}#bootstrap=${sessions.createBootstrapTicket()}`,
14492
- uiUrl
14493
- } })
14359
+ }
14494
14360
  };
14495
14361
  ipc = await startIpcServer({
14496
14362
  socketPath: location.socketPath,
@@ -5601,8 +5601,6 @@ const LIMITS = {
5601
5601
  printRenderTimeoutMs: 1e4,
5602
5602
  pdfExportTimeoutMs: 6e4,
5603
5603
  watchDebounceMs: 200,
5604
- bootstrapTicketTtlMs: 6e4,
5605
- sessionIdleMs: 432e5,
5606
5604
  sseHeartbeatMs: 15e3,
5607
5605
  ssePendingEvents: 256,
5608
5606
  sseStallMs: 6e4,
@@ -5610,7 +5608,7 @@ const LIMITS = {
5610
5608
  documentAssetBytes: 100 * MiB,
5611
5609
  documentAssets: 500,
5612
5610
  cssReferenceDepth: 8,
5613
- renderGrantsPerSession: 64,
5611
+ renderGrants: 64,
5614
5612
  searchLimitDefault: 5,
5615
5613
  searchLimitMax: 50,
5616
5614
  searchQueryCodePoints: 512,
@@ -5876,14 +5874,6 @@ strictObject({
5876
5874
  openDocuments: number().int().nonnegative(),
5877
5875
  previewOrigin: string()
5878
5876
  });
5879
- strictObject({
5880
- bootstrapUrl: string(),
5881
- uiUrl: string()
5882
- });
5883
- strictObject({
5884
- token: string(),
5885
- idleTimeoutSeconds: number().int().positive()
5886
- });
5887
5877
  const renderGrantParamsSchema = strictObject({
5888
5878
  documentId: documentIdSchema,
5889
5879
  revision: revisionSchema.optional(),