@harborclient/sdk 0.4.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.
Files changed (106) hide show
  1. package/README.md +41 -0
  2. package/dist/client.d.ts +2 -0
  3. package/dist/clipboard.d.ts +27 -0
  4. package/dist/clipboard.d.ts.map +1 -0
  5. package/dist/clipboard.js +17 -0
  6. package/dist/http/index.d.ts +3 -0
  7. package/dist/http/index.d.ts.map +1 -0
  8. package/dist/http/index.js +2 -0
  9. package/dist/http/resolveRequest.d.ts +66 -0
  10. package/dist/http/resolveRequest.d.ts.map +1 -0
  11. package/dist/http/resolveRequest.js +191 -0
  12. package/dist/http/resolveRequest.test.d.ts +2 -0
  13. package/dist/http/resolveRequest.test.d.ts.map +1 -0
  14. package/dist/http/resolveRequest.test.js +40 -0
  15. package/dist/http/substitute.d.ts +29 -0
  16. package/dist/http/substitute.d.ts.map +1 -0
  17. package/dist/http/substitute.js +43 -0
  18. package/dist/http/substitute.test.d.ts +2 -0
  19. package/dist/http/substitute.test.d.ts.map +1 -0
  20. package/dist/http/substitute.test.js +85 -0
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +1 -0
  24. package/dist/main.d.ts +2 -0
  25. package/dist/main.d.ts.map +1 -0
  26. package/dist/main.js +1 -0
  27. package/dist/runtime/index.d.ts +20 -0
  28. package/dist/runtime/index.js +43 -0
  29. package/dist/runtime/jsx-dev-runtime.d.ts +12 -0
  30. package/dist/runtime/jsx-dev-runtime.js +15 -0
  31. package/dist/runtime/jsx-runtime.d.ts +35 -0
  32. package/dist/runtime/jsx-runtime.js +30 -0
  33. package/dist/runtime/react.d.ts +1 -0
  34. package/dist/runtime/react.js +46 -0
  35. package/dist/runtime/reactHost.js +26 -0
  36. package/dist/runtime/store.d.ts +19 -0
  37. package/dist/runtime/store.d.ts.map +1 -0
  38. package/dist/runtime/store.js +38 -0
  39. package/dist/runtime/store.ts +45 -0
  40. package/dist/runtime-utils.d.ts +36 -0
  41. package/dist/runtime-utils.d.ts.map +1 -0
  42. package/dist/runtime-utils.js +101 -0
  43. package/dist/runtime-utils.test.d.ts +2 -0
  44. package/dist/runtime-utils.test.d.ts.map +1 -0
  45. package/dist/runtime-utils.test.js +104 -0
  46. package/dist/signing/canonical.d.ts +27 -0
  47. package/dist/signing/canonical.d.ts.map +1 -0
  48. package/dist/signing/canonical.js +92 -0
  49. package/dist/signing/cli-sign.d.ts +3 -0
  50. package/dist/signing/cli-sign.d.ts.map +1 -0
  51. package/dist/signing/cli-sign.js +4 -0
  52. package/dist/signing/cli-verify.d.ts +3 -0
  53. package/dist/signing/cli-verify.d.ts.map +1 -0
  54. package/dist/signing/cli-verify.js +4 -0
  55. package/dist/signing/cli.d.ts +13 -0
  56. package/dist/signing/cli.d.ts.map +1 -0
  57. package/dist/signing/cli.js +148 -0
  58. package/dist/signing/index.d.ts +10 -0
  59. package/dist/signing/index.d.ts.map +1 -0
  60. package/dist/signing/index.js +7 -0
  61. package/dist/signing/inventory.d.ts +21 -0
  62. package/dist/signing/inventory.d.ts.map +1 -0
  63. package/dist/signing/inventory.js +80 -0
  64. package/dist/signing/manifest.d.ts +16 -0
  65. package/dist/signing/manifest.d.ts.map +1 -0
  66. package/dist/signing/manifest.js +33 -0
  67. package/dist/signing/sign.d.ts +10 -0
  68. package/dist/signing/sign.d.ts.map +1 -0
  69. package/dist/signing/sign.js +48 -0
  70. package/dist/signing/signing.test.d.ts +2 -0
  71. package/dist/signing/signing.test.d.ts.map +1 -0
  72. package/dist/signing/signing.test.js +100 -0
  73. package/dist/signing/testFixtures.d.ts +21 -0
  74. package/dist/signing/testFixtures.d.ts.map +1 -0
  75. package/dist/signing/testFixtures.js +41 -0
  76. package/dist/signing/types.d.ts +87 -0
  77. package/dist/signing/types.d.ts.map +1 -0
  78. package/dist/signing/types.js +12 -0
  79. package/dist/signing/verify.d.ts +17 -0
  80. package/dist/signing/verify.d.ts.map +1 -0
  81. package/dist/signing/verify.js +129 -0
  82. package/dist/storage/cappedList.d.ts +55 -0
  83. package/dist/storage/cappedList.d.ts.map +1 -0
  84. package/dist/storage/cappedList.js +102 -0
  85. package/dist/storage/cappedList.test.d.ts +2 -0
  86. package/dist/storage/cappedList.test.d.ts.map +1 -0
  87. package/dist/storage/cappedList.test.js +11 -0
  88. package/dist/storage/index.d.ts +2 -0
  89. package/dist/storage/index.d.ts.map +1 -0
  90. package/dist/storage/index.js +1 -0
  91. package/dist/types.d.ts +1282 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +1 -0
  94. package/dist/ui/format.d.ts +23 -0
  95. package/dist/ui/format.d.ts.map +1 -0
  96. package/dist/ui/format.js +45 -0
  97. package/dist/ui/index.d.ts +3 -0
  98. package/dist/ui/index.d.ts.map +1 -0
  99. package/dist/ui/index.js +2 -0
  100. package/dist/ui/tokens.d.ts +34 -0
  101. package/dist/ui/tokens.d.ts.map +1 -0
  102. package/dist/ui/tokens.js +56 -0
  103. package/dist/utilities.test.d.ts +2 -0
  104. package/dist/utilities.test.d.ts.map +1 -0
  105. package/dist/utilities.test.js +16 -0
  106. package/package.json +130 -0
