@devframes/service-open 0.9.7 → 0.9.9

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/dist/index.d.mts CHANGED
@@ -5,21 +5,21 @@ declare const OPEN_SERVICE_PACKAGE = "@devframes/service-open";
5
5
  declare const OPEN_SERVICE_SCOPE = "devframes:service:open";
6
6
  interface OpenServiceOptions {
7
7
  /**
8
- * Preferred editor command — one of the `KNOWN_EDITORS` `launch-editor`
8
+ * Preferred editor command, one of the `KNOWN_EDITORS` `launch-editor`
9
9
  * recognizes. Auto-detected (via `LAUNCH_EDITOR` and common defaults)
10
10
  * when omitted. On merge, the later installer's choice wins.
11
11
  */
12
12
  editor?: KnownEditor;
13
13
  /**
14
14
  * Additional directories files may be opened from, on top of the
15
- * context's `workspaceRoot` — e.g. a plugin's managed storage dir that
15
+ * context's `workspaceRoot`, e.g. a plugin's managed storage dir that
16
16
  * lives outside the workspace. Merged as a union across installers.
17
17
  */
18
18
  roots?: string[];
19
19
  }
20
20
  interface OpenInEditorInput {
21
21
  /**
22
- * File to open — absolute, or relative to the service's `workspaceRoot`
22
+ * File to open, absolute or relative to the service's `workspaceRoot`
23
23
  * (so a client with only a workspace-relative path, e.g. a message's file
24
24
  * position, can call this directly without a server-side bridge).
25
25
  */
@@ -52,7 +52,7 @@ declare module 'devframe' {
52
52
  }
53
53
  }
