@devframes/plugin-assets 0.9.8 → 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/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # `@devframes/plugin-assets`
2
2
 
3
- Browse, preview, upload, rename, and delete the files in a directory — a framework-neutral port of Nuxt DevTools' Assets tab, built as a **Vue** SPA on `@antfu/design`.
3
+ Browse, preview, upload, rename, and delete the files in a directory: a framework-neutral port of Nuxt DevTools' Assets tab, built as a **Vue** SPA on `@antfu/design`.
4
4
 
5
5
  ```sh
6
6
  pnpx @devframes/plugin-assets
7
7
  ```
8
8
 
9
- Manages `<cwd>/public` by default. The package exports `createAssetsDevframe()` for custom definitions — mount it into a Vite host with `devframeVite()` from `@devframes/vite/single`. Pass `{ dir }` to manage a different directory, `{ baseURL }` to match where the host framework serves it, or `{ write: false }` (`--read-only` on the standalone CLI) for a browse-only deployment.
9
+ Manages `<cwd>/public` by default. The package exports `createAssetsDevframe()` for custom definitions; mount it into a Vite host with `devframeVite()` from `@devframes/vite/single`. Pass `{ dir }` to manage a different directory, `{ baseURL }` to match where the host framework serves it, or `{ write: false }` (`--read-only` on the standalone CLI) for a browse-only deployment.
10
10
 
11
- Asset previews load files by their public URL, served by the host framework the devframe is mounted into (Vite/Nuxt/etc. already serve `public/` at `/`) — the devframe doesn't serve the bytes itself. The standalone CLI serves the directory for you.
11
+ Asset previews load files by their public URL, served by the host framework the devframe is mounted into (Vite/Nuxt/etc. already serve `public/` at `/`), so the devframe doesn't serve the bytes itself. The standalone CLI serves the directory for you.
12
12
 
13
- The standalone server requires devframe's trust handshake by default because it can read, write, and delete real files. The `build` CLI subcommand is disabled by default — see the docs page for why.
13
+ The standalone server requires devframe's trust handshake by default because it can read, write, and delete real files. The `build` CLI subcommand is disabled by default; see the docs page for why.
package/dist/cli.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as createAssetsDevframe } from "./src-CPYq9rfy.mjs";
1
+ import { t as createAssetsDevframe } from "./src-BpDib51Z.mjs";
2
2
  import { createCac } from "devframe/adapters/cac";
3
3
  //#region src/cli.ts
4
4
  function createAssetsCli() {
@@ -1,4 +1,4 @@
1
- import { i as CodeSnippet, n as AssetInfo, r as AssetType, t as AssetImageMeta } from "../types-BrE0klRv.mjs";
1
+ import { i as CodeSnippet, n as AssetInfo, r as AssetType, t as AssetImageMeta } from "../types-BKiX3RbA.mjs";
2
2
  import { DevframeRpcClientOptions } from "devframe/client";
3
3
  //#region src/client/index.d.ts
4
4
  declare function connectAssets(options?: DevframeRpcClientOptions): Promise<import("devframe/client").DevframeRpcClient>;
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as DEFAULT_ALLOWED_UPLOAD_EXTENSIONS, i as CodeSnippet, n as AssetInfo, r as AssetType, t as AssetImageMeta } from "./types-BrE0klRv.mjs";
1
+ import { a as DEFAULT_ALLOWED_UPLOAD_EXTENSIONS, i as CodeSnippet, n as AssetInfo, r as AssetType, t as AssetImageMeta } from "./types-BKiX3RbA.mjs";
2
2
  import { DevframeDefinition } from "devframe";
3
3
  //#region src/index.d.ts
