i-repo 2.5.0 → 2.7.0

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.
@@ -106,9 +106,17 @@ pluginCommand
106
106
  if (Array.isArray(row.platforms)) {
107
107
  row.platforms = row.platforms.join(", ");
108
108
  }
109
+ if (Array.isArray(row.input)) {
110
+ row.input = row.input.join(", ");
111
+ }
109
112
  if (Array.isArray(row.subcommands)) {
110
113
  row.subcommands = row.subcommands
111
- .map((s) => (s.description ? `${String(s.name)} (${String(s.description)})` : String(s.name)))
114
+ .map((s) => {
115
+ const intake = Array.isArray(s.input) ? ` [${s.input.join("/")}]` : "";
116
+ return s.description
117
+ ? `${String(s.name)}${intake} (${String(s.description)})`
118
+ : `${String(s.name)}${intake}`;
119
+ })
112
120
  .join(", ");
113
121
  }
114
122
  }
@@ -1,5 +1,5 @@
1
- import { assertNumericId, handleError } from "../../core/errors.js";
2
- import { saveBinaryDownload, inferFilename } from "../../utils/file.js";
1
+ import { assertNumericId, handleError, ValidationError } from "../../core/errors.js";
2
+ import { saveBinaryDownload, inferFilename, sanitizeServerFilename } from "../../utils/file.js";
3
3
  import { withSpinner } from "../../ui/spinner.js";
4
4
  import { choiceOption, parsePageSpec } from "../shared-options.js";
