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