@objectstack/core 0.8.2 → 0.9.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.
- package/API_REGISTRY.md +392 -0
- package/CHANGELOG.md +8 -0
- package/README.md +36 -0
- package/dist/api-registry-plugin.d.ts +54 -0
- package/dist/api-registry-plugin.d.ts.map +1 -0
- package/dist/api-registry-plugin.js +53 -0
- package/dist/api-registry-plugin.test.d.ts +2 -0
- package/dist/api-registry-plugin.test.d.ts.map +1 -0
- package/dist/api-registry-plugin.test.js +332 -0
- package/dist/api-registry.d.ts +259 -0
- package/dist/api-registry.d.ts.map +1 -0
- package/dist/api-registry.js +599 -0
- package/dist/api-registry.test.d.ts +2 -0
- package/dist/api-registry.test.d.ts.map +1 -0
- package/dist/api-registry.test.js +957 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +35 -11
- package/dist/plugin-loader.d.ts +3 -2
- package/dist/plugin-loader.d.ts.map +1 -1
- package/dist/plugin-loader.js +13 -11
- package/dist/qa/adapter.d.ts +14 -0
- package/dist/qa/adapter.d.ts.map +1 -0
- package/dist/qa/adapter.js +1 -0
- package/dist/qa/http-adapter.d.ts +16 -0
- package/dist/qa/http-adapter.d.ts.map +1 -0
- package/dist/qa/http-adapter.js +107 -0
- package/dist/qa/index.d.ts +4 -0
- package/dist/qa/index.d.ts.map +1 -0
- package/dist/qa/index.js +3 -0
- package/dist/qa/runner.d.ts +27 -0
- package/dist/qa/runner.d.ts.map +1 -0
- package/dist/qa/runner.js +157 -0
- package/dist/security/index.d.ts +14 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +13 -0
- package/dist/security/plugin-config-validator.d.ts +79 -0
- package/dist/security/plugin-config-validator.d.ts.map +1 -0
- package/dist/security/plugin-config-validator.js +166 -0
- package/dist/security/plugin-config-validator.test.d.ts +2 -0
- package/dist/security/plugin-config-validator.test.d.ts.map +1 -0
- package/dist/security/plugin-config-validator.test.js +223 -0
- package/dist/security/plugin-permission-enforcer.d.ts +154 -0
- package/dist/security/plugin-permission-enforcer.d.ts.map +1 -0
- package/dist/security/plugin-permission-enforcer.js +323 -0
- package/dist/security/plugin-permission-enforcer.test.d.ts +2 -0
- package/dist/security/plugin-permission-enforcer.test.d.ts.map +1 -0
- package/dist/security/plugin-permission-enforcer.test.js +205 -0
- package/dist/security/plugin-signature-verifier.d.ts +96 -0
- package/dist/security/plugin-signature-verifier.d.ts.map +1 -0
- package/dist/security/plugin-signature-verifier.js +250 -0
- package/examples/api-registry-example.ts +557 -0
- package/package.json +2 -2
- package/src/api-registry-plugin.test.ts +391 -0
- package/src/api-registry-plugin.ts +86 -0
- package/src/api-registry.test.ts +1089 -0
- package/src/api-registry.ts +736 -0
- package/src/index.ts +6 -0
- package/src/logger.ts +36 -11
- package/src/plugin-loader.ts +17 -13
- package/src/qa/adapter.ts +14 -0
- package/src/qa/http-adapter.ts +114 -0
- package/src/qa/index.ts +3 -0
- package/src/qa/runner.ts +179 -0
- package/src/security/index.ts +29 -0
- package/src/security/plugin-config-validator.test.ts +276 -0
- package/src/security/plugin-config-validator.ts +191 -0
- package/src/security/plugin-permission-enforcer.test.ts +251 -0
- package/src/security/plugin-permission-enforcer.ts +408 -0
- package/src/security/plugin-signature-verifier.ts +359 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// Conditionally import crypto for Node.js environments
|
|
2
|
+
let cryptoModule = null;
|
|
3
|
+
if (typeof globalThis.window === 'undefined') {
|
|
4
|
+
try {
|
|
5
|
+
// Dynamic import for Node.js crypto module (using eval to avoid bundling issues)
|
|
6
|
+
// @ts-ignore - dynamic require for Node.js
|
|
7
|
+
cryptoModule = eval('require("crypto")');
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
// Crypto module not available (e.g., browser environment)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Plugin Signature Verifier
|
|
15
|
+
*
|
|
16
|
+
* Implements cryptographic verification of plugin signatures to ensure:
|
|
17
|
+
* 1. Plugin integrity - code hasn't been tampered with
|
|
18
|
+
* 2. Publisher authenticity - plugin comes from trusted source
|
|
19
|
+
* 3. Non-repudiation - publisher cannot deny signing
|
|
20
|
+
*
|
|
21
|
+
* Architecture:
|
|
22
|
+
* - Uses Node.js crypto module for signature verification
|
|
23
|
+
* - Supports RSA (RS256) and ECDSA (ES256) algorithms
|
|
24
|
+
* - Verifies against trusted public key registry
|
|
25
|
+
* - Computes hash of plugin code for integrity check
|
|
26
|
+
*
|
|
27
|
+
* Security Model:
|
|
28
|
+
* - Public keys are pre-registered and trusted
|
|
29
|
+
* - Plugin signature is verified before loading
|
|
30
|
+
* - Strict mode rejects unsigned plugins
|
|
31
|
+
* - Development mode allows self-signed plugins
|
|
32
|
+
*/
|
|
33
|
+
export class PluginSignatureVerifier {
|
|
34
|
+
constructor(config, logger) {
|
|
35
|
+
this.config = config;
|
|
36
|
+
this.logger = logger;
|
|
37
|
+
this.validateConfig();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Verify plugin signature
|
|
41
|
+
*
|
|
42
|
+
* @param plugin - Plugin metadata with signature
|
|
43
|
+
* @returns Verification result
|
|
44
|
+
* @throws Error if verification fails in strict mode
|
|
45
|
+
*/
|
|
46
|
+
async verifyPluginSignature(plugin) {
|
|
47
|
+
// Handle unsigned plugins
|
|
48
|
+
if (!plugin.signature) {
|
|
49
|
+
return this.handleUnsignedPlugin(plugin);
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
// 1. Extract publisher ID from plugin name (reverse domain notation)
|
|
53
|
+
const publisherId = this.extractPublisherId(plugin.name);
|
|
54
|
+
// 2. Get trusted public key for publisher
|
|
55
|
+
const publicKey = this.config.trustedPublicKeys.get(publisherId);
|
|
56
|
+
if (!publicKey) {
|
|
57
|
+
const error = `No trusted public key for publisher: ${publisherId}`;
|
|
58
|
+
this.logger.warn(error, { plugin: plugin.name, publisherId });
|
|
59
|
+
if (this.config.strictMode && !this.config.allowSelfSigned) {
|
|
60
|
+
throw new Error(error);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
verified: false,
|
|
64
|
+
error,
|
|
65
|
+
publisherId,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// 3. Compute plugin code hash
|
|
69
|
+
const pluginHash = this.computePluginHash(plugin);
|
|
70
|
+
// 4. Verify signature using crypto module
|
|
71
|
+
const isValid = await this.verifyCryptoSignature(pluginHash, plugin.signature, publicKey);
|
|
72
|
+
if (!isValid) {
|
|
73
|
+
const error = `Signature verification failed for plugin: ${plugin.name}`;
|
|
74
|
+
this.logger.error(error, undefined, { plugin: plugin.name, publisherId });
|
|
75
|
+
throw new Error(error);
|
|
76
|
+
}
|
|
77
|
+
this.logger.info(`✅ Plugin signature verified: ${plugin.name}`, {
|
|
78
|
+
plugin: plugin.name,
|
|
79
|
+
publisherId,
|
|
80
|
+
algorithm: this.config.algorithm,
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
verified: true,
|
|
84
|
+
publisherId,
|
|
85
|
+
algorithm: this.config.algorithm,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
this.logger.error(`Signature verification error: ${plugin.name}`, error);
|
|
90
|
+
if (this.config.strictMode) {
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
verified: false,
|
|
95
|
+
error: error.message,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Register a trusted public key for a publisher
|
|
101
|
+
*/
|
|
102
|
+
registerPublicKey(publisherId, publicKey) {
|
|
103
|
+
this.config.trustedPublicKeys.set(publisherId, publicKey);
|
|
104
|
+
this.logger.info(`Trusted public key registered for: ${publisherId}`);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Remove a trusted public key
|
|
108
|
+
*/
|
|
109
|
+
revokePublicKey(publisherId) {
|
|
110
|
+
this.config.trustedPublicKeys.delete(publisherId);
|
|
111
|
+
this.logger.warn(`Public key revoked for: ${publisherId}`);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get list of trusted publishers
|
|
115
|
+
*/
|
|
116
|
+
getTrustedPublishers() {
|
|
117
|
+
return Array.from(this.config.trustedPublicKeys.keys());
|
|
118
|
+
}
|
|
119
|
+
// Private methods
|
|
120
|
+
handleUnsignedPlugin(plugin) {
|
|
121
|
+
if (this.config.strictMode) {
|
|
122
|
+
const error = `Plugin missing signature (strict mode): ${plugin.name}`;
|
|
123
|
+
this.logger.error(error, undefined, { plugin: plugin.name });
|
|
124
|
+
throw new Error(error);
|
|
125
|
+
}
|
|
126
|
+
this.logger.warn(`⚠️ Plugin not signed: ${plugin.name}`, {
|
|
127
|
+
plugin: plugin.name,
|
|
128
|
+
recommendation: 'Consider signing plugins for production environments',
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
verified: false,
|
|
132
|
+
error: 'Plugin not signed',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
extractPublisherId(pluginName) {
|
|
136
|
+
// Extract publisher from reverse domain notation
|
|
137
|
+
// Example: "com.objectstack.engine.objectql" -> "com.objectstack"
|
|
138
|
+
const parts = pluginName.split('.');
|
|
139
|
+
if (parts.length < 2) {
|
|
140
|
+
throw new Error(`Invalid plugin name format: ${pluginName} (expected reverse domain notation)`);
|
|
141
|
+
}
|
|
142
|
+
// Return first two parts (domain reversed)
|
|
143
|
+
return `${parts[0]}.${parts[1]}`;
|
|
144
|
+
}
|
|
145
|
+
computePluginHash(plugin) {
|
|
146
|
+
// In browser environment, use SubtleCrypto
|
|
147
|
+
if (typeof globalThis.window !== 'undefined') {
|
|
148
|
+
return this.computePluginHashBrowser(plugin);
|
|
149
|
+
}
|
|
150
|
+
// In Node.js environment, use crypto module
|
|
151
|
+
return this.computePluginHashNode(plugin);
|
|
152
|
+
}
|
|
153
|
+
computePluginHashNode(plugin) {
|
|
154
|
+
// Use pre-loaded crypto module
|
|
155
|
+
if (!cryptoModule) {
|
|
156
|
+
this.logger.warn('crypto module not available, using fallback hash');
|
|
157
|
+
return this.computePluginHashFallback(plugin);
|
|
158
|
+
}
|
|
159
|
+
// Compute hash of plugin code
|
|
160
|
+
const pluginCode = this.serializePluginCode(plugin);
|
|
161
|
+
return cryptoModule.createHash('sha256').update(pluginCode).digest('hex');
|
|
162
|
+
}
|
|
163
|
+
computePluginHashBrowser(plugin) {
|
|
164
|
+
// Browser environment - use simple hash for now
|
|
165
|
+
// In production, should use SubtleCrypto for proper cryptographic hash
|
|
166
|
+
this.logger.debug('Using browser hash (SubtleCrypto integration pending)');
|
|
167
|
+
return this.computePluginHashFallback(plugin);
|
|
168
|
+
}
|
|
169
|
+
computePluginHashFallback(plugin) {
|
|
170
|
+
// Simple hash fallback (not cryptographically secure)
|
|
171
|
+
const pluginCode = this.serializePluginCode(plugin);
|
|
172
|
+
let hash = 0;
|
|
173
|
+
for (let i = 0; i < pluginCode.length; i++) {
|
|
174
|
+
const char = pluginCode.charCodeAt(i);
|
|
175
|
+
hash = ((hash << 5) - hash) + char;
|
|
176
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
177
|
+
}
|
|
178
|
+
return hash.toString(16);
|
|
179
|
+
}
|
|
180
|
+
serializePluginCode(plugin) {
|
|
181
|
+
// Serialize plugin code for hashing
|
|
182
|
+
// Include init, start, destroy functions
|
|
183
|
+
const parts = [
|
|
184
|
+
plugin.name,
|
|
185
|
+
plugin.version,
|
|
186
|
+
plugin.init.toString(),
|
|
187
|
+
];
|
|
188
|
+
if (plugin.start) {
|
|
189
|
+
parts.push(plugin.start.toString());
|
|
190
|
+
}
|
|
191
|
+
if (plugin.destroy) {
|
|
192
|
+
parts.push(plugin.destroy.toString());
|
|
193
|
+
}
|
|
194
|
+
return parts.join('|');
|
|
195
|
+
}
|
|
196
|
+
async verifyCryptoSignature(data, signature, publicKey) {
|
|
197
|
+
// In browser environment, use SubtleCrypto
|
|
198
|
+
if (typeof globalThis.window !== 'undefined') {
|
|
199
|
+
return this.verifyCryptoSignatureBrowser(data, signature, publicKey);
|
|
200
|
+
}
|
|
201
|
+
// In Node.js environment, use crypto module
|
|
202
|
+
return this.verifyCryptoSignatureNode(data, signature, publicKey);
|
|
203
|
+
}
|
|
204
|
+
verifyCryptoSignatureNode(data, signature, publicKey) {
|
|
205
|
+
if (!cryptoModule) {
|
|
206
|
+
this.logger.error('Crypto module not available for signature verification');
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
// Create verify object based on algorithm
|
|
211
|
+
if (this.config.algorithm === 'ES256') {
|
|
212
|
+
// ECDSA verification - requires lowercase 'sha256'
|
|
213
|
+
const verify = cryptoModule.createVerify('sha256');
|
|
214
|
+
verify.update(data);
|
|
215
|
+
return verify.verify({
|
|
216
|
+
key: publicKey,
|
|
217
|
+
format: 'pem',
|
|
218
|
+
type: 'spki',
|
|
219
|
+
}, signature, 'base64');
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
// RSA verification (RS256)
|
|
223
|
+
const verify = cryptoModule.createVerify('RSA-SHA256');
|
|
224
|
+
verify.update(data);
|
|
225
|
+
return verify.verify(publicKey, signature, 'base64');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
this.logger.error('Signature verification failed', error);
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async verifyCryptoSignatureBrowser(_data, _signature, _publicKey) {
|
|
234
|
+
// Browser implementation using SubtleCrypto
|
|
235
|
+
// TODO: Implement SubtleCrypto-based verification
|
|
236
|
+
this.logger.warn('Browser signature verification not yet implemented');
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
validateConfig() {
|
|
240
|
+
if (!this.config.trustedPublicKeys || this.config.trustedPublicKeys.size === 0) {
|
|
241
|
+
this.logger.warn('No trusted public keys configured - all signatures will fail');
|
|
242
|
+
}
|
|
243
|
+
if (!this.config.algorithm) {
|
|
244
|
+
throw new Error('Signature algorithm must be specified');
|
|
245
|
+
}
|
|
246
|
+
if (!['RS256', 'ES256'].includes(this.config.algorithm)) {
|
|
247
|
+
throw new Error(`Unsupported algorithm: ${this.config.algorithm}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|