54
54
  /**
55
- * The open wire service — `open-in-editor` / `open-in-finder` RPC shared by
55
+ * The open wire service: `open-in-editor` / `open-in-finder` RPC shared by
56
56
  * every plugin on the host, replacing per-plugin registrations of the
57
57
  * (deprecated) `devframe/recipes/common-rpc-functions` recipes. Paths may be
58
58
  * absolute or relative to the `workspaceRoot`; the service refuses paths
package/dist/index.mjs CHANGED
@@ -1,12 +1,18 @@
1
+ import { realpath } from "node:fs/promises";
1
2
  import { defineRpcFunction } from "devframe";
2
3
  import { KNOWN_EDITORS } from "devframe/recipes/common-rpc-functions";
3
4
  import { s } from "devframe/utils/simple-schema";
4
- import { isAbsolute, relative, resolve } from "pathe";
5
+ import { dirname, isAbsolute, normalize, relative, resolve } from "pathe";
5
6
  import { defineDiagnostics } from "devframe/utils/nostics";
6
7
  //#region package.json
7
- var version = "0.9.7";
8
+ var version = "0.9.9";
8
9
  //#endregion
9
10
  //#region src/diagnostics.ts
11
+ /**
12
+ * Uses the service's own `DS_OPEN_` prefix per the built-in convention,
13
+ * keeping it collision-free with devframe core (`DF00xx`), the hub
14
+ * (`DF8xxx`), and the plugins (`DP_<SLUG>_`).
15
+ */
10
16
  const diagnostics = defineDiagnostics({
11
17
  docsBase: "https://devfra.me/errors",
12
18
  codes: { DS_OPEN_0002: {
@@ -16,10 +22,21 @@ const diagnostics = defineDiagnostics({
16
22
  });
17
23
  //#endregion
18
24
  //#region src/index.ts
25
+ /** Canonical path of the nearest existing ancestor of `absolute`. */
26
+ async function nearestExistingCanonical(absolute) {
27
+ let current = absolute;
28
+ for (;;) try {
29
+ return normalize(await realpath(current));
30
+ } catch {
31
+ const parent = dirname(current);
32
+ if (parent === current) return current;
33
+ current = parent;
34
+ }
35
+ }
19
36
  const OPEN_SERVICE_PACKAGE = "@devframes/service-open";
20
37
  const OPEN_SERVICE_SCOPE = "devframes:service:open";
21
38
  /**
22
- * The open wire service — `open-in-editor` / `open-in-finder` RPC shared by
39
+ * The open wire service: `open-in-editor` / `open-in-finder` RPC shared by
23
40
  * every plugin on the host, replacing per-plugin registrations of the
24
41
  * (deprecated) `devframe/recipes/common-rpc-functions` recipes. Paths may be
25
42
  * absolute or relative to the `workspaceRoot`; the service refuses paths
@@ -34,29 +51,38 @@ function createOpenService(options) {
34
51
  version,
35
52
  scope: OPEN_SERVICE_SCOPE,
36
53
  options,
54
+ /**
55
+ * Option sets from multiple installers merge via devframe's default
56
+ * deep-merge: `roots` union, `editor` last-wins.
57
+ */
37
58
  setup(ctx, { options }) {
38
- const allowedRoots = [ctx.workspaceRoot, ...options?.roots ?? []].map((root) => resolve(root));
59
+ const allowedRoots = [ctx.workspaceRoot, ...options?.roots ?? []].map((r) => resolve(r));
60
+ const canonicalRoots = Promise.all(allowedRoots.map((r) => nearestExistingCanonical(r)));
61
+ const within = (roots, p) => roots.some((root) => {
62
+ const rel = relative(root, p);
63
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
64
+ });
39
65
  /**
40
66
  * Resolve `path` (relative paths against `workspaceRoot`) and assert it
41
- * lands inside one of the allowed roots, or throw.
67
+ * lands inside one of the allowed roots, or throw. The lexical pass
68
+ * rejects plain `..`/absolute escapes; the canonical pass rejects a
69
+ * symlink that would redirect the open outside every root, while still
70
+ * allowing not-yet-existing files under a root.
42
71
  */
43
- function assertAllowedPath(path) {
72
+ async function assertAllowedPath(path) {
44
73
  const resolved = isAbsolute(path) ? resolve(path) : resolve(ctx.workspaceRoot, path);
45
- if (!allowedRoots.some((root) => {
46
- const rel = relative(root, resolved);
47
- return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
48
- })) throw diagnostics.DS_OPEN_0002({ path });
74
+ if (!within(allowedRoots, resolved) || !within(await canonicalRoots, await nearestExistingCanonical(resolved))) throw diagnostics.DS_OPEN_0002({ path });
49
75
  return resolved;
50
76
  }
51
77
  const api = {
52
78
  async openInEditor(input) {
53
- const path = assertAllowedPath(input.path);
79
+ const path = await assertAllowedPath(input.path);
54
80
  const target = input.line != null ? `${path}:${input.line}${input.column != null ? `:${input.column}` : ""}` : path;
55
81
  const { launchEditor } = await import("devframe/utils/launch-editor");
56
82
  launchEditor(target, input.editor ?? options?.editor);
57
83
  },
58
84
  async openInFinder(input) {
59
- const path = assertAllowedPath(input.path);
85
+ const path = await assertAllowedPath(input.path);
60
86
  const { open } = await import("devframe/utils/open");
61
87
  await open(path);
62
88
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@devframes/service-open",
3
3
  "type": "module",
4
- "version": "0.9.7",
4
+ "version": "0.9.9",
5
5
  "description": "Devframe wire service that opens files in the user's editor or reveals them in the OS file explorer.",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -28,16 +28,16 @@
28
28
  "dist"
29
29
  ],
30
30
  "peerDependencies": {
31
- "devframe": "0.9.7"
31
+ "devframe": "0.9.9"
32
32
  },
33
33
  "dependencies": {
34
34
  "pathe": "^2.0.3"
35
35
  },
36
36
  "devDependencies": {
37
- "@types/node": "^26.2.0",
37
+ "@types/node": "^26.4.0",
38
+ "devframe": "0.9.9",
38
39
  "tsdown": "^0.22.14",
39
- "vitest": "^4.1.10",
40
- "devframe": "0.9.7"
40
+ "vitest": "^4.1.11"
41
41
  },
42
42
  "scripts": {
43
43
  "build": "tsdown",