@@ -0,0 +1,92 @@
1
+ import { PLUGIN_SIGNATURE_ALGORITHM, PLUGIN_SIGNATURE_SCHEMA_VERSION } from './types.js';
2
+ /**
3
+ * Builds the canonical signing payload object from manifest identity and file hashes.
4
+ *
5
+ * @param pluginId - Plugin manifest id.
6
+ * @param pluginVersion - Plugin manifest version.
7
+ * @param files - Sorted plugin file inventory.
8
+ * @param keyId - Optional signing key label.
9
+ */
10
+ export function buildSignaturePayload(pluginId, pluginVersion, files, keyId) {
11
+ const payload = {
12
+ schemaVersion: PLUGIN_SIGNATURE_SCHEMA_VERSION,
13
+ pluginId,
14
+ pluginVersion,
15
+ algorithm: PLUGIN_SIGNATURE_ALGORITHM,
16
+ files: [...files]
17
+ };
18
+ if (keyId?.trim()) {
19
+ payload.keyId = keyId.trim();
20
+ }
21
+ return payload;
22
+ }
23
+ /**
24
+ * Serializes a signing payload to canonical JSON bytes for Ed25519 signing.
25
+ *
26
+ * @param payload - Unsigned signature payload.
27
+ */
28
+ export function canonicalizeSignaturePayload(payload) {
29
+ return Buffer.from(JSON.stringify(payload), 'utf8');
30
+ }
31
+ /**
32
+ * Parses and validates a signature.json object read from disk.
33
+ *
34
+ * @param raw - Parsed JSON value.
35
+ * @returns Validated signature file contents.
36
+ * @throws When the payload shape is invalid.
37
+ */
38
+ export function parsePluginSignatureFile(raw) {
39
+ if (typeof raw !== 'object' || raw == null) {
40
+ throw new Error('Plugin signature must be a JSON object.');
41
+ }
42
+ const record = raw;
43
+ if (record.schemaVersion !== PLUGIN_SIGNATURE_SCHEMA_VERSION) {
44
+ throw new Error(`Unsupported plugin signature schema version: ${String(record.schemaVersion)}`);
45
+ }
46
+ if (record.algorithm !== PLUGIN_SIGNATURE_ALGORITHM) {
47
+ throw new Error(`Unsupported plugin signature algorithm: ${String(record.algorithm)}`);
48
+ }
49
+ if (typeof record.pluginId !== 'string' || record.pluginId.trim().length === 0) {
50
+ throw new Error('Plugin signature is missing pluginId.');
51
+ }
52
+ if (typeof record.pluginVersion !== 'string' || record.pluginVersion.trim().length === 0) {
53
+ throw new Error('Plugin signature is missing pluginVersion.');
54
+ }
55
+ if (typeof record.signature !== 'string' || record.signature.trim().length === 0) {
56
+ throw new Error('Plugin signature is missing signature.');
57
+ }
58
+ if (!Array.isArray(record.files)) {
59
+ throw new Error('Plugin signature files must be an array.');
60
+ }
61
+ const files = record.files.map((entry, index) => {
62
+ if (typeof entry !== 'object' || entry == null) {
63
+ throw new Error(`Plugin signature files[${index}] must be an object.`);
64
+ }
65
+ const fileRecord = entry;
66
+ if (typeof fileRecord.path !== 'string' || fileRecord.path.trim().length === 0) {
67
+ throw new Error(`Plugin signature files[${index}].path is invalid.`);
68
+ }
69
+ if (typeof fileRecord.sha256 !== 'string' || !/^[a-f0-9]{64}$/i.test(fileRecord.sha256)) {
70
+ throw new Error(`Plugin signature files[${index}].sha256 is invalid.`);
71
+ }
72
+ return {
73
+ path: fileRecord.path,
74
+ sha256: fileRecord.sha256.toLowerCase()
75
+ };
76
+ });
77
+ const payload = {
78
+ schemaVersion: PLUGIN_SIGNATURE_SCHEMA_VERSION,
79
+ pluginId: record.pluginId,
80
+ pluginVersion: record.pluginVersion,
81
+ algorithm: PLUGIN_SIGNATURE_ALGORITHM,
82
+ files,
83
+ signature: record.signature
84
+ };
85
+ if (record.keyId != null) {
86
+ if (typeof record.keyId !== 'string' || record.keyId.trim().length === 0) {
87
+ throw new Error('Plugin signature keyId must be a non-empty string when present.');
88
+ }
89
+ payload.keyId = record.keyId.trim();
90
+ }
91
+ return payload;
92
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli-sign.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-sign.d.ts","sourceRoot":"","sources":["../../src/signing/cli-sign.ts"],"names":[],"mappings":""}
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { runSignCli } from './cli.js';
3
+ const exitCode = await runSignCli(process.argv);
4
+ process.exit(exitCode);
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli-verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-verify.d.ts","sourceRoot":"","sources":["../../src/signing/cli-verify.ts"],"names":[],"mappings":""}
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { runVerifyCli } from './cli.js';
3
+ const exitCode = await runVerifyCli(process.argv);
4
+ process.exit(exitCode);
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Runs the plugin sign CLI and returns a process exit code.
3
+ *
4
+ * @param argv - Raw process argv including node and script paths.
5
+ */
6
+ export declare function runSignCli(argv: string[]): Promise<number>;
7
+ /**
8
+ * Runs the plugin verify CLI and returns a process exit code.
9
+ *
10
+ * @param argv - Raw process argv including node and script paths.
11
+ */
12
+ export declare function runVerifyCli(argv: string[]): Promise<number>;
13
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/signing/cli.ts"],"names":[],"mappings":"AAwFA;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BhE;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAyClE"}
@@ -0,0 +1,148 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { signPlugin } from './sign.js';
4
+ import { verifyPlugin } from './verify.js';
5
+ const SIGN_USAGE = 'Usage: hc-plugin-sign --dir <pluginDir> --private-key <path> [--key-id <id>] [--signature <path>]';
6
+ const VERIFY_USAGE = 'Usage: hc-plugin-verify --dir <pluginDir> --public-key <path> [--public-key <path> ...] [--signature <path>] [--allow-unsigned]';
7
+ /**
8
+ * Parses supported CLI flags from process argv.
9
+ *
10
+ * @param argv - Raw process argv including node and script paths.
11
+ */
12
+ function parseCliArgs(argv) {
13
+ const parsed = {
14
+ publicKeyPaths: [],
15
+ allowUnsigned: false
16
+ };
17
+ for (let index = 2; index < argv.length; index += 1) {
18
+ const arg = argv[index];
19
+ if (arg === '--') {
20
+ continue;
21
+ }
22
+ switch (arg) {
23
+ case '--dir': {
24
+ parsed.dir = argv[index + 1];
25
+ index += 1;
26
+ break;
27
+ }
28
+ case '--private-key': {
29
+ parsed.privateKeyPath = argv[index + 1];
30
+ index += 1;
31
+ break;
32
+ }
33
+ case '--public-key': {
34
+ const value = argv[index + 1];
35
+ if (value) {
36
+ parsed.publicKeyPaths.push(value);
37
+ }
38
+ index += 1;
39
+ break;
40
+ }
41
+ case '--key-id': {
42
+ parsed.keyId = argv[index + 1];
43
+ index += 1;
44
+ break;
45
+ }
46
+ case '--signature': {
47
+ parsed.signaturePath = argv[index + 1];
48
+ index += 1;
49
+ break;
50
+ }
51
+ case '--allow-unsigned': {
52
+ parsed.allowUnsigned = true;
53
+ break;
54
+ }
55
+ default:
56
+ throw new Error(`Unknown argument: ${arg}`);
57
+ }
58
+ }
59
+ return parsed;
60
+ }
61
+ /**
62
+ * Reads a PEM key file from disk.
63
+ *
64
+ * @param path - Absolute or relative key file path.
65
+ */
66
+ function readKeyFile(path) {
67
+ return readFileSync(resolve(path), 'utf8');
68
+ }
69
+ /**
70
+ * Runs the plugin sign CLI and returns a process exit code.
71
+ *
72
+ * @param argv - Raw process argv including node and script paths.
73
+ */
74
+ export async function runSignCli(argv) {
75
+ let parsed;
76
+ try {
77
+ parsed = parseCliArgs(argv);
78
+ }
79
+ catch (error) {
80
+ const message = error instanceof Error ? error.message : String(error);
81
+ console.error(message);
82
+ console.error(SIGN_USAGE);
83
+ return 1;
84
+ }
85
+ if (!parsed.dir || !parsed.privateKeyPath) {
86
+ console.error(SIGN_USAGE);
87
+ return 1;
88
+ }
89
+ try {
90
+ const result = await signPlugin({
91
+ pluginDir: parsed.dir,
92
+ privateKeyPem: readKeyFile(parsed.privateKeyPath),
93
+ keyId: parsed.keyId,
94
+ signaturePath: parsed.signaturePath
95
+ });
96
+ console.log(`Wrote ${result.signaturePath}`);
97
+ return 0;
98
+ }
99
+ catch (error) {
100
+ const message = error instanceof Error ? error.message : String(error);
101
+ console.error(message);
102
+ return 2;
103
+ }
104
+ }
105
+ /**
106
+ * Runs the plugin verify CLI and returns a process exit code.
107
+ *
108
+ * @param argv - Raw process argv including node and script paths.
109
+ */
110
+ export async function runVerifyCli(argv) {
111
+ let parsed;
112
+ try {
113
+ parsed = parseCliArgs(argv);
114
+ }
115
+ catch (error) {
116
+ const message = error instanceof Error ? error.message : String(error);
117
+ console.error(message);
118
+ console.error(VERIFY_USAGE);
119
+ return 1;
120
+ }
121
+ if (!parsed.dir || parsed.publicKeyPaths.length === 0) {
122
+ console.error(VERIFY_USAGE);
123
+ return 1;
124
+ }
125
+ try {
126
+ const result = await verifyPlugin({
127
+ pluginDir: parsed.dir,
128
+ trustedPublicKeysPem: parsed.publicKeyPaths.map(readKeyFile),
129
+ signaturePath: parsed.signaturePath
130
+ });
131
+ if (result.status === 'valid') {
132
+ const keyLabel = result.keyId ? ` (keyId: ${result.keyId})` : '';
133
+ console.log(`Plugin signature is valid${keyLabel}.`);
134
+ return 0;
135
+ }
136
+ if (result.status === 'unsigned') {
137
+ console.error('Plugin is unsigned.');
138
+ return parsed.allowUnsigned ? 0 : 4;
139
+ }
140
+ console.error(result.error ?? 'Plugin signature is invalid.');
141
+ return 3;
142
+ }
143
+ catch (error) {
144
+ const message = error instanceof Error ? error.message : String(error);
145
+ console.error(message);
146
+ return 3;
147
+ }
148
+ }
@@ -0,0 +1,10 @@
1
+ export { PLUGIN_SIGNATURE_ALGORITHM, PLUGIN_SIGNATURE_FILENAME, PLUGIN_SIGNATURE_SCHEMA_VERSION } from './types.js';
2
+ export type { PluginFileHash, PluginSignatureFile, PluginSignaturePayload, PluginVerifyStatus, SignPluginOptions, SignPluginResult, VerifyPluginOptions, VerifyPluginResult } from './types.js';
3
+ export { readPluginManifestIdentity } from './manifest.js';
4
+ export type { PluginManifestIdentity } from './manifest.js';
5
+ export { assertPluginDirectory, collectPluginFiles, shouldExcludePluginPath } from './inventory.js';
6
+ export { buildSignaturePayload, canonicalizeSignaturePayload, parsePluginSignatureFile } from './canonical.js';
7
+ export { signPlugin } from './sign.js';
8
+ export { readPluginSignature, verifyPlugin } from './verify.js';
9
+ export { runSignCli, runVerifyCli } from './cli.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/signing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAC1B,yBAAyB,EACzB,+BAA+B,EAChC,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,cAAc,EACd,mBAAmB,EACnB,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,mBAAmB,EACnB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAC3D,YAAY,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAE5D,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAEpG,OAAO,EACL,qBAAqB,EACrB,4BAA4B,EAC5B,wBAAwB,EACzB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC"}
@@ -0,0 +1,7 @@
1
+ export { PLUGIN_SIGNATURE_ALGORITHM, PLUGIN_SIGNATURE_FILENAME, PLUGIN_SIGNATURE_SCHEMA_VERSION } from './types.js';
2
+ export { readPluginManifestIdentity } from './manifest.js';
3
+ export { assertPluginDirectory, collectPluginFiles, shouldExcludePluginPath } from './inventory.js';
4
+ export { buildSignaturePayload, canonicalizeSignaturePayload, parsePluginSignatureFile } from './canonical.js';
5
+ export { signPlugin } from './sign.js';
6
+ export { readPluginSignature, verifyPlugin } from './verify.js';
7
+ export { runSignCli, runVerifyCli } from './cli.js';
@@ -0,0 +1,21 @@
1
+ import type { PluginFileHash } from './types.js';
2
+ /**
3
+ * Returns true when a relative plugin path should be excluded from signing.
4
+ *
5
+ * @param relativePath - Path relative to the plugin root using POSIX separators.
6
+ */
7
+ export declare function shouldExcludePluginPath(relativePath: string): boolean;
8
+ /**
9
+ * Walks a plugin directory and returns a sorted inventory of file hashes.
10
+ *
11
+ * @param pluginDir - Plugin root directory.
12
+ * @returns File paths (POSIX) and SHA-256 digests sorted by path.
13
+ */
14
+ export declare function collectPluginFiles(pluginDir: string): PluginFileHash[];
15
+ /**
16
+ * Returns true when a plugin path exists and is a directory.
17
+ *
18
+ * @param pluginDir - Plugin root directory.
19
+ */
20
+ export declare function assertPluginDirectory(pluginDir: string): void;
21
+ //# sourceMappingURL=inventory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inventory.d.ts","sourceRoot":"","sources":["../../src/signing/inventory.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAKjD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAOrE;AAWD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,EAAE,CAoCtE;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAS7D"}
@@ -0,0 +1,80 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
3
+ import { join, relative } from 'node:path';
4
+ import { PLUGIN_SIGNATURE_FILENAME } from './types.js';
5
+ const EXCLUDED_PATH_SEGMENTS = new Set(['.git', 'node_modules']);
6
+ const EXCLUDED_FILE_NAMES = new Set(['.DS_Store', PLUGIN_SIGNATURE_FILENAME]);
7
+ /**
8
+ * Returns true when a relative plugin path should be excluded from signing.
9
+ *
10
+ * @param relativePath - Path relative to the plugin root using POSIX separators.
11
+ */
12
+ export function shouldExcludePluginPath(relativePath) {
13
+ const segments = relativePath.split('/');
14
+ if (segments.some((segment) => EXCLUDED_PATH_SEGMENTS.has(segment))) {
15
+ return true;
16
+ }
17
+ const fileName = segments.at(-1);
18
+ return fileName != null && EXCLUDED_FILE_NAMES.has(fileName);
19
+ }
20
+ /**
21
+ * Computes a SHA-256 hex digest for one file.
22
+ *
23
+ * @param absolutePath - Absolute path to the file on disk.
24
+ */
25
+ function hashFile(absolutePath) {
26
+ return createHash('sha256').update(readFileSync(absolutePath)).digest('hex');
27
+ }
28
+ /**
29
+ * Walks a plugin directory and returns a sorted inventory of file hashes.
30
+ *
31
+ * @param pluginDir - Plugin root directory.
32
+ * @returns File paths (POSIX) and SHA-256 digests sorted by path.
33
+ */
34
+ export function collectPluginFiles(pluginDir) {
35
+ const files = [];
36
+ /**
37
+ * Recursively collects file hashes under one directory.
38
+ *
39
+ * @param currentDir - Absolute directory being scanned.
40
+ */
41
+ function walk(currentDir) {
42
+ for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
43
+ const absolutePath = join(currentDir, entry.name);
44
+ const relativePath = relative(pluginDir, absolutePath).split('\\').join('/');
45
+ if (shouldExcludePluginPath(relativePath)) {
46
+ continue;
47
+ }
48
+ if (entry.isDirectory()) {
49
+ walk(absolutePath);
50
+ continue;
51
+ }
52
+ if (!entry.isFile()) {
53
+ continue;
54
+ }
55
+ files.push({
56
+ path: relativePath,
57
+ sha256: hashFile(absolutePath)
58
+ });
59
+ }
60
+ }
61
+ walk(pluginDir);
62
+ files.sort((left, right) => left.path.localeCompare(right.path));
63
+ return files;
64
+ }
65
+ /**
66
+ * Returns true when a plugin path exists and is a directory.
67
+ *
68
+ * @param pluginDir - Plugin root directory.
69
+ */
70
+ export function assertPluginDirectory(pluginDir) {
71
+ try {
72
+ const stats = statSync(pluginDir);
73
+ if (!stats.isDirectory()) {
74
+ throw new Error(`Plugin directory is not a folder: ${pluginDir}`);
75
+ }
76
+ }
77
+ catch (error) {
78
+ throw new Error(`Plugin directory not found: ${pluginDir}`, { cause: error });
79
+ }
80
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Minimal manifest fields required for plugin signing.
3
+ */
4
+ export interface PluginManifestIdentity {
5
+ id: string;
6
+ version: string;
7
+ }
8
+ /**
9
+ * Reads and validates plugin id and version from manifest.json.
10
+ *
11
+ * @param pluginDir - Plugin root directory containing manifest.json.
12
+ * @returns Parsed manifest identity fields.
13
+ * @throws When manifest.json is missing, invalid JSON, or fails validation.
14
+ */
15
+ export declare function readPluginManifestIdentity(pluginDir: string): PluginManifestIdentity;
16
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/signing/manifest.ts"],"names":[],"mappings":"AAKA;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG,sBAAsB,CAyBpF"}
@@ -0,0 +1,33 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ const PLUGIN_ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9.-]*\.[a-zA-Z][a-zA-Z0-9.-]+$/;
4
+ /**
5
+ * Reads and validates plugin id and version from manifest.json.
6
+ *
7
+ * @param pluginDir - Plugin root directory containing manifest.json.
8
+ * @returns Parsed manifest identity fields.
9
+ * @throws When manifest.json is missing, invalid JSON, or fails validation.
10
+ */
11
+ export function readPluginManifestIdentity(pluginDir) {
12
+ const manifestPath = join(pluginDir, 'manifest.json');
13
+ let raw;
14
+ try {
15
+ raw = JSON.parse(readFileSync(manifestPath, 'utf8'));
16
+ }
17
+ catch (error) {
18
+ throw new Error(`Plugin manifest is not valid JSON: ${manifestPath}`, { cause: error });
19
+ }
20
+ if (typeof raw !== 'object' || raw == null) {
21
+ throw new Error(`Plugin manifest must be a JSON object: ${manifestPath}`);
22
+ }
23
+ const record = raw;
24
+ const id = record.id;
25
+ const version = record.version;
26
+ if (typeof id !== 'string' || !PLUGIN_ID_PATTERN.test(id)) {
27
+ throw new Error(`Plugin manifest id is invalid: ${manifestPath}`);
28
+ }
29
+ if (typeof version !== 'string' || version.trim().length === 0) {
30
+ throw new Error(`Plugin manifest version is invalid: ${manifestPath}`);
31
+ }
32
+ return { id, version };
33
+ }
@@ -0,0 +1,10 @@
1
+ import type { SignPluginOptions, SignPluginResult } from './types.js';
2
+ /**
3
+ * Signs a plugin directory and writes signature.json beside manifest.json.
4
+ *
5
+ * @param options - Plugin directory and signing key options.
6
+ * @returns Path to the written signature file and parsed signature contents.
7
+ * @throws When manifest validation, inventory, or signing fails.
8
+ */
9
+ export declare function signPlugin(options: SignPluginOptions): Promise<SignPluginResult>;
10
+ //# sourceMappingURL=sign.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sign.d.ts","sourceRoot":"","sources":["../../src/signing/sign.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEtE;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA6BtF"}
@@ -0,0 +1,48 @@
1
+ import { createPrivateKey, sign } from 'node:crypto';
2
+ import { writeFileSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { buildSignaturePayload, canonicalizeSignaturePayload } from './canonical.js';
5
+ import { assertPluginDirectory, collectPluginFiles } from './inventory.js';
6
+ import { readPluginManifestIdentity } from './manifest.js';
7
+ import { PLUGIN_SIGNATURE_FILENAME } from './types.js';
8
+ /**
9
+ * Signs a plugin directory and writes signature.json beside manifest.json.
10
+ *
11
+ * @param options - Plugin directory and signing key options.
12
+ * @returns Path to the written signature file and parsed signature contents.
13
+ * @throws When manifest validation, inventory, or signing fails.
14
+ */
15
+ export async function signPlugin(options) {
16
+ const pluginDir = resolve(options.pluginDir);
17
+ assertPluginDirectory(pluginDir);
18
+ const { id, version } = readPluginManifestIdentity(pluginDir);
19
+ const files = collectPluginFiles(pluginDir);
20
+ const payload = buildSignaturePayload(id, version, files, options.keyId);
21
+ const payloadBytes = canonicalizeSignaturePayload(payload);
22
+ let privateKey;
23
+ try {
24
+ privateKey = createPrivateKey(options.privateKeyPem);
25
+ }
26
+ catch (error) {
27
+ throw new Error('Invalid Ed25519 private key', { cause: error });
28
+ }
29
+ const signature = sign(null, new Uint8Array(payloadBytes), privateKey).toString('base64');
30
+ const signatureFile = {
31
+ ...payload,
32
+ signature
33
+ };
34
+ const signaturePath = resolve(options.signaturePath ?? joinSignaturePath(pluginDir));
35
+ writeFileSync(signaturePath, `${JSON.stringify(signatureFile, null, 2)}\n`, 'utf8');
36
+ return {
37
+ signaturePath,
38
+ signature: signatureFile
39
+ };
40
+ }
41
+ /**
42
+ * Returns the default signature.json path for one plugin directory.
43
+ *
44
+ * @param pluginDir - Plugin root directory.
45
+ */
46
+ function joinSignaturePath(pluginDir) {
47
+ return resolve(pluginDir, PLUGIN_SIGNATURE_FILENAME);
48
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=signing.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signing.test.d.ts","sourceRoot":"","sources":["../../src/signing/signing.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,100 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+ import { writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { signPlugin } from './sign.js';
5
+ import { createTestPluginDir, createTestSigningKeys } from './testFixtures.js';
6
+ import { verifyPlugin } from './verify.js';
7
+ describe('signPlugin', () => {
8
+ it('writes signature.json for a valid plugin directory', async () => {
9
+ const keys = createTestSigningKeys();
10
+ const fixture = createTestPluginDir();
11
+ try {
12
+ const result = await signPlugin({
13
+ pluginDir: fixture.pluginDir,
14
+ privateKeyPem: keys.privateKeyPem,
15
+ keyId: 'test-key'
16
+ });
17
+ expect(result.signature.pluginId).toBe('com.example.test-plugin');
18
+ expect(result.signature.pluginVersion).toBe('1.0.0');
19
+ expect(result.signature.keyId).toBe('test-key');
20
+ expect(result.signature.files.map((file) => file.path)).toEqual([
21
+ 'dist/renderer.js',
22
+ 'manifest.json'
23
+ ]);
24
+ const verification = await verifyPlugin({
25
+ pluginDir: fixture.pluginDir,
26
+ trustedPublicKeysPem: [keys.publicKeyPem]
27
+ });
28
+ expect(verification.status).toBe('valid');
29
+ }
30
+ finally {
31
+ fixture.cleanup();
32
+ }
33
+ });
34
+ it('rejects invalid private keys', async () => {
35
+ const fixture = createTestPluginDir();
36
+ try {
37
+ await expect(signPlugin({
38
+ pluginDir: fixture.pluginDir,
39
+ privateKeyPem: 'not-a-key'
40
+ })).rejects.toThrow(/invalid ed25519 private key/i);
41
+ }
42
+ finally {
43
+ fixture.cleanup();
44
+ }
45
+ });
46
+ });
47
+ describe('verifyPlugin', () => {
48
+ it('returns unsigned when signature.json is absent', async () => {
49
+ const fixture = createTestPluginDir();
50
+ try {
51
+ const result = await verifyPlugin({
52
+ pluginDir: fixture.pluginDir,
53
+ trustedPublicKeysPem: [createTestSigningKeys().publicKeyPem]
54
+ });
55
+ expect(result.status).toBe('unsigned');
56
+ }
57
+ finally {
58
+ fixture.cleanup();
59
+ }
60
+ });
61
+ it('returns invalid when a signed file is tampered with', async () => {
62
+ const keys = createTestSigningKeys();
63
+ const fixture = createTestPluginDir();
64
+ try {
65
+ await signPlugin({
66
+ pluginDir: fixture.pluginDir,
67
+ privateKeyPem: keys.privateKeyPem
68
+ });
69
+ writeFileSync(join(fixture.pluginDir, 'dist', 'renderer.js'), 'export function activate() { return 1; }');
70
+ const result = await verifyPlugin({
71
+ pluginDir: fixture.pluginDir,
72
+ trustedPublicKeysPem: [keys.publicKeyPem]
73
+ });
74
+ expect(result.status).toBe('invalid');
75
+ expect(result.error).toMatch(/signed inventory/i);
76
+ }
77
+ finally {
78
+ fixture.cleanup();
79
+ }
80
+ });
81
+ it('returns invalid when verified with the wrong public key', async () => {
82
+ const keys = createTestSigningKeys();
83
+ const otherKeys = createTestSigningKeys();
84
+ const fixture = createTestPluginDir();
85
+ try {
86
+ await signPlugin({
87
+ pluginDir: fixture.pluginDir,
88
+ privateKeyPem: keys.privateKeyPem
89
+ });
90
+ const result = await verifyPlugin({
91
+ pluginDir: fixture.pluginDir,
92
+ trustedPublicKeysPem: [otherKeys.publicKeyPem]
93
+ });
94
+ expect(result.status).toBe('invalid');
95
+ }
96
+ finally {
97
+ fixture.cleanup();
98
+ }
99
+ });
100
+ });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Temporary Ed25519 key pair for signing tests.
3
+ */
4
+ export interface TestSigningKeys {
5
+ privateKeyPem: string;
6
+ publicKeyPem: string;
7
+ }
8
+ /**
9
+ * Generates an Ed25519 PEM key pair for tests.
10
+ */
11
+ export declare function createTestSigningKeys(): TestSigningKeys;
12
+ /**
13
+ * Creates a minimal plugin directory suitable for signing tests.
14
+ *
15
+ * @param pluginId - Plugin manifest id.
16
+ */
17
+ export declare function createTestPluginDir(pluginId?: string): {
18
+ pluginDir: string;
19
+ cleanup: () => void;
20
+ };
21
+ //# sourceMappingURL=testFixtures.d.ts.map