@droposs/plugin-cli 0.6.1 → 0.6.3

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/signer.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { SIGNATURE_VERSION } from "@drop-oss/plugin-sdk";
1
+ import { SIGNATURE_VERSION, type PluginManifest } from "@drop-oss/plugin-sdk";
2
2
  /** Current signature scheme; re-exported for backwards compatibility. */
3
3
  export { SIGNATURE_VERSION };
4
4
  /**
@@ -39,6 +39,13 @@ export interface VerifyResult {
39
39
  export declare function verifyPlugin(targetDir: string, signingKey?: string, options?: {
40
40
  allowUnsigned?: boolean;
41
41
  }): Promise<VerifyResult>;
42
+ /**
43
+ * Validate the `client.sidecars` declaration against the bundle contents:
44
+ * each target path must resolve inside the bundle, point to a present regular
45
+ * file covered by the `files` checksums, and cite a matching SHA-256 of the
46
+ * binary. Every sidecar name must be allowlisted in `client.commands`, and
47
+ */
48
+ export declare function verifySidecars(bundleDir: string, manifest: PluginManifest & Record<string, unknown>, files: string[], present: Set<string>, errors: string[]): Promise<void>;
42
49
  export declare function packPlugin(targetDir: string, outputDir?: string): Promise<{
43
50
  packagePath: string;
44
51
  id: string;
package/dist/signer.js CHANGED
@@ -91,7 +91,6 @@ let cachedErrors = [];
91
91
  export async function validateManifest(manifest) {
92
92
  if (!cachedValidator) {
93
93
  const schema = await loadSchema();
94
- // @ts-ignore
95
94
  const AjvClass = Ajv.default ?? Ajv;
96
95
  const ajv = new AjvClass({ allErrors: true, strict: false });
97
96
  const compiled = ajv.compile(schema);
@@ -189,6 +188,13 @@ export async function signPlugin(targetDir, signingKey, validate = true, options
189
188
  if (!validation.valid) {
190
189
  throw new Error(`Manifest validation failed against schema:\n ${validation.errors.join("\n ")}`);
191
190
  }
191
+ const files = await listFiles(bundleDir);
192
+ const present = new Set(files);
193
+ const sidecarErrors = [];
194
+ await verifySidecars(bundleDir, manifest, files, present, sidecarErrors);
195
+ if (sidecarErrors.length > 0) {
196
+ throw new Error(`Sidecar validation failed:\n ${sidecarErrors.join("\n ")}`);
197
+ }
192
198
  }
193
199
  const derived = await deriveManifest(bundleDir, manifest, signingKey);
194
200
  const outPath = options.outManifest
@@ -246,6 +252,7 @@ export async function verifyPlugin(targetDir, signingKey, options = {}) {
246
252
  errors.push("bundle contains multiple code files but no 'files' checksums; refusing unverified imports");
247
253
  }
248
254
  const entry = manifest.entry ?? manifest.server?.entry ?? manifest.client?.entry;
255
+ await verifySidecars(bundleDir, manifest, files, present, errors);
249
256
  if (entry) {
250
257
  const entryPath = path.resolve(bundleDir, entry);
251
258
  if (!isInside(bundleDir, entryPath)) {
@@ -320,6 +327,72 @@ async function resolveSignedPayload(bundleDir, manifest, filesAggregate, entry)
320
327
  error: "legacy signature has neither file checksums nor an entry checksum",
321
328
  };
322
329
  }
330
+ /**
331
+ * Validate the `client.sidecars` declaration against the bundle contents:
332
+ * each target path must resolve inside the bundle, point to a present regular
333
+ * file covered by the `files` checksums, and cite a matching SHA-256 of the
334
+ * binary. Every sidecar name must be allowlisted in `client.commands`, and
335
+ */
336
+ export async function verifySidecars(bundleDir, manifest, files, present, errors) {
337
+ const sidecars = manifest.client?.sidecars;
338
+ if (sidecars === undefined)
339
+ return;
340
+ if (!Array.isArray(sidecars) || sidecars.length === 0) {
341
+ errors.push("client.sidecars must be a non-empty array when declared");
342
+ return;
343
+ }
344
+ const commands = new Set(Array.isArray(manifest.client?.commands) ? manifest.client.commands : []);
345
+ for (const [idx, sidecar] of sidecars.entries()) {
346
+ const label = `client.sidecars[${idx}]`;
347
+ if (typeof sidecar !== "object" ||
348
+ sidecar === null ||
349
+ typeof sidecar.name !== "string" ||
350
+ !Array.isArray(sidecar.targets)) {
351
+ errors.push(`${label}: expected { name: string, targets: array }`);
352
+ continue;
353
+ }
354
+ const { name, targets } = sidecar;
355
+ if (!commands.has(name)) {
356
+ errors.push(`${label}: sidecar name '${name}' must be allowlisted in client.commands`);
357
+ }
358
+ const seenTargets = new Set();
359
+ for (const [tIdx, target] of targets.entries()) {
360
+ const tLabel = `${label}.targets[${tIdx}]`;
361
+ if (!target || typeof target !== "object") {
362
+ errors.push(`${tLabel}: expected target object`);
363
+ continue;
364
+ }
365
+ if (typeof target.os !== "string" || typeof target.arch !== "string") {
366
+ errors.push(`${tLabel}: expected string os and arch`);
367
+ continue;
368
+ }
369
+ const key = `${target.os}-${target.arch}`;
370
+ if (seenTargets.has(key)) {
371
+ errors.push(`${tLabel}: duplicate target '${key}' (only one binary per os+arch for sidecar '${name}')`);
372
+ continue;
373
+ }
374
+ seenTargets.add(key);
375
+ if (typeof target.path !== "string" ||
376
+ path.isAbsolute(target.path) ||
377
+ !isInside(bundleDir, path.resolve(bundleDir, target.path))) {
378
+ errors.push(`${tLabel}: path must be a bundle-relative path`);
379
+ continue;
380
+ }
381
+ if (isBundleCodeFile(target.path)) {
382
+ errors.push(`${tLabel}: 'sidecars' paths must not be JavaScript code files`);
383
+ continue;
384
+ }
385
+ if (!present.has(target.path)) {
386
+ errors.push(`${tLabel}: declared sidecar file missing: ${target.path}`);
387
+ continue;
388
+ }
389
+ const digest = sha256Hex(await readFile(path.join(bundleDir, target.path)));
390
+ if (target.sha256 !== digest) {
391
+ errors.push(`${tLabel}: sha256 mismatch for ${target.path} (declared ${target.sha256}, actual ${digest})`);
392
+ }
393
+ }
394
+ }
395
+ }
323
396
  export async function packPlugin(targetDir, outputDir) {
324
397
  const resolvedPath = path.resolve(process.cwd(), targetDir);
325
398
  const bundleDir = await realpath(resolvedPath).catch(() => null);
@@ -332,6 +405,13 @@ export async function packPlugin(targetDir, outputDir) {
332
405
  if (!validation.valid) {
333
406
  throw new Error(`Manifest validation failed against schema:\n ${validation.errors.join("\n ")}`);
334
407
  }
408
+ const files = await listFiles(bundleDir);
409
+ const present = new Set(files);
410
+ const sidecarErrors = [];
411
+ await verifySidecars(bundleDir, rawManifest, files, present, sidecarErrors);
412
+ if (sidecarErrors.length > 0) {
413
+ throw new Error(`Sidecar validation failed:\n ${sidecarErrors.join("\n ")}`);
414
+ }
335
415
  // Derive the signed manifest in memory: packing must not dirty the source
336
416
  // tree, the derived fields live in the archive only.
337
417
  const { manifest } = await deriveManifest(bundleDir, rawManifest);
@@ -344,7 +424,6 @@ export async function packPlugin(targetDir, outputDir) {
344
424
  ? path.resolve(process.cwd(), outputDir)
345
425
  : path.join(bundleDir, "dist-package");
346
426
  await mkdir(outDir, { recursive: true });
347
- const files = await listFiles(bundleDir);
348
427
  const bundleMap = {};
349
428
  for (const rel of files) {
350
429
  const content = await readFile(path.join(bundleDir, rel));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@droposs/plugin-cli",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Drop Plugin build, test, signing, and packaging CLI for Drop OSS",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,6 +50,6 @@
50
50
  "dependencies": {
51
51
  "ajv": "^8.20.0",
52
52
  "esbuild": "^0.28.2",
53
- "@droposs/plugin-sdk": "0.6.1"
53
+ "@droposs/plugin-sdk": "0.6.3"
54
54
  }
55
55
  }