@droposs/plugin-cli 0.5.9 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@ import {
6
6
  testPlugin,
7
7
  initPlugin,
8
8
  validateManifest,
9
+ verifyPlugin,
9
10
  } from "../dist/index.js";
10
11
  import { readFile } from "node:fs/promises";
11
12
  import path from "node:path";
@@ -13,6 +14,23 @@ import path from "node:path";
13
14
  const command = process.argv[2];
14
15
  const args = process.argv.slice(3);
15
16
 
17
+ /** Reads `--flag value`; exits with an error when the value is missing. */
18
+ function readFlagValue(argv, flag) {
19
+ const index = argv.indexOf(flag);
20
+ if (index === -1) return undefined;
21
+ const value = argv[index + 1];
22
+ if (!value || value.startsWith("-")) {
23
+ console.error(`${flag} requires a value`);
24
+ process.exit(1);
25
+ }
26
+ return value;
27
+ }
28
+
29
+ /** First positional argument, ignoring flags and their values. */
30
+ function positionalArg(argv, flagValues = []) {
31
+ return argv.find((arg) => !arg.startsWith("-") && !flagValues.includes(arg));
32
+ }
33
+
16
34
  async function runValidate(dir) {
17
35
  const manifestPath = path.join(
18
36
  path.resolve(process.cwd(), dir),
@@ -36,20 +54,42 @@ function printUsage() {
36
54
  Usage:
37
55
  drop-plugin init [dir] Initialize a new plugin from starter template
38
56
  drop-plugin build [dir] Bundle server/client entry points with esbuild and sign
57
+ (--out-manifest <path> keeps the source manifest clean)
39
58
  drop-plugin sign [dir] Calculate SHA-256 digests and sign drop-plugin.json
59
+ (--out-manifest <path> writes the derived manifest elsewhere)
60
+ drop-plugin verify [dir] Verify checksums and signature of a bundle
61
+ (--allow-unsigned accepts bundles without a signature)
40
62
  drop-plugin validate [dir] Validate drop-plugin.json against official schema
41
63
  drop-plugin test [dir] Run plugin tests with Node test runner
42
64
  drop-plugin pack [dir] [out] Verify, sign, and package bundle into .dropplugin archive
43
65
  `);
44
66
  }
45
67
 
68
+ async function runVerify(argv) {
69
+ const allowUnsigned = argv.includes("--allow-unsigned");
70
+ const dir = positionalArg(argv) || ".";
71
+ const res = await verifyPlugin(dir, undefined, { allowUnsigned });
72
+ if (!res.valid) {
73
+ console.error("Verification failed:");
74
+ for (const err of res.errors) {
75
+ console.error(` - ${err}`);
76
+ }
77
+ process.exit(1);
78
+ }
79
+ console.log(
80
+ `Bundle at ${dir} is valid (signature: ${res.signed ? "verified" : "none"}).`,
81
+ );
82
+ }
83
+
46
84
  async function main() {
47
85
  switch (command) {
48
86
  case "sign": {
49
- const dir = args[0] || ".";
50
- const res = await signPlugin(dir);
87
+ const outManifest = readFlagValue(args, "--out-manifest");
88
+ const dir = positionalArg(args, [outManifest]) || ".";
89
+ const res = await signPlugin(dir, undefined, true, { outManifest });
51
90
  console.log(
52
- `Signed bundle at ${dir}: ${res.fileCount} files verified (signature: ${res.signed ? "yes" : "no"})`,
91
+ `Signed bundle at ${dir}: ${res.fileCount} files verified (signature: ${res.signed ? "yes" : "no"})` +
92
+ (outManifest ? `; manifest written to ${outManifest}` : ""),
53
93
  );
54
94
  break;
55
95
  }
@@ -63,8 +103,9 @@ async function main() {
63
103
  break;
64
104
  }
65
105
  case "build": {
66
- const dir = args[0] || ".";
67
- const res = await buildPlugin(dir);
106
+ const outManifest = readFlagValue(args, "--out-manifest");
107
+ const dir = positionalArg(args, [outManifest]) || ".";
108
+ const res = await buildPlugin(dir, { outManifest });
68
109
  console.log(
69
110
  `Built plugin at ${dir} (server: ${res.serverBuilt ? "yes" : "no"}, client: ${res.clientBuilt ? "yes" : "no"})`,
70
111
  );
@@ -87,6 +128,10 @@ async function main() {
87
128
  await runValidate(args[0] || ".");
88
129
  break;
89
130
  }
131
+ case "verify": {
132
+ await runVerify(args);
133
+ break;
134
+ }
90
135
  case "help":
91
136
  case "--help":
92
137
  case "-h":
package/dist/builder.d.ts CHANGED
@@ -3,6 +3,8 @@ export interface BuildOptions {
3
3
  sourcemap?: boolean;
4
4
  sign?: boolean;
5
5
  signingKey?: string;
6
+ /** Write derived manifest fields here instead of the source manifest. */
7
+ outManifest?: string;
6
8
  }
7
9
  export declare function buildPlugin(targetDir?: string, options?: BuildOptions): Promise<{
8
10
  serverBuilt: boolean;
package/dist/builder.js CHANGED
@@ -34,7 +34,13 @@ export async function buildPlugin(targetDir = ".", options = {}) {
34
34
  format: "esm",
35
35
  sourcemap: options.sourcemap ?? true,
36
36
  minify: options.minify ?? false,
37
- external: ["@droposs/plugin-sdk", "@drop/plugin-sdk", "h3", "pino"],
37
+ external: [
38
+ "@droposs/plugin-sdk",
39
+ "@droposs/plugin-sdk",
40
+ "@drop/plugin-sdk",
41
+ "h3",
42
+ "pino",
43
+ ],
38
44
  });
39
45
  serverBuilt = true;
40
46
  }
@@ -63,13 +69,20 @@ export async function buildPlugin(targetDir = ".", options = {}) {
63
69
  format: "esm",
64
70
  sourcemap: options.sourcemap ?? true,
65
71
  minify: options.minify ?? false,
66
- external: ["vue", "@droposs/plugin-sdk", "@drop/plugin-sdk"],
72
+ external: [
73
+ "vue",
74
+ "@droposs/plugin-sdk",
75
+ "@droposs/plugin-sdk",
76
+ "@drop/plugin-sdk",
77
+ ],
67
78
  });
68
79
  clientBuilt = true;
69
80
  }
70
81
  // 3. Automatically re-sign the plugin bundle after building
71
82
  if (options.sign !== false) {
72
- await signPlugin(dir, options.signingKey);
83
+ await signPlugin(dir, options.signingKey, true, {
84
+ outManifest: options.outManifest,
85
+ });
73
86
  }
74
87
  return { serverBuilt, clientBuilt };
75
88
  }
package/dist/signer.d.ts CHANGED
@@ -1,13 +1,44 @@
1
+ import { SIGNATURE_VERSION } from "@drop-oss/plugin-sdk";
2
+ /** Current signature scheme; re-exported for backwards compatibility. */
3
+ export { SIGNATURE_VERSION };
4
+ /**
5
+ * Signature payload v2: the files aggregate plus the canonical manifest
6
+ * (excluding its `signature` field), so `id`, `version`, and `capabilities`
7
+ * are covered and cannot be tampered with undetected. Shared algorithm with
8
+ * `drop` core's verifier.
9
+ */
10
+ export declare function signaturePayloadV2(filesAggregate: string, manifest: Record<string, unknown>): string;
1
11
  export declare function isInside(base: string, candidate: string): boolean;
2
12
  export declare function validateManifest(manifest: unknown): Promise<{
3
13
  valid: boolean;
4
14
  errors: string[];
5
15
  }>;
6
16
  export declare function listFiles(root: string, prefix?: string): Promise<string[]>;
7
- export declare function signPlugin(targetDir: string, signingKey?: string, validate?: boolean): Promise<{
17
+ export interface SignPluginOptions {
18
+ /**
19
+ * Write the derived manifest here instead of `<bundle>/drop-plugin.json`.
20
+ * The source manifest is left untouched, so derived fields (checksum, files,
21
+ * signature) can live only in a packaged artifact.
22
+ */
23
+ outManifest?: string;
24
+ }
25
+ export declare function signPlugin(targetDir: string, signingKey?: string, validate?: boolean, options?: SignPluginOptions): Promise<{
8
26
  fileCount: number;
9
27
  signed: boolean;
10
28
  }>;
29
+ export interface VerifyResult {
30
+ valid: boolean;
31
+ signed: boolean;
32
+ errors: string[];
33
+ }
34
+ /**
35
+ * Verify a bundle against its shipped manifest: schema validity, per-file and
36
+ * entry SHA-256 checksums, and (when present) the HMAC signature covering the
37
+ * file aggregate plus the manifest itself.
38
+ */
39
+ export declare function verifyPlugin(targetDir: string, signingKey?: string, options?: {
40
+ allowUnsigned?: boolean;
41
+ }): Promise<VerifyResult>;
11
42
  export declare function packPlugin(targetDir: string, outputDir?: string): Promise<{
12
43
  packagePath: string;
13
44
  id: string;
package/dist/signer.js CHANGED
@@ -1,23 +1,90 @@
1
- import { createHash, createHmac } from "node:crypto";
1
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
2
2
  import { readdir, readFile, realpath, stat, writeFile, mkdir, } from "node:fs/promises";
3
3
  import { createRequire } from "node:module";
4
4
  import path from "node:path";
5
5
  import Ajv from "ajv";
6
+ import { SIGNATURE_VERSION } from "@droposs/plugin-sdk";
6
7
  const MANIFEST_FILE = "drop-plugin.json";
8
+ /** Current signature scheme; re-exported for backwards compatibility. */
9
+ export { SIGNATURE_VERSION };
10
+ /** Deterministic JSON so signer and verifier hash identical manifest bytes. */
11
+ function stableStringify(value) {
12
+ if (value === null || typeof value !== "object") {
13
+ return JSON.stringify(value) ?? "null";
14
+ }
15
+ if (Array.isArray(value)) {
16
+ return `[${value.map(stableStringify).join(",")}]`;
17
+ }
18
+ const record = value;
19
+ const keys = Object.keys(record).sort((a, b) => a.localeCompare(b, "en"));
20
+ return `{${keys
21
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
22
+ .join(",")}}`;
23
+ }
24
+ function sha256Hex(bytes) {
25
+ return createHash("sha256").update(bytes).digest("hex");
26
+ }
27
+ /** Whether a bundle-relative path is executable plugin code. */
28
+ function isBundleCodeFile(rel) {
29
+ const ext = path.extname(rel).toLowerCase();
30
+ return ext === ".js" || ext === ".mjs" || ext === ".cjs";
31
+ }
32
+ /**
33
+ * Aggregate digest over every bundle file, byte-compatible with the digest
34
+ * `drop` core computes when verifying legacy signatures.
35
+ */
36
+ async function computeFilesAggregate(bundleDir, files) {
37
+ const aggregate = createHash("sha256");
38
+ for (const rel of files) {
39
+ const bytes = await readFile(path.join(bundleDir, rel));
40
+ aggregate.update(rel);
41
+ aggregate.update("\0");
42
+ aggregate.update(String(bytes.length));
43
+ aggregate.update("\0");
44
+ aggregate.update(bytes);
45
+ }
46
+ return aggregate.digest("hex");
47
+ }
48
+ /**
49
+ * Signature payload v2: the files aggregate plus the canonical manifest
50
+ * (excluding its `signature` field), so `id`, `version`, and `capabilities`
51
+ * are covered and cannot be tampered with undetected. Shared algorithm with
52
+ * `drop` core's verifier.
53
+ */
54
+ export function signaturePayloadV2(filesAggregate, manifest) {
55
+ const { signature: _ignored, ...signable } = manifest;
56
+ return createHash("sha256")
57
+ .update(filesAggregate)
58
+ .update("\0")
59
+ .update(stableStringify(signable))
60
+ .digest("hex");
61
+ }
62
+ function safeEqualHex(a, b) {
63
+ const left = Buffer.from(a, "hex");
64
+ const right = Buffer.from(b, "hex");
65
+ if (left.length !== right.length || left.length === 0)
66
+ return false;
67
+ return timingSafeEqual(left, right);
68
+ }
7
69
  export function isInside(base, candidate) {
8
70
  const rel = path.relative(path.resolve(base), path.resolve(candidate));
9
71
  return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
10
72
  }
11
73
  async function loadSchema() {
12
- try {
13
- const require = createRequire(import.meta.url);
14
- const schemaPath = require.resolve("@droposs/plugin-sdk/schema.json");
15
- return JSON.parse(await readFile(schemaPath, "utf-8"));
16
- }
17
- catch {
18
- const fallback = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../plugin-sdk/schema/drop-plugin.schema.json");
19
- return JSON.parse(await readFile(fallback, "utf-8"));
74
+ const require = createRequire(import.meta.url);
75
+ for (const specifier of [
76
+ "@droposs/plugin-sdk/schema.json",
77
+ "@droposs/plugin-sdk/schema.json",
78
+ ]) {
79
+ try {
80
+ return JSON.parse(await readFile(require.resolve(specifier), "utf-8"));
81
+ }
82
+ catch {
83
+ // Try the next package scope before falling back to the monorepo path.
84
+ }
20
85
  }
86
+ const fallback = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../plugin-sdk/schema/drop-plugin.schema.json");
87
+ return JSON.parse(await readFile(fallback, "utf-8"));
21
88
  }
22
89
  let cachedValidator = null;
23
90
  let cachedErrors = [];
@@ -68,20 +135,11 @@ export async function listFiles(root, prefix = "") {
68
135
  }
69
136
  return results.sort((a, b) => a.localeCompare(b));
70
137
  }
71
- export async function signPlugin(targetDir, signingKey, validate = true) {
72
- const resolvedPath = path.resolve(process.cwd(), targetDir);
73
- const bundleDir = await realpath(resolvedPath).catch(() => null);
74
- if (!bundleDir) {
75
- throw new Error(`Directory not found: ${targetDir}`);
76
- }
77
- const manifestPath = path.join(bundleDir, MANIFEST_FILE);
78
- const manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
79
- if (validate) {
80
- const validation = await validateManifest(manifest);
81
- if (!validation.valid) {
82
- throw new Error(`Manifest validation failed against schema:\n ${validation.errors.join("\n ")}`);
83
- }
84
- }
138
+ /**
139
+ * Compute the derived manifest (entry checksum, file checksums, signature)
140
+ * without writing anything to disk.
141
+ */
142
+ async function deriveManifest(bundleDir, manifest, signingKey) {
85
143
  // Identify primary entry (v1 or v2 server/client entry)
86
144
  const entry = manifest.entry ??
87
145
  manifest.server?.entry ??
@@ -94,43 +152,189 @@ export async function signPlugin(targetDir, signingKey, validate = true) {
94
152
  const entryExists = await stat(entryPath).catch(() => null);
95
153
  if (entryExists) {
96
154
  const entryBytes = await readFile(entryPath);
97
- manifest.checksum = createHash("sha256").update(entryBytes).digest("hex");
155
+ manifest.checksum = sha256Hex(entryBytes);
98
156
  }
99
157
  const files = await listFiles(bundleDir);
100
158
  const fileChecksums = {};
101
- const aggregate = createHash("sha256");
102
159
  for (const rel of files) {
103
160
  const bytes = await readFile(path.join(bundleDir, rel));
104
- fileChecksums[rel] = createHash("sha256").update(bytes).digest("hex");
105
- aggregate.update(rel);
106
- aggregate.update("\0");
107
- aggregate.update(String(bytes.length));
108
- aggregate.update("\0");
109
- aggregate.update(bytes);
161
+ fileChecksums[rel] = sha256Hex(bytes);
110
162
  }
111
163
  manifest.files = fileChecksums;
112
164
  const key = signingKey ?? process.env.DROP_PLUGIN_SIGNING_KEY;
113
165
  if (key) {
166
+ manifest.signatureVersion = SIGNATURE_VERSION;
167
+ const filesAggregate = await computeFilesAggregate(bundleDir, files);
168
+ const payload = signaturePayloadV2(filesAggregate, manifest);
114
169
  manifest.signature = createHmac("sha256", key)
115
- .update(aggregate.digest("hex"))
170
+ .update(payload)
116
171
  .digest("hex");
117
172
  }
118
173
  else {
119
174
  delete manifest.signature;
175
+ delete manifest.signatureVersion;
120
176
  }
121
- await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
122
- return { fileCount: files.length, signed: Boolean(key) };
177
+ return { manifest, fileCount: files.length, signed: Boolean(key) };
123
178
  }
124
- export async function packPlugin(targetDir, outputDir) {
179
+ export async function signPlugin(targetDir, signingKey, validate = true, options = {}) {
125
180
  const resolvedPath = path.resolve(process.cwd(), targetDir);
126
181
  const bundleDir = await realpath(resolvedPath).catch(() => null);
127
182
  if (!bundleDir) {
128
183
  throw new Error(`Directory not found: ${targetDir}`);
129
184
  }
130
- // Ensure bundle is signed and validated
131
- await signPlugin(bundleDir);
132
185
  const manifestPath = path.join(bundleDir, MANIFEST_FILE);
133
186
  const manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
187
+ if (validate) {
188
+ const validation = await validateManifest(manifest);
189
+ if (!validation.valid) {
190
+ throw new Error(`Manifest validation failed against schema:\n ${validation.errors.join("\n ")}`);
191
+ }
192
+ }
193
+ const derived = await deriveManifest(bundleDir, manifest, signingKey);
194
+ const outPath = options.outManifest
195
+ ? path.resolve(process.cwd(), options.outManifest)
196
+ : manifestPath;
197
+ await writeFile(outPath, `${JSON.stringify(derived.manifest, null, 2)}\n`);
198
+ return { fileCount: derived.fileCount, signed: derived.signed };
199
+ }
200
+ /**
201
+ * Verify a bundle against its shipped manifest: schema validity, per-file and
202
+ * entry SHA-256 checksums, and (when present) the HMAC signature covering the
203
+ * file aggregate plus the manifest itself.
204
+ */
205
+ export async function verifyPlugin(targetDir, signingKey, options = {}) {
206
+ const resolvedPath = path.resolve(process.cwd(), targetDir);
207
+ const bundleDir = await realpath(resolvedPath).catch(() => null);
208
+ if (!bundleDir) {
209
+ throw new Error(`Directory not found: ${targetDir}`);
210
+ }
211
+ const manifestPath = path.join(bundleDir, MANIFEST_FILE);
212
+ const manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
213
+ const errors = [];
214
+ const validation = await validateManifest(manifest);
215
+ if (!validation.valid) {
216
+ errors.push(...validation.errors.map((error) => `schema: ${error}`));
217
+ }
218
+ const declared = manifest.files;
219
+ const files = await listFiles(bundleDir);
220
+ const present = new Set(files);
221
+ const codeFiles = files.filter(isBundleCodeFile);
222
+ if (declared) {
223
+ for (const rel of files) {
224
+ const digest = sha256Hex(await readFile(path.join(bundleDir, rel)));
225
+ if (declared[rel] !== digest) {
226
+ errors.push(`checksum mismatch: ${rel}`);
227
+ }
228
+ }
229
+ for (const rel of Object.keys(declared)) {
230
+ const resolved = path.resolve(bundleDir, rel);
231
+ if (path.isAbsolute(rel) || !isInside(bundleDir, resolved)) {
232
+ errors.push(`invalid bundle file path: ${rel}`);
233
+ continue;
234
+ }
235
+ if (!present.has(rel)) {
236
+ errors.push(`declared file missing: ${rel}`);
237
+ }
238
+ }
239
+ for (const rel of codeFiles) {
240
+ if (!(rel in declared)) {
241
+ errors.push(`bundle file '${rel}' is not covered by the manifest 'files' checksums`);
242
+ }
243
+ }
244
+ }
245
+ else if (codeFiles.length > 1) {
246
+ errors.push("bundle contains multiple code files but no 'files' checksums; refusing unverified imports");
247
+ }
248
+ const entry = manifest.entry ?? manifest.server?.entry ?? manifest.client?.entry;
249
+ if (entry) {
250
+ const entryPath = path.resolve(bundleDir, entry);
251
+ if (!isInside(bundleDir, entryPath)) {
252
+ errors.push(`entry path outside bundle: ${entry}`);
253
+ }
254
+ else if (!(await stat(entryPath).catch(() => null))) {
255
+ errors.push(`entry file missing: ${entry}`);
256
+ }
257
+ else if (manifest.checksum) {
258
+ const digest = sha256Hex(await readFile(entryPath));
259
+ if (digest !== manifest.checksum) {
260
+ errors.push(`entry checksum mismatch: ${entry}`);
261
+ }
262
+ }
263
+ }
264
+ const key = signingKey ?? process.env.DROP_PLUGIN_SIGNING_KEY;
265
+ let signed = false;
266
+ if (manifest.signature) {
267
+ if (!key) {
268
+ errors.push("manifest is signed but no signing key is available");
269
+ }
270
+ else {
271
+ const filesAggregate = await computeFilesAggregate(bundleDir, files);
272
+ const resolved = await resolveSignedPayload(bundleDir, manifest, filesAggregate, entry);
273
+ if (resolved.payload === undefined) {
274
+ errors.push(resolved.error ?? "unable to reconstruct signature payload");
275
+ }
276
+ else {
277
+ const expected = createHmac("sha256", key)
278
+ .update(resolved.payload)
279
+ .digest("hex");
280
+ if (safeEqualHex(expected, manifest.signature)) {
281
+ signed = true;
282
+ }
283
+ else {
284
+ errors.push("signature verification failed");
285
+ }
286
+ }
287
+ }
288
+ }
289
+ else if (!options.allowUnsigned) {
290
+ errors.push("bundle is unsigned (pass --allow-unsigned to accept unsigned bundles)");
291
+ }
292
+ return { valid: errors.length === 0, signed, errors };
293
+ }
294
+ /**
295
+ * Reconstruct the payload a signature covers, depending on the scheme:
296
+ * v2 covers the file aggregate plus the canonical manifest; legacy bundles
297
+ * cover the file aggregate, or the entry checksum for single-file bundles.
298
+ */
299
+ async function resolveSignedPayload(bundleDir, manifest, filesAggregate, entry) {
300
+ if (manifest.signatureVersion === SIGNATURE_VERSION) {
301
+ if (!manifest.files) {
302
+ return { error: "signatureVersion 2 requires manifest file checksums" };
303
+ }
304
+ return { payload: signaturePayloadV2(filesAggregate, manifest) };
305
+ }
306
+ if (manifest.signatureVersion !== undefined) {
307
+ return {
308
+ error: `unsupported signatureVersion: ${manifest.signatureVersion}`,
309
+ };
310
+ }
311
+ if (manifest.files) {
312
+ return { payload: filesAggregate };
313
+ }
314
+ if (manifest.checksum && entry) {
315
+ return {
316
+ payload: sha256Hex(await readFile(path.join(bundleDir, entry))),
317
+ };
318
+ }
319
+ return {
320
+ error: "legacy signature has neither file checksums nor an entry checksum",
321
+ };
322
+ }
323
+ export async function packPlugin(targetDir, outputDir) {
324
+ const resolvedPath = path.resolve(process.cwd(), targetDir);
325
+ const bundleDir = await realpath(resolvedPath).catch(() => null);
326
+ if (!bundleDir) {
327
+ throw new Error(`Directory not found: ${targetDir}`);
328
+ }
329
+ const manifestPath = path.join(bundleDir, MANIFEST_FILE);
330
+ const rawManifest = JSON.parse(await readFile(manifestPath, "utf-8"));
331
+ const validation = await validateManifest(rawManifest);
332
+ if (!validation.valid) {
333
+ throw new Error(`Manifest validation failed against schema:\n ${validation.errors.join("\n ")}`);
334
+ }
335
+ // Derive the signed manifest in memory: packing must not dirty the source
336
+ // tree, the derived fields live in the archive only.
337
+ const { manifest } = await deriveManifest(bundleDir, rawManifest);
134
338
  const id = manifest.id;
135
339
  const version = manifest.version;
136
340
  if (!id || !version) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@droposs/plugin-cli",
3
- "version": "0.5.9",
3
+ "version": "0.6.1",
4
4
  "description": "Drop Plugin build, test, signing, and packaging CLI for Drop OSS",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,18 +34,22 @@
34
34
  "packaging",
35
35
  "playnite"
36
36
  ],
37
+ "scripts": {
38
+ "build": "tsc",
39
+ "test": "node --test",
40
+ "typecheck": "tsc --noEmit -p tsconfig.typecheck.json"
41
+ },
42
+ "engines": {
43
+ "node": ">=22"
44
+ },
37
45
  "devDependencies": {
38
- "@types/node": "^22.0.0",
46
+ "@types/node": "^26.5.1",
39
47
  "typescript": "^5.7.0"
40
48
  },
41
49
  "license": "MIT",
42
50
  "dependencies": {
43
51
  "ajv": "^8.20.0",
44
52
  "esbuild": "^0.28.2",
45
- "@droposs/plugin-sdk": "0.5.9"
46
- },
47
- "scripts": {
48
- "build": "tsc",
49
- "test": "node --test"
53
+ "@droposs/plugin-sdk": "0.6.1"
50
54
  }
51
- }
55
+ }