4
4
  interface AssetsDevframeOptions {
@@ -6,7 +6,7 @@ interface AssetsDevframeOptions {
6
6
  name?: string;
7
7
  icon?: string;
8
8
  /**
9
- * Directory this devframe manages. Defaults to `<cwd>/public` — the
9
+ * Directory this devframe manages. Defaults to `<cwd>/public`, the
10
10
  * conventional static-asset folder for Nuxt, Vite, and most static-site
11
11
  * frameworks. Pass an absolute path, or one relative to the devframe's
12
12
  * own `cwd`.
@@ -14,7 +14,7 @@ interface AssetsDevframeOptions {
14
14
  dir?: string;
15
15
  /**
16
16
  * URL base the **host** serves the managed directory at, used to build
17
- * each asset's `publicPath`. Defaults to `/` — Vite, Nuxt, and most
17
+ * each asset's `publicPath`. Defaults to `/`; Vite, Nuxt, and most
18
18
  * frameworks serve their `public/` folder at the site root. Set this to
19
19
  * match a non-root deployment base (e.g. Nuxt's `app.baseURL`).
20
20
  */
@@ -34,7 +34,7 @@ interface AssetsDevframeOptions {
34
34
  port?: number;
35
35
  /**
36
36
  * Enable upload, rename, delete, and folder creation from the UI.
37
- * Enabled by default — set `false` (or pass `--read-only` on the
37
+ * Enabled by default; set `false` (or pass `--read-only` on the
38
38
  * standalone CLI) for a browse-only deployment.
39
39
  */
40
40
  write?: boolean;
@@ -45,7 +45,7 @@ interface AssetsDevframeOptions {
45
45
  uploadExtensions?: readonly string[] | '*';
46
46
  /**
47
47
  * Watch the managed directory in dev mode and push live listing updates.
48
- * Enabled by default. Set `false` to skip the file watcher — useful when a
48
+ * Enabled by default. Set `false` to skip the file watcher, useful when a
49
49
  * host mounts the devframe in a short-lived or repeatedly-recreated
50
50
  * context (tests, some SSR dev servers) where a background watcher isn't
51
51
  * wanted.
@@ -53,21 +53,21 @@ interface AssetsDevframeOptions {
53
53
  watch?: boolean;
54
54
  /**
55
55
  * Require the trust handshake on the standalone server. Enabled by
56
- * default — this devframe can read, write, and delete real files.
56
+ * default, because this devframe can read, write, and delete real files.
57
57
  */
58
58
  auth?: boolean;
59
59
  /**
60
60
  * Register the `build` CLI subcommand. Disabled by default: a static
61
- * export can only ever list file metadata from a baked snapshot — there
61
+ * export can only ever list file metadata from a baked snapshot; there
62
62
  * is no live host serving the files, and every write action is inherently
63
- * excluded from a static dump — so the command would produce a broken,
63
+ * excluded from a static dump, so the command would produce a broken,
64
64
  * write-less shell of the tool. Opt back in if that degraded export is
65
65
  * still useful to you.
66
66
  */
67
67
  build?: boolean;
68
68
  }
69
69
  /**
70
- * Create the assets manager devframe — a framework-neutral port of Nuxt
70
+ * Create the assets manager devframe, a framework-neutral port of Nuxt
71
71
  * DevTools' Assets tab. Mount it into any host via devframe's adapters, or
72
72
  * run it standalone with the bundled CLI (`devframe-assets`).
73
73
  *
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { n as DEFAULT_ALLOWED_UPLOAD_EXTENSIONS, t as createAssetsDevframe } from "./src-CPYq9rfy.mjs";
1
+ import { n as DEFAULT_ALLOWED_UPLOAD_EXTENSIONS, t as createAssetsDevframe } from "./src-BpDib51Z.mjs";
2
2
  export { DEFAULT_ALLOWED_UPLOAD_EXTENSIONS, createAssetsDevframe, createAssetsDevframe as default };
@@ -1,4 +1,4 @@
1
- import { n as AssetInfo, r as AssetType } from "../types-BrE0klRv.mjs";
1
+ import { n as AssetInfo, r as AssetType } from "../types-BKiX3RbA.mjs";
2
2
  import { DevframeNodeContext, RpcStreamingChannel } from "devframe";
3
3
  //#region src/node/context.d.ts
4
4
  interface AssetsConfig {
@@ -18,7 +18,12 @@ interface AssetsConfig {
18
18
  uploadChannel?: RpcStreamingChannel<Uint8Array>;
19
19
  }
20
20
  interface AssetsContext extends AssetsConfig {
21
- /** Resolve a root-relative path to an absolute one, rejecting escapes. */
21
+ /**
22
+ * Resolve a root-relative path to an absolute one, rejecting lexical
23
+ * escapes. Symlink-aware containment for reads and mutations lives in
24
+ * `node/paths` (`resolveAssetReadPath` / `assertAssetMutationPath`), which
25
+ * the RPC handlers call directly with {@link AssetsContext.dir}.
26
+ */
22
27
  resolvePath: (relativePath: string) => string;
23
28
  }
24
29
  /**
@@ -36,9 +41,10 @@ declare function getAssetsContext(ctx: DevframeNodeContext): AssetsContext;
36
41
  //#region src/node/paths.d.ts
37
42
  /**
38
43
  * Resolve a client-supplied, root-relative path against the managed
39
- * directory, rejecting anything that would escape it (`..` traversal, a
40
- * rogue absolute path, etc.). Every RPC handler that touches the
41
- * filesystem goes through this — never trust a path from the wire.
44
+ * directory, rejecting anything that would escape it lexically (`..`
45
+ * traversal, a rogue absolute path). The first guard every RPC handler runs;
46
+ * symlink-aware containment is layered on by {@link resolveAssetReadPath}
47
+ * (reads) and {@link assertAssetMutationPath} (mutations).
42
48
  */
43
49
  declare function resolveAssetPath(root: string, relativePath: string): string;
44
50
  //#endregion
@@ -57,7 +63,7 @@ interface SetupAssetsOptions {
57
63
  /** Extensions `upload` accepts, or `'*'` to accept any. */
58
64
  uploadExtensions: readonly string[] | '*';
59
65
  /**
60
- * URL base the managed directory is served at — the base every asset's
66
+ * URL base the managed directory is served at, the base every asset's
61
67
  * `publicPath` is resolved against.
62
68
  */
63
69
  baseURL: string;
@@ -77,7 +83,7 @@ interface SetupAssetsOptions {
77
83
  * RPCs (when enabled), and starts a live file watcher in dev mode.
78
84
  *
79
85
  * Raw asset bytes (for `<img>`/`<video>`/download previews) are served by
80
- * the **host** the plugin is attached to — Vite, Nuxt, or any framework
86
+ * the **host** the plugin is attached to: Vite, Nuxt, or any framework
81
87
  * already serves its `public/` dir at {@link SetupAssetsOptions.baseURL}.
82
88
  * The plugin only serves them itself when `serveStatic` is set (the
83
89
  * standalone CLI, which is its own host).
@@ -88,7 +94,7 @@ interface SetupAssetsOptions {
88
94
  declare function setupAssets(ctx: DevframeNodeContext, options: SetupAssetsOptions): Promise<void>;
89
95
  /**
90
96
  * Stop the live file watcher started by {@link setupAssets}. Test harnesses
91
- * wrap their dev-server `close()` with this — see `test/_utils.ts` — so a
97
+ * wrap their dev-server `close()` with this (see `test/_utils.ts`), so a
92
98
  * leaked chokidar watcher doesn't keep the process alive.
93
99
  */
94
100
  declare function disposeAssetsWatcher(ctx: DevframeNodeContext): Promise<void>;
@@ -1,3 +1,3 @@
1
- import { n as setupAssets, t as disposeAssetsWatcher } from "../node-Cit_U9yi.mjs";
2
- import { _ as getAssetsContext, d as scanAssets, g as configureAssets, u as guessAssetType, v as resolveAssetPath } from "../rpc-BOraEthN.mjs";
1
+ import { n as setupAssets, t as disposeAssetsWatcher } from "../node-Diynhcd3.mjs";
2
+ import { _ as getAssetsContext, d as scanAssets, g as configureAssets, u as guessAssetType, v as resolveAssetPath } from "../rpc-Pau8tZGm.mjs";
3
3
  export { configureAssets, disposeAssetsWatcher, getAssetsContext, guessAssetType, resolveAssetPath, scanAssets, setupAssets };
@@ -1,4 +1,4 @@
1
- import { g as configureAssets, m as UPLOAD_CHANNEL, r as writeFunctions, t as readFunctions } from "./rpc-BOraEthN.mjs";
1
+ import { g as configureAssets, m as UPLOAD_CHANNEL, r as writeFunctions, t as readFunctions } from "./rpc-Pau8tZGm.mjs";
2
2
  import process from "node:process";
3
3
  import { existsSync } from "node:fs";
4
4
  import fsp from "node:fs/promises";
@@ -15,7 +15,7 @@ const DEFAULT_PORT = 9015;
15
15
  * Watch the managed directory and broadcast {@link CHANGED_EVENT} (debounced)
16
16
  * whenever a file is added, removed, or changed, so connected UIs refresh
17
17
  * their listing live. Returns a disposer; call it when the devframe shuts
18
- * down (tests in particular — a leaked watcher keeps the process alive).
18
+ * down (tests in particular, since a leaked watcher keeps the process alive).
19
19
  */
20
20
  function watchAssetsDir(ctx, dir) {
21
21
  const notify = debounce(async () => {
@@ -26,6 +26,12 @@ function watchAssetsDir(ctx, dir) {
26
26
  }, 300);
27
27
  const watcher = watch(dir, {
28
28
  ignoreInitial: true,
29
+ /**
30
+ * Directories can nest arbitrarily deep (icon sets, generated builds
31
+ * dropped into the managed dir, …); chokidar's default depth is
32
+ * unlimited, but pin a generous cap so a runaway symlink loop can't
33
+ * spin the watcher forever.
34
+ */
29
35
  depth: 32
30
36
  });
31
37
  watcher.on("add", () => void notify()).on("unlink", () => void notify()).on("addDir", () => void notify()).on("unlinkDir", () => void notify()).on("change", () => void notify());
@@ -43,7 +49,7 @@ const watchers = /* @__PURE__ */ new WeakMap();
43
49
  * RPCs (when enabled), and starts a live file watcher in dev mode.
44
50
  *
45
51
  * Raw asset bytes (for `<img>`/`<video>`/download previews) are served by
46
- * the **host** the plugin is attached to — Vite, Nuxt, or any framework
52
+ * the **host** the plugin is attached to: Vite, Nuxt, or any framework
47
53
  * already serves its `public/` dir at {@link SetupAssetsOptions.baseURL}.
48
54
  * The plugin only serves them itself when `serveStatic` is set (the
49
55
  * standalone CLI, which is its own host).
@@ -65,7 +71,7 @@ async function setupAssets(ctx, options) {
65
71
  }
66
72
  /**
67
73
  * Stop the live file watcher started by {@link setupAssets}. Test harnesses
68
- * wrap their dev-server `close()` with this — see `test/_utils.ts` — so a
74
+ * wrap their dev-server `close()` with this (see `test/_utils.ts`), so a
69
75
  * leaked chokidar watcher doesn't keep the process alive.
70
76
  */
71
77
  async function disposeAssetsWatcher(ctx) {
@@ -8,7 +8,7 @@ interface AssetsCapabilities {
8
8
  /**
9
9
  * Lets the UI know upfront whether upload / rename / delete / mkdir are
10
10
  * available, so it can hide or disable those affordances proactively
11
- * instead of letting the user hit a "method not found" error — the same
11
+ * instead of letting the user hit a "method not found" error, the same
12
12
  * `canWrite`-gating idea the git plugin's `GitStatus.canWrite` follows.
13
13
  */
14
14
  declare const capabilities: {
@@ -273,7 +273,7 @@ interface RenameArgs {
273
273
  /** Root-relative path of the asset to rename. */
274
274
  path: string;
275
275
  /**
276
- * New base name, without an extension — the original extension is
276
+ * New base name, without an extension; the original extension is
277
277
  * always preserved (mirrors Nuxt DevTools' rename dialog exactly). Any
278
278
  * extension included here is treated as part of the base name, not
279
279
  * stripped.
@@ -371,7 +371,7 @@ declare const rename: {
371
371
  declare const UPLOAD_CHANNEL = "devframes:plugin:assets:upload";
372
372
  /**
373
373
  * Allocates an upload and returns a streaming id. Actual bytes flow over
374
- * the `upload` channel opened in `setupAssets` — see the client-side
374
+ * the `upload` channel opened in `setupAssets`; see the client-side
375
375
  * `useUpload` hook for the matching `rpc.streaming.upload()` call.
376
376
  */
377
377
  declare const upload: {
@@ -419,7 +419,7 @@ declare const upload: {
419
419
  };
420
420
  //#endregion
421
421
  //#region src/rpc/index.d.ts
422
- /** Read-only RPC — always registered. */
422
+ /** Read-only RPC, always registered. */
423
423
  declare const readFunctions: readonly [{
424
424
  name: "devframes:plugin:assets:list";
425
425
  type?: "query" | undefined;
@@ -575,7 +575,7 @@ declare const readFunctions: readonly [{
575
575
  uploadExtensions: string[] | "*";
576
576
  }>>> | undefined;
577
577
  }];
578
- /** Mutating RPC — registered only when write actions are enabled. */
578
+ /** Mutating RPC, registered only when write actions are enabled. */
579
579
  declare const writeFunctions: readonly [{
580
580
  name: "devframes:plugin:assets:upload";
581
581
  type?: "action" | undefined;
@@ -1,2 +1,2 @@
1
- import { a as readText, c as assetInfoSchema, f as deleteAssets, h as upload, i as rename, l as list, m as UPLOAD_CHANNEL, n as serverFunctions, o as readImageMeta, p as capabilities, r as writeFunctions, s as mkdir, t as readFunctions } from "../rpc-BOraEthN.mjs";
1
+ import { a as readText, c as assetInfoSchema, f as deleteAssets, h as upload, i as rename, l as list, m as UPLOAD_CHANNEL, n as serverFunctions, o as readImageMeta, p as capabilities, r as writeFunctions, s as mkdir, t as readFunctions } from "../rpc-Pau8tZGm.mjs";
2
2
  export { UPLOAD_CHANNEL, assetInfoSchema, capabilities, deleteAssets, list, mkdir, readFunctions, readImageMeta, readText, rename, serverFunctions, upload, writeFunctions };
@@ -1,4 +1,4 @@
1
- import { dirname, extname, join, resolve } from "pathe";
1
+ import { dirname, extname, join, normalize, resolve } from "pathe";
2
2
  import { createWriteStream } from "node:fs";
3
3
  import fsp from "node:fs/promises";
4
4
  import { createDefineWrapperWithContext } from "devframe/rpc";
@@ -8,6 +8,11 @@ import { glob } from "tinyglobby";
8
8
  import { joinURL } from "ufo";
9
9
  import { imageMeta } from "image-meta";
10
10
  //#region src/diagnostics.ts
11
+ /**
12
+ * Uses the plugin's own `DP_ASSETS_` prefix per the built-in plugin
13
+ * convention, keeping it collision-free with devframe core (`DF`) and the
14
+ * hub (`DF8xxx`).
15
+ */
11
16
  const diagnostics = defineDiagnostics({
12
17
  docsBase: "https://devfra.me/errors",
13
18
  codes: {
@@ -25,7 +30,7 @@ const diagnostics = defineDiagnostics({
25
30
  },
26
31
  DP_ASSETS_0004: {
27
32
  why: (p) => `No asset found at "${p.path}".`,
28
- fix: "Refresh the asset list — it may have been moved or deleted already."
33
+ fix: "Refresh the asset list; it may have been moved or deleted already."
29
34
  },
30
35
  DP_ASSETS_0005: {
31
36
  why: (p) => `Cannot create folder "${p.path}" because a file already exists there.`,
@@ -37,25 +42,67 @@ const diagnostics = defineDiagnostics({
37
42
  },
38
43
  DP_ASSETS_0007: {
39
44
  why: "The upload streaming channel is unavailable because this devframe was set up with `write: false`.",
40
- fix: "This indicates an internal registration bug — `upload` should never be reachable without `write: true`. Please report it."
45
+ fix: "This indicates an internal registration bug, since `upload` should never be reachable without `write: true`. Please report it."
41
46
  }
42
47
  }
43
48
  });
44
49
  //#endregion
45
50
  //#region src/node/paths.ts
51
+ /** realpath, pathe-normalized, or `null` when the path doesn't exist. */
52
+ async function realpath(path) {
53
+ try {
54
+ return normalize(await fsp.realpath(path));
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
46
59
  /**
47
60
  * Resolve a client-supplied, root-relative path against the managed
48
- * directory, rejecting anything that would escape it (`..` traversal, a
49
- * rogue absolute path, etc.). Every RPC handler that touches the
50
- * filesystem goes through this — never trust a path from the wire.
61
+ * directory, rejecting anything that would escape it lexically (`..`
62
+ * traversal, a rogue absolute path). The first guard every RPC handler runs;
63
+ * symlink-aware containment is layered on by {@link resolveAssetReadPath}
64
+ * (reads) and {@link assertAssetMutationPath} (mutations).
51
65
  */
52
66
  function resolveAssetPath(root, relativePath) {
53
- const cleaned = relativePath.replace(/^[/\\]+/, "");
54
67
  const normalizedRoot = resolve(root);
55
- const absolute = resolve(normalizedRoot, cleaned);
68
+ const absolute = resolve(normalizedRoot, relativePath.replace(/^[/\\]+/, ""));
56
69
  if (absolute !== normalizedRoot && !absolute.startsWith(`${normalizedRoot}/`)) throw diagnostics.DP_ASSETS_0001({ path: relativePath });
57
70
  return absolute;
58
71
  }
72
+ /**
73
+ * Resolve a path for a **read**, allowing a symlink only when its canonical
74
+ * target stays inside the canonical managed root. A target resolving outside
75
+ * throws `DP_ASSETS_0001`; a missing target is left for the caller's own read
76
+ * to fail.
77
+ */
78
+ async function resolveAssetReadPath(root, relativePath) {
79
+ const absolute = resolveAssetPath(root, relativePath);
80
+ const real = await realpath(absolute);
81
+ const canonRoot = await realpath(root) ?? resolve(root);
82
+ if (real && real !== canonRoot && !real.startsWith(`${canonRoot}/`)) throw diagnostics.DP_ASSETS_0001({ path: relativePath });
83
+ return absolute;
84
+ }
85
+ /**
86
+ * Resolve a path for a **mutation**, rejecting every pre-existing symlink
87
+ * among the path components from the managed root down to the target
88
+ * (including in-root symlinks) so a mutation can never follow a symlink out
89
+ * of, or around, the root. Only existing components are inspected, so it is
90
+ * safe for not-yet-created upload/mkdir targets; call it again after
91
+ * creating directories and right before the I/O. This closes deterministic,
92
+ * pre-existing symlink escapes, not concurrent component-swap races.
93
+ */
94
+ async function assertAssetMutationPath(root, relativePath) {
95
+ const lexRoot = resolve(root);
96
+ const absolute = resolveAssetPath(root, relativePath);
97
+ let current = await realpath(root) ?? lexRoot;
98
+ for (const segment of absolute.slice(lexRoot.length).split("/").filter(Boolean)) {
99
+ current += `/${segment}`;
100
+ const stat = await fsp.lstat(current).catch(() => null);
101
+ if (!stat) break;
102
+ if (stat.isSymbolicLink()) throw diagnostics.DP_ASSETS_0001({ path: relativePath });
103
+ }
104
+ return absolute;
105
+ }
59
106
  //#endregion
60
107
  //#region src/node/context.ts
61
108
  const configs = /* @__PURE__ */ new WeakMap();
@@ -101,7 +148,7 @@ function isExtensionAllowed(path, allowed) {
101
148
  }
102
149
  /**
103
150
  * Allocates an upload and returns a streaming id. Actual bytes flow over
104
- * the `upload` channel opened in `setupAssets` — see the client-side
151
+ * the `upload` channel opened in `setupAssets`; see the client-side
105
152
  * `useUpload` hook for the matching `rpc.streaming.upload()` call.
106
153
  */
107
154
  const upload = defineAssetsRpc$7({
@@ -118,14 +165,17 @@ const upload = defineAssetsRpc$7({
118
165
  },
119
166
  setup: (ctx) => {
120
167
  const assets = getAssetsContext(ctx);
121
- return { handler: (async ({ path }) => {
168
+ return {
169
+ /** See `list.ts` for why the async handler is cast. */
170
+ handler: (async ({ path }) => {
122
171
  if (!isExtensionAllowed(path, assets.uploadExtensions)) throw diagnostics.DP_ASSETS_0002({
123
172
  path,
124
173
  extension: extname(path).slice(1).toLowerCase() || "(none)",
125
174
  allowed: assets.uploadExtensions === "*" ? [] : assets.uploadExtensions
126
175
  });
127
- const absolute = assets.resolvePath(path);
176
+ const absolute = await assertAssetMutationPath(assets.dir, path);
128
177
  await fsp.mkdir(dirname(absolute), { recursive: true });
178
+ await assertAssetMutationPath(assets.dir, path);
129
179
  const channel = assets.uploadChannel;
130
180
  if (!channel) throw diagnostics.DP_ASSETS_0007();
131
181
  const reader = channel.openInbound();
@@ -147,7 +197,7 @@ const upload = defineAssetsRpc$7({
147
197
  /**
148
198
  * Lets the UI know upfront whether upload / rename / delete / mkdir are
149
199
  * available, so it can hide or disable those affordances proactively
150
- * instead of letting the user hit a "method not found" error — the same
200
+ * instead of letting the user hit a "method not found" error, the same
151
201
  * `canWrite`-gating idea the git plugin's `GitStatus.canWrite` follows.
152
202
  */
153
203
  const capabilities = createDefineWrapperWithContext()({
@@ -189,10 +239,12 @@ const deleteAssets = createDefineWrapperWithContext()({
189
239
  },
190
240
  setup: (ctx) => {
191
241
  const assets = getAssetsContext(ctx);
192
- return { handler: (async ({ paths }) => {
242
+ return {
243
+ /** See `list.ts` for why the async handler is cast. */
244
+ handler: (async ({ paths }) => {
193
245
  const deleted = [];
194
246
  for (const path of paths) {
195
- const absolute = assets.resolvePath(path);
247
+ const absolute = await assertAssetMutationPath(assets.dir, path);
196
248
  try {
197
249
  await fsp.unlink(absolute);
198
250
  deleted.push(path);
@@ -245,11 +297,14 @@ async function scanAssets(dir, baseURL, includeFsPath = false) {
245
297
  const files = await glob(["**/*"], {
246
298
  cwd: dir,
247
299
  onlyFiles: true,
248
- dot: false
300
+ dot: false,
301
+ followSymbolicLinks: false
249
302
  });
250
303
  return (await Promise.all(files.map(async (relPath) => {
251
304
  try {
252
- return statToAssetInfo(dir, baseURL, relPath, await fsp.lstat(join(dir, relPath)), includeFsPath);
305
+ const stat = await fsp.lstat(join(dir, relPath));
306
+ if (stat.isSymbolicLink()) return void 0;
307
+ return statToAssetInfo(dir, baseURL, relPath, stat, includeFsPath);
253
308
  } catch {
254
309
  return;
255
310
  }
@@ -288,7 +343,14 @@ const list = defineAssetsRpc$4({
288
343
  },
289
344
  setup: (ctx) => {
290
345
  const assets = getAssetsContext(ctx);
291
- return { handler: (async () => scanAssets(assets.dir, assets.baseURL, ctx.mode === "dev")) };
346
+ return {
347
+ /**
348
+ * The RPC runtime awaits handlers before validating `returns`; its
349
+ * public setup type currently models schema-backed returns as
350
+ * synchronous.
351
+ * `fsPath` is dev-only, never baked into a static build's dump.
352
+ */
353
+ handler: (async () => scanAssets(assets.dir, assets.baseURL, ctx.mode === "dev")) };
292
354
  }
293
355
  });
294
356
  const mkdir = createDefineWrapperWithContext()({
@@ -305,11 +367,14 @@ const mkdir = createDefineWrapperWithContext()({
305
367
  },
306
368
  setup: (ctx) => {
307
369
  const assets = getAssetsContext(ctx);
308
- return { handler: (async ({ path }) => {
309
- const absolute = assets.resolvePath(path);
370
+ return {
371
+ /** See `list.ts` for why the async handler is cast. */
372
+ handler: (async ({ path }) => {
373
+ const absolute = await assertAssetMutationPath(assets.dir, path);
310
374
  const stat = await fsp.stat(absolute).catch(() => void 0);
311
375
  if (stat && !stat.isDirectory()) throw diagnostics.DP_ASSETS_0005({ path });
312
376
  await fsp.mkdir(absolute, { recursive: true });
377
+ await assertAssetMutationPath(assets.dir, path);
313
378
  }) };
314
379
  }
315
380
  });
@@ -331,9 +396,11 @@ const readImageMeta = createDefineWrapperWithContext()({
331
396
  },
332
397
  setup: (ctx) => {
333
398
  const assets = getAssetsContext(ctx);
334
- return { handler: (async (path) => {
399
+ return {
400
+ /** See `list.ts` for why the async handler is cast. */
401
+ handler: (async (path) => {
335
402
  try {
336
- const buffer = await fsp.readFile(assets.resolvePath(path));
403
+ const buffer = await fsp.readFile(await resolveAssetReadPath(assets.dir, path));
337
404
  const meta = imageMeta(buffer);
338
405
  return {
339
406
  width: meta.width,
@@ -364,9 +431,11 @@ const readText = defineAssetsRpc$1({
364
431
  },
365
432
  setup: (ctx) => {
366
433
  const assets = getAssetsContext(ctx);
367
- return { handler: (async (path, limit = DEFAULT_LIMIT) => {
434
+ return {
435
+ /** See `list.ts` for why the async handler is cast. */
436
+ handler: (async (path, limit = DEFAULT_LIMIT) => {
368
437
  try {
369
- return (await fsp.readFile(assets.resolvePath(path), "utf-8")).slice(0, limit);
438
+ return (await fsp.readFile(await resolveAssetReadPath(assets.dir, path), "utf-8")).slice(0, limit);
370
439
  } catch {
371
440
  return null;
372
441
  }
@@ -390,20 +459,24 @@ const rename = createDefineWrapperWithContext()({
390
459
  },
391
460
  setup: (ctx) => {
392
461
  const assets = getAssetsContext(ctx);
393
- return { handler: (async ({ path, newName }) => {
462
+ return {
463
+ /** See `list.ts` for why the async handler is cast. */
464
+ handler: (async ({ path, newName }) => {
394
465
  const trimmed = newName.trim();
395
466
  if (!trimmed || /[/\\]/.test(trimmed)) throw diagnostics.DP_ASSETS_0006({ name: newName });
396
467
  const folder = dirname(path);
397
468
  const nextName = `${trimmed}${extname(path)}`;
398
469
  const nextRelPath = folder === "." ? nextName : `${folder}/${nextName}`;
399
- const from = assets.resolvePath(path);
400
- const to = assets.resolvePath(nextRelPath);
470
+ const from = await assertAssetMutationPath(assets.dir, path);
471
+ const to = await assertAssetMutationPath(assets.dir, nextRelPath);
401
472
  if (from === to) {
402
473
  const stat = await fsp.lstat(from);
403
474
  return statToAssetInfo(assets.dir, assets.baseURL, path, stat, true);
404
475
  }
405
476
  if (await fsp.access(to).then(() => true).catch(() => false)) throw diagnostics.DP_ASSETS_0003({ path: nextRelPath });
406
477
  await fsp.mkdir(dirname(to), { recursive: true });
478
+ await assertAssetMutationPath(assets.dir, path);
479
+ await assertAssetMutationPath(assets.dir, nextRelPath);
407
480
  try {
408
481
  await fsp.rename(from, to);
409
482
  } catch (error) {
@@ -417,14 +490,14 @@ const rename = createDefineWrapperWithContext()({
417
490
  });
418
491
  //#endregion
419
492
  //#region src/rpc/index.ts
420
- /** Read-only RPC — always registered. */
493
+ /** Read-only RPC, always registered. */
421
494
  const readFunctions = [
422
495
  list,
423
496
  readImageMeta,
424
497
  readText,
425
498
  capabilities
426
499
  ];
427
- /** Mutating RPC — registered only when write actions are enabled. */
500
+ /** Mutating RPC, registered only when write actions are enabled. */
428
501
  const writeFunctions = [
429
502
  upload,
430
503
  rename,
@@ -1,10 +1,10 @@
1
- import { n as setupAssets } from "./node-Cit_U9yi.mjs";
1
+ import { n as setupAssets } from "./node-Diynhcd3.mjs";
2
2
  import process from "node:process";
3
3
  import { defineDevframe } from "devframe";
4
4
  import { resolve } from "pathe";
5
5
  //#region package.json
6
6
  var name = "@devframes/plugin-assets";
7
- var version = "0.9.8";
7
+ var version = "0.9.9";
8
8
  var description = "Devframe plugin for browsing, previewing, uploading, and managing static assets in a directory.";
9
9
  var homepage = "https://github.com/devframes/devframe#readme";
10
10
  //#endregion
@@ -39,7 +39,7 @@ const remoteAssets = {
39
39
  };
40
40
  const DEFAULT_ID = "devframes_plugin_assets";
41
41
  /**
42
- * Create the assets manager devframe — a framework-neutral port of Nuxt
42
+ * Create the assets manager devframe, a framework-neutral port of Nuxt
43
43
  * DevTools' Assets tab. Mount it into any host via devframe's adapters, or
44
44
  * run it standalone with the bundled CLI (`devframe-assets`).
45
45
  *
@@ -74,6 +74,14 @@ function createAssetsDevframe(options = {}) {
74
74
  }
75
75
  },
76
76
  dock: { category: "~builtin" },
77
+ /**
78
+ * Both wire services are declared, not imperatively installed: devframe
79
+ * constructs them (deep-merging options across every declarer) before
80
+ * setup runs. `service-open` gets the managed dir as an allowed root so
81
+ * out-of-workspace dirs open; the client hits it directly with the
82
+ * dev-only absolute `fsPath`. `service-shiki` backs server-highlighted
83
+ * text previews, with a plain `<pre>` fallback when it isn't advertised.
84
+ */
77
85
  services: [{
78
86
  package: "@devframes/service-open",
79
87
  options: { roots: [dir] }
@@ -9,10 +9,10 @@ interface AssetInfo {
9
9
  /** URL the browser can fetch the raw bytes from (live adapters only). */
10
10
  publicPath: string;
11
11
  size: number;
12
- /** `mtimeMs` — milliseconds since epoch. */
12
+ /** `mtimeMs` in milliseconds since epoch. */
13
13
  mtime: number;
14
14
  /**
15
- * Absolute filesystem path — populated in `dev` mode only (never baked
15
+ * Absolute filesystem path, populated in `dev` mode only (never baked
16
16
  * into a static build's dump), so the client can hand it to the
17
17
  * `@devframes/service-open` wire service for open-in-editor / reveal.
18
18
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@devframes/plugin-assets",
3
3
  "type": "module",
4
- "version": "0.9.8",
4
+ "version": "0.9.9",
5
5
  "description": "Devframe plugin for browsing, previewing, uploading, and managing static assets in a directory.",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -37,9 +37,9 @@
37
37
  "dist"
38
38
  ],
39
39
  "peerDependencies": {
40
- "@devframes/plugin-assets--assets": "0.9.8",
41
- "@devframes/service-shiki": "0.9.8",
42
- "devframe": "0.9.8",
40
+ "@devframes/plugin-assets--assets": "0.9.9",
41
+ "@devframes/service-shiki": "0.9.9",
42
+ "devframe": "0.9.9",
43
43
  "vite": "^7.0.0 || ^8.0.0"
44
44
  },
45
45
  "peerDependenciesMeta": {
@@ -54,7 +54,7 @@
54
54
  }
55
55
  },
56
56
  "dependencies": {
57
- "@devframes/service-open": "0.9.8",
57
+ "@devframes/service-open": "0.9.9",
58
58
  "cac": "^7.0.0",
59
59
  "chokidar": "^5.0.0",
60
60
  "image-meta": "^0.2.2",
@@ -65,9 +65,9 @@
65
65
  },
66
66
  "devDependencies": {
67
67
  "@antfu/design": "^0.4.0",
68
- "@devframes/plugin-assets--assets": "0.9.8",
69
- "@devframes/service-shiki": "0.9.8",
70
- "@devframes/vite": "0.9.8",
68
+ "@devframes/plugin-assets--assets": "0.9.9",
69
+ "@devframes/service-shiki": "0.9.9",
70
+ "@devframes/vite": "0.9.9",
71
71
  "@iconify-json/ph": "^1.2.2",
72
72
  "@storybook/addon-a11y": "^10.5.10",
73
73
  "@storybook/addon-docs": "^10.5.10",
@@ -75,7 +75,7 @@
75
75
  "@types/node": "^26.4.0",
76
76
  "@vitejs/plugin-vue": "^6.0.8",
77
77
  "colorjs.io": "^0.7.1",
78
- "devframe": "0.9.8",
78
+ "devframe": "0.9.9",
79
79
  "floating-vue": "^5.2.2",
80
80
  "h3": "^2.0.1-rc.29",
81
81
  "reka-ui": "^2.10.4",