5
5
  export function registerReportsDownload(parent, getClient) {
@@ -8,12 +8,19 @@ export function registerReportsDownload(parent, getClient) {
8
8
  .description("Download report file (PDF/Excel)")
9
9
  .argument("<reportId>", "Report ID (numeric)")
10
10
  .addOption(choiceOption("--file-type <type>", "File type", ["pdf", "pdfLayer", "excel"]).makeOptionMandatory())
11
- .option("-o, --output <path>", "Output file path")
11
+ .option("-o, --output <path>", "Output file path (default: server-provided filename, else report-<id>)")
12
12
  .option("--page <pages>", 'Pages (pdf/pdfLayer only; e.g. "1,3" or "2-4")', parsePageSpec)
13
- .option("--file-name <name>", "Output file name")
13
+ .option("--file-name <name>", "Output file name (also sent to the server's fileName parameter)")
14
14
  .action(async (reportId, options, cmd) => {
15
15
  try {
16
16
  assertNumericId(reportId, "reportId");
17
+ // --file-name は「ファイル名」専用 — パスは -o/--output の仕事。
18
+ // 空白のみ (→ 隠しファイル ".pdf") やパス区切り入りは黙って変形せず
19
+ // ここで明示的に拒否する (API 呼び出し前に早期失敗)。
20
+ const requestedName = options.fileName?.trim();
21
+ if (requestedName !== undefined && (requestedName === "" || /[/\\]/.test(requestedName))) {
22
+ throw new ValidationError(`Invalid --file-name "${options.fileName}": must be a plain file name without path separators (use --output for paths).`);
23
+ }
17
24
  const globalOpts = cmd.optsWithGlobals();
18
25
  const client = await getClient(cmd);
19
26
  const result = await withSpinner("downloading", async () => {
@@ -21,11 +28,29 @@ export function registerReportsDownload(parent, getClient) {
21
28
  fileType: options.fileType,
22
29
  report: reportId,
23
30
  pageNo: options.page,
24
- fileName: options.fileName,
31
+ fileName: requestedName,
25
32
  });
26
33
  });
27
34
  const ext = options.fileType === "excel" ? "xlsx" : "pdf";
28
- const defaultName = `report-${reportId}.${ext}`;
35
+ // 保存名の優先順位: -o/--output (パスごと明示) > --file-name (明示) >
36
+ // サーバ申告名 (Content-Disposition = ファイル自動出力設定の名称) >
37
+ // 従来の report-<ID>.<ext>。ユーザーの明示指定は、サーバが fileName
38
+ // パラメータを無視/正規化して別名を申告してきても負けない。
39
+ // --file-name / サーバ名とも、拡張子付きならそのまま・無ければ .<ext>
40
+ // を補う ("foo.pdf" を "foo.pdf.pdf" にしない。--file-type で拡張子は
41
+ // 確定しているので、octet-stream 応答でも拡張子なし保存にしない)。
42
+ // サーバ名は無害化してから使う。
43
+ // 末尾のドット/空白は Windows のファイルシステムが黙って剥がす — 先に
44
+ // 剥がしてから拡張子判定する ("foo." / "foo.pdf " を誤判定して、要求と
45
+ // 違う名前でディスクに載るのを防ぐ。"foo." → "foo.pdf")
46
+ const withExt = (name) => {
47
+ const base = name.replace(/[. ]+$/, "");
48
+ return base.lastIndexOf(".") > 0 ? base : `${base}.${ext}`;
49
+ };
50
+ const serverBase = sanitizeServerFilename(result.fileName);
51
+ const serverName = serverBase ? withExt(serverBase) : undefined;
52
+ const explicitName = requestedName ? withExt(requestedName) : undefined;
53
+ const defaultName = explicitName ?? serverName ?? `report-${reportId}.${ext}`;
29
54
  const filename = inferFilename(result.contentType, defaultName);
30
55
  await saveBinaryDownload(result, options.output, filename, globalOpts.quiet);
31
56
  }
@@ -21,6 +21,15 @@ export declare const KNOWN_PLATFORMS: readonly ["macos", "linux", "windows"];
21
21
  export type PluginPlatform = (typeof KNOWN_PLATFORMS)[number];
22
22
  /** process.platform → platforms 語彙。未知の OS は null (= ゲート対象外) */
23
23
  export declare function platformKeywordFor(p: NodeJS.Platform): PluginPlatform | null;
24
+ /**
25
+ * input (intake = データの受け取り方) の閉じた語彙 (要望書v2)。
26
+ * 「どう渡すか」を実行前に機械可読にする — sink にディレクトリを渡す等の
27
+ * 取り違えを連携側 (GUI/Connector) が起動前に検出できるようにする。
28
+ * CLI 本体はこれでゲートしない (宣言した意図を伝えるヒント。成否の判定は
29
+ * あくまで receipt の verified)。
30
+ */
31
+ export declare const KNOWN_INPUTS: readonly ["stdin-ndjson", "files", "dir", "none"];
32
+ export type PluginInput = (typeof KNOWN_INPUTS)[number];
24
33
  /** params の1エントリ。GUI/ink がフォーム部品へ写像できる最小集合に絞る */
25
34
  export interface PluginParam {
26
35
  /** フラグ名 (例: "--bucket") */
@@ -44,6 +53,8 @@ export interface PluginParam {
44
53
  export interface PluginSubcommand {
45
54
  name: string;
46
55
  description?: string;
56
+ /** このサブコマンドの intake。プラグインレベルの input を上書きする (要望書v2 §3.2) */
57
+ input?: PluginInput[];
47
58
  }
48
59
  /** --plugin-schema 応答の全体形 (基本 + 拡張。未知フィールドは無視) */
49
60
  export interface PluginDescribe {
@@ -59,6 +70,11 @@ export interface PluginDescribe {
59
70
  /** 対応 OS。未宣言 = 制限なし。宣言があり現在の OS が含まれない場合、dispatch は実行を拒否する */
60
71
  platforms?: PluginPlatform[];
61
72
  subcommands?: PluginSubcommand[];
73
+ /**
74
+ * intake = データの受け取り方 (要望書v2)。未宣言 = 不明 (連携側は安全側 =
75
+ * 渡し方を推測しない)。subcommands[].input がある場合はそちらが優先。
76
+ */
77
+ input?: PluginInput[];
62
78
  }
63
79
  /**
64
80
  * plugin describe の表示行を組む。plugin/path は CLI が解決した事実 —
@@ -31,6 +31,14 @@ export function platformKeywordFor(p) {
31
31
  return "linux";
32
32
  return null;
33
33
  }
34
+ /**
35
+ * input (intake = データの受け取り方) の閉じた語彙 (要望書v2)。
36
+ * 「どう渡すか」を実行前に機械可読にする — sink にディレクトリを渡す等の
37
+ * 取り違えを連携側 (GUI/Connector) が起動前に検出できるようにする。
38
+ * CLI 本体はこれでゲートしない (宣言した意図を伝えるヒント。成否の判定は
39
+ * あくまで receipt の verified)。
40
+ */
41
+ export const KNOWN_INPUTS = ["stdin-ndjson", "files", "dir", "none"];
34
42
  const PARAM_TYPES = new Set(["string", "number", "boolean", "select"]);
35
43
  /**
36
44
  * plugin describe の表示行を組む。plugin/path は CLI が解決した事実 —
@@ -112,7 +120,23 @@ export async function probePlatforms(bin, timeoutMs = 1500) {
112
120
  }
113
121
  /** 拡張フィールドのいずれかを宣言しているか */
114
122
  export function hasDescribeFields(row) {
115
- return ["name", "version", "description", "phases", "params", "platforms", "subcommands"].some((k) => k in row);
123
+ return ["name", "version", "description", "phases", "params", "platforms", "subcommands", "input"].some((k) => k in row);
124
+ }
125
+ /**
126
+ * input (intake) 宣言の検査。閉じた語彙の非空配列であること。
127
+ * "none" は「データ入力なし」なので他の値との併記は矛盾 — 受理すると
128
+ * 連携側が「渡さなくてよいのか stdin で渡すのか」を判断できない。
129
+ */
130
+ function validateInputList(value, field) {
131
+ if (!Array.isArray(value) ||
132
+ value.length === 0 ||
133
+ !value.every((v) => typeof v === "string" && KNOWN_INPUTS.includes(v))) {
134
+ return `${field} must be a non-empty array of ${KNOWN_INPUTS.map((k) => `"${k}"`).join(" | ")}`;
135
+ }
136
+ if (value.includes("none") && value.length > 1) {
137
+ return `${field} must not combine "none" with other intake values`;
138
+ }
139
+ return null;
116
140
  }
117
141
  /**
118
142
  * 宣言の形式検査。問題の説明文を返す (空配列 = 適合)。
@@ -142,6 +166,11 @@ export function validateDescribe(row) {
142
166
  problems.push(`platforms must be a non-empty array of ${KNOWN_PLATFORMS.map((k) => `"${k}"`).join(" | ")}`);
143
167
  }
144
168
  }
169
+ if ("input" in row) {
170
+ const problem = validateInputList(row.input, "input");
171
+ if (problem)
172
+ problems.push(problem);
173
+ }
145
174
  let declaredSubs = null;
146
175
  if ("subcommands" in row) {
147
176
  const subs = row.subcommands;
@@ -168,6 +197,11 @@ export function validateDescribe(row) {
168
197
  if ("description" in s && typeof s.description !== "string") {
169
198
  problems.push(`subcommands[${i}].description must be a string`);
170
199
  }
200
+ if ("input" in s) {
201
+ const problem = validateInputList(s.input, `subcommands[${i}].input`);
202
+ if (problem)
203
+ problems.push(problem);
204
+ }
171
205
  });
172
206
  }
173
207
  }
@@ -25,7 +25,7 @@ case "\${1:-}" in
25
25
  exit 0 ;;
26
26
  --plugin-schema)
27
27
  # 自己記述 (PLUGINS.md §3)。params は GUI/ink のフォーム生成に使われる
28
- printf '{"pluginApi":["1"],"schemaVersions":["1.0"],"recordTypes":["*"],"name":"i-repo-${name}","version":"0.1.0","description":"TODO: what this plugin does","phases":["write","verify"],"platforms":["macos","linux"],"params":[{"name":"--dry-run","type":"boolean","label":"Dry run","description":"Validate without writing"}]}\\n'
28
+ printf '{"pluginApi":["1"],"schemaVersions":["1.0"],"recordTypes":["*"],"name":"i-repo-${name}","version":"0.1.0","description":"TODO: what this plugin does","phases":["write","verify"],"platforms":["macos","linux"],"input":["stdin-ndjson"],"params":[{"name":"--dry-run","type":"boolean","label":"Dry run","description":"Validate without writing"}]}\\n'
29
29
  exit 0 ;;
30
30
  esac
31
31
 
@@ -115,6 +115,7 @@ if (arg0 === "--plugin-schema") {
115
115
  description: "TODO: what this plugin does",
116
116
  phases: ["write", "verify"],
117
117
  platforms: ["macos", "linux", "windows"],
118
+ input: ["stdin-ndjson"],
118
119
  params: [
119
120
  { name: "--dry-run", type: "boolean", label: "Dry run", description: "Validate without writing" },
120
121
  ],
@@ -53,11 +53,13 @@ export declare class ReportApi extends BaseApi {
53
53
  copyReport(params: CopyReportParams): Promise<CopyReportResponse>;
54
54
  /**
55
55
  * 帳票ファイル (PDF/Excel) をバイナリとして取得する
56
- * @returns バイナリデータとContentType
56
+ * @returns バイナリデータとContentType。fileName はサーバが Content-Disposition で
57
+ * 申告した出力名(ファイル自動出力設定の名称。無ければ undefined)
57
58
  */
58
59
  getReportFile(params: GetReportFileParams): Promise<{
59
60
  data: ArrayBuffer;
60
61
  contentType: string;
62
+ fileName?: string;
61
63
  }>;
62
64
  /**
63
65
  * クラスター値を取得する。
@@ -132,7 +132,8 @@ export class ReportApi extends BaseApi {
132
132
  }
133
133
  /**
134
134
  * 帳票ファイル (PDF/Excel) をバイナリとして取得する
135
- * @returns バイナリデータとContentType
135
+ * @returns バイナリデータとContentType。fileName はサーバが Content-Disposition で
136
+ * 申告した出力名(ファイル自動出力設定の名称。無ければ undefined)
136
137
  */
137
138
  async getReportFile(params) {
138
139
  const reqParams = buildParams({ command: "GetReportFile", fileType: params.fileType }, {
@@ -18,6 +18,7 @@ export declare abstract class BaseApi {
18
18
  protected executeForBinary(params: Record<string, string>): Promise<{
19
19
  data: ArrayBuffer;
20
20
  contentType: string;
21
+ fileName?: string;
21
22
  }>;
22
23
  /**
23
24
  * クラスタ値取得用。XMLレスポンスで `conmas.value` があればテキスト値を
@@ -58,7 +58,7 @@ export class BaseApi {
58
58
  }
59
59
  // Content-Type が xml でなくても、本文が XML エラーなら沈黙保存させない
60
60
  this.checkBinaryErrorEnvelope(result.data, params.command);
61
- return { data: result.data, contentType: result.contentType };
61
+ return { data: result.data, contentType: result.contentType, fileName: result.fileName };
62
62
  }
63
63
  /**
64
64
  * クラスタ値取得用。XMLレスポンスで `conmas.value` があればテキスト値を
@@ -1,4 +1,18 @@
1
1
  import type { IReporterConfig } from "../types/common.js";
2
+ /** バイナリ応答。fileName は Content-Disposition から得たサーバ提供名 (無ければ undefined) */
3
+ export interface BinaryResponse {
4
+ data: ArrayBuffer;
5
+ contentType: string;
6
+ fileName?: string;
7
+ }
8
+ /**
9
+ * Content-Disposition からファイル名を取り出す (RFC 6266)。
10
+ * `filename*=charset''pct-encoded` (RFC 5987) を優先し、無ければ
11
+ * `filename="..."` / `filename=token`。パースできない・壊れた値は undefined —
12
+ * 呼び出し側がフォールバック名を使う。値はサーバ申告のまま返す (パス区切り等の
13
+ * 無害化は保存する側の責務 — SDK はワイヤ忠実)。
14
+ */
15
+ export declare function parseContentDispositionFilename(header: string | null | undefined): string | undefined;
2
16
  /** HTTPクライアント: セッション(Cookie)管理 + POSTリクエスト */
3
17
  export declare class HttpClient {
4
18
  private readonly baseUrl;
@@ -41,19 +55,14 @@ export declare class HttpClient {
41
55
  filename: string;
42
56
  }): Promise<string>;
43
57
  /** バイナリレスポンスでPOSTリクエスト(ダウンロード用)。画像等を破損させないため arraybuffer で受信 */
44
- postForBinary(params: Record<string, string>): Promise<{
45
- data: ArrayBuffer;
46
- contentType: string;
47
- }>;
58
+ postForBinary(params: Record<string, string>): Promise<BinaryResponse>;
48
59
  /** レスポンスがXMLかどうかを判定してXML/バイナリを返す。画像等は arraybuffer で取得するため専用経路を使用 */
49
60
  postAutoDetect(params: Record<string, string>): Promise<{
50
61
  type: "xml";
51
62
  xml: string;
52
- } | {
63
+ } | ({
53
64
  type: "binary";
54
- data: ArrayBuffer;
55
- contentType: string;
56
- }>;
65
+ } & BinaryResponse)>;
57
66
  /** multipart/form-data でPOSTし、Content-Typeを判定してXML/バイナリを返す */
58
67
  postMultipartForBinary(params: Record<string, string>, file: {
59
68
  name: string;
@@ -2,6 +2,39 @@ import http from "node:http";
2
2
  import https from "node:https";
3
3
  import { URL } from "node:url";
4
4
  import { HttpError, NetworkError } from "./errors.js";
5
+ /**
6
+ * Content-Disposition からファイル名を取り出す (RFC 6266)。
7
+ * `filename*=charset''pct-encoded` (RFC 5987) を優先し、無ければ
8
+ * `filename="..."` / `filename=token`。パースできない・壊れた値は undefined —
9
+ * 呼び出し側がフォールバック名を使う。値はサーバ申告のまま返す (パス区切り等の
10
+ * 無害化は保存する側の責務 — SDK はワイヤ忠実)。
11
+ */
12
+ export function parseContentDispositionFilename(header) {
13
+ if (!header)
14
+ return undefined;
15
+ // パラメータ名と charset は大文字小文字を区別しない (RFC 6266/5987 — copilot 指摘)
16
+ // filename*=UTF-8''%E5%B8%B3%E7%A5%A8.pdf (charset は UTF-8 のみ対応)
17
+ const ext = /filename\*\s*=\s*utf-8''([^;]+)/i.exec(header);
18
+ if (ext) {
19
+ try {
20
+ const decoded = decodeURIComponent(ext[1].trim());
21
+ if (decoded.length > 0)
22
+ return decoded;
23
+ }
24
+ catch {
25
+ // 壊れた % エンコード → 素の filename= にフォールバック
26
+ }
27
+ }
28
+ const quoted = /filename\s*=\s*"((?:[^"\\]|\\.)*)"/i.exec(header);
29
+ if (quoted) {
30
+ const unescaped = quoted[1].replace(/\\(.)/g, "$1");
31
+ return unescaped.length > 0 ? unescaped : undefined;
32
+ }
33
+ const token = /filename\s*=\s*([^;\s]+)/i.exec(header);
34
+ if (token && token[1].length > 0)
35
+ return token[1];
36
+ return undefined;
37
+ }
5
38
  /** HTTPクライアント: セッション(Cookie)管理 + POSTリクエスト */
6
39
  export class HttpClient {
7
40
  baseUrl;
@@ -142,13 +175,13 @@ export class HttpClient {
142
175
  }
143
176
  /** レスポンスがXMLかどうかを判定してXML/バイナリを返す。画像等は arraybuffer で取得するため専用経路を使用 */
144
177
  async postAutoDetect(params) {
145
- const { data, contentType } = await this.postForBinary(params);
178
+ const { data, contentType, fileName } = await this.postForBinary(params);
146
179
  const rawContentType = contentType ?? "";
147
180
  if (rawContentType.includes("xml") && !isZipMagic(data)) {
148
181
  const xml = new TextDecoder("utf-8").decode(data);
149
182
  return { type: "xml", xml };
150
183
  }
151
- return { type: "binary", data, contentType: rawContentType };
184
+ return { type: "binary", data, contentType: rawContentType, fileName };
152
185
  }
153
186
  /** multipart/form-data でPOSTし、Content-Typeを判定してXML/バイナリを返す */
154
187
  async postMultipartForBinary(params, file) {
@@ -206,6 +239,7 @@ export class HttpClient {
206
239
  return {
207
240
  data: await this.readArrayBuffer(response),
208
241
  contentType: response.headers.get("content-type") ?? "application/octet-stream",
242
+ fileName: parseContentDispositionFilename(response.headers.get("content-disposition")),
209
243
  };
210
244
  }
211
245
  /** Node `http`/`https` で URL-encoded を送る(ASP.NET と相性が良い) */
@@ -228,6 +262,7 @@ export class HttpClient {
228
262
  return {
229
263
  data: u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength),
230
264
  contentType,
265
+ fileName: parseContentDispositionFilename(getIncomingHeader(headers, "content-disposition")),
231
266
  };
232
267
  }
233
268
  async nodePostUrlEncoded(startUrl, bodyString) {
@@ -24,6 +24,7 @@ export declare class ReportsResource {
24
24
  downloadFile(params: GetReportFileParams): Promise<{
25
25
  data: ArrayBuffer;
26
26
  contentType: string;
27
+ fileName?: string;
27
28
  }>;
28
29
  getClusterValue(params: GetClusterValueParams): Promise<{
29
30
  data: ArrayBuffer;
@@ -27,6 +27,15 @@ export declare function saveBinaryDownload(result: {
27
27
  data: ArrayBuffer;
28
28
  contentType: string;
29
29
  }, outputPath: string | undefined, defaultFilename: string, quiet?: boolean): Promise<DownloadResult>;
30
+ /**
31
+ * サーバ申告のダウンロードファイル名 (Content-Disposition) を保存に安全な
32
+ * 単一ファイル名へ無害化する。サーバ (または途中の何か) が "../../evil.pdf" や
33
+ * 制御文字入りの名前を申告しても、保存先ディレクトリの外へ書かせない。
34
+ * 日本語等の Unicode は保持する (帳票名がそのまま使えることが目的)。
35
+ * 無害化の結果ファイル名として成立しない場合は undefined — 呼び出し側が
36
+ * フォールバック名を使う。
37
+ */
38
+ export declare function sanitizeServerFilename(name: string | undefined): string | undefined;
30
39
  /**
31
40
  * Infer a filename from content-type header.
32
41
  * Falls back to the provided default.
@@ -109,6 +109,34 @@ export async function saveBinaryDownload(result, outputPath, defaultFilename, qu
109
109
  size: buffer.byteLength,
110
110
  };
111
111
  }
112
+ /**
113
+ * サーバ申告のダウンロードファイル名 (Content-Disposition) を保存に安全な
114
+ * 単一ファイル名へ無害化する。サーバ (または途中の何か) が "../../evil.pdf" や
115
+ * 制御文字入りの名前を申告しても、保存先ディレクトリの外へ書かせない。
116
+ * 日本語等の Unicode は保持する (帳票名がそのまま使えることが目的)。
117
+ * 無害化の結果ファイル名として成立しない場合は undefined — 呼び出し側が
118
+ * フォールバック名を使う。
119
+ */
120
+ export function sanitizeServerFilename(name) {
121
+ if (!name)
122
+ return undefined;
123
+ // パス区切りを越えさせない: 最後のセグメントだけを採用 (\ は Windows 区切り)
124
+ const lastSegment = name.split(/[/\\]/).pop() ?? "";
125
+ const cleaned = lastSegment
126
+ .replace(/[\u0000-\u001f\u007f]/g, "") // 制御文字 (端末・FS 両方に有害)
127
+ .replace(/[<>:"|?*]/g, "_") // Windows で使えない文字
128
+ .trim()
129
+ // Windows は末尾のドット/空白を黙って剥がす — 先にこちらで剥がして
130
+ // 「指定と違う名前で保存される」を起こさない (copilot 指摘)
131
+ .replace(/[. ]+$/, "");
132
+ if (cleaned === "")
133
+ return undefined;
134
+ // Windows 予約デバイス名 (NUL / CON.pdf 等) はどの OS でも採用しない —
135
+ // 保存物が後で Windows に渡ったとき開けない名前を作らない (可搬性)
136
+ if (isWindowsReservedName(cleaned))
137
+ return undefined;
138
+ return cleaned;
139
+ }
112
140
  /**
113
141
  * Infer a filename from content-type header.
114
142
  * Falls back to the provided default.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i-repo",
3
- "version": "2.5.0",
3
+ "version": "2.7.0",
4
4
  "description": "Modern CLI for ConMas i-Reporter - Built for humans and AI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -61,7 +61,7 @@
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/gradient-string": "1.1.6",
64
- "@types/node": "24.13.0",
64
+ "@types/node": "25.9.1",
65
65
  "@types/react": "19.2.17",
66
66
  "typescript": "5.9.3",
67
67
  "vitest": "4.1.8"