@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,557 @@
1
+ /**
2
+ * API Registry Example
3
+ *
4
+ * Demonstrates how to use the API Registry in the ObjectStack kernel
5
+ * to register and discover API endpoints across plugins.
6
+ */
7
+
8
+ import { ObjectKernel, createApiRegistryPlugin, ApiRegistry } from '@objectstack/core';
9
+ import type { Plugin } from '@objectstack/core';
10
+ import type { ApiRegistryEntry } from '@objectstack/spec/api';
11
+
12
+ // Example 1: Basic API Registration
13
+ async function example1_BasicApiRegistration() {
14
+ console.log('\n=== Example 1: Basic API Registration ===\n');
15
+
16
+ const kernel = new ObjectKernel();
17
+
18
+ // Register API Registry plugin with default settings
19
+ kernel.use(createApiRegistryPlugin());
20
+
21
+ // Create a plugin that registers a simple REST API
22
+ const customerPlugin: Plugin = {
23
+ name: 'customer-plugin',
24
+ version: '1.0.0',
25
+ init: async (ctx) => {
26
+ const registry = ctx.getService<ApiRegistry>('api-registry');
27
+
28
+ const customerApi: ApiRegistryEntry = {
29
+ id: 'customer_api',
30
+ name: 'Customer Management API',
31
+ type: 'rest',
32
+ version: 'v1',
33
+ basePath: '/api/v1/customers',
34
+ description: 'CRUD operations for customer records',
35
+ endpoints: [
36
+ {
37
+ id: 'list_customers',
38
+ method: 'GET',
39
+ path: '/api/v1/customers',
40
+ summary: 'List all customers',
41
+ parameters: [
42
+ {
43
+ name: 'limit',
44
+ in: 'query',
45
+ schema: { type: 'number' },
46
+ description: 'Maximum number of results',
47
+ },
48
+ {
49
+ name: 'offset',
50
+ in: 'query',
51
+ schema: { type: 'number' },
52
+ description: 'Offset for pagination',
53
+ },
54
+ ],
55
+ responses: [
56
+ {
57
+ statusCode: 200,
58
+ description: 'Customers retrieved successfully',
59
+ schema: {
60
+ type: 'array',
61
+ items: {
62
+ type: 'object',
63
+ properties: {
64
+ id: { type: 'string' },
65
+ name: { type: 'string' },
66
+ email: { type: 'string' },
67
+ },
68
+ },
69
+ },
70
+ },
71
+ ],
72
+ },
73
+ {
74
+ id: 'get_customer',
75
+ method: 'GET',
76
+ path: '/api/v1/customers/:id',
77
+ summary: 'Get customer by ID',
78
+ requiredPermissions: ['customer.read'], // RBAC integration
79
+ parameters: [
80
+ {
81
+ name: 'id',
82
+ in: 'path',
83
+ required: true,
84
+ schema: { type: 'string', format: 'uuid' },
85
+ },
86
+ ],
87
+ responses: [
88
+ {
89
+ statusCode: 200,
90
+ description: 'Customer found',
91
+ },
92
+ {
93
+ statusCode: 404,
94
+ description: 'Customer not found',
95
+ },
96
+ ],
97
+ },
98
+ {
99
+ id: 'create_customer',
100
+ method: 'POST',
101
+ path: '/api/v1/customers',
102
+ summary: 'Create new customer',
103
+ requiredPermissions: ['customer.create'],
104
+ requestBody: {
105
+ required: true,
106
+ contentType: 'application/json',
107
+ schema: {
108
+ type: 'object',
109
+ properties: {
110
+ name: { type: 'string' },
111
+ email: { type: 'string', format: 'email' },
112
+ },
113
+ },
114
+ },
115
+ responses: [
116
+ {
117
+ statusCode: 201,
118
+ description: 'Customer created',
119
+ },
120
+ ],
121
+ },
122
+ ],
123
+ metadata: {
124
+ status: 'active',
125
+ tags: ['customer', 'crm', 'data'],
126
+ owner: 'sales_team',
127
+ },
128
+ };
129
+
130
+ registry.registerApi(customerApi);
131
+ ctx.logger.info('Customer API registered', {
132
+ endpointCount: customerApi.endpoints.length,
133
+ });
134
+ },
135
+ };
136
+
137
+ kernel.use(customerPlugin);
138
+ await kernel.bootstrap();
139
+
140
+ // Access the registry
141
+ const registry = kernel.getService<ApiRegistry>('api-registry');
142
+ const snapshot = registry.getRegistry();
143
+
144
+ console.log(`Total APIs: ${snapshot.totalApis}`);
145
+ console.log(`Total Endpoints: ${snapshot.totalEndpoints}`);
146
+ console.log('\nRegistered APIs:');
147
+ snapshot.apis.forEach((api) => {
148
+ console.log(` - ${api.name} (${api.type}) - ${api.endpoints.length} endpoints`);
149
+ });
150
+
151
+ await kernel.shutdown();
152
+ }
153
+
154
+ // Example 2: Multi-Plugin API Discovery
155
+ async function example2_MultiPluginDiscovery() {
156
+ console.log('\n=== Example 2: Multi-Plugin API Discovery ===\n');
157
+
158
+ const kernel = new ObjectKernel();
159
+ kernel.use(createApiRegistryPlugin());
160
+
161
+ // Data Plugin - REST APIs
162
+ const dataPlugin: Plugin = {
163
+ name: 'data-plugin',
164
+ init: async (ctx) => {
165
+ const registry = ctx.getService<ApiRegistry>('api-registry');
166
+
167
+ registry.registerApi({
168
+ id: 'customer_api',
169
+ name: 'Customer API',
170
+ type: 'rest',
171
+ version: 'v1',
172
+ basePath: '/api/v1/customers',
173
+ endpoints: [
174
+ {
175
+ id: 'get_customers',
176
+ method: 'GET',
177
+ path: '/api/v1/customers',
178
+ responses: [],
179
+ },
180
+ ],
181
+ metadata: {
182
+ status: 'active',
183
+ tags: ['data', 'crm'],
184
+ },
185
+ });
186
+
187
+ registry.registerApi({
188
+ id: 'product_api',
189
+ name: 'Product API',
190
+ type: 'rest',
191
+ version: 'v1',
192
+ basePath: '/api/v1/products',
193
+ endpoints: [
194
+ {
195
+ id: 'get_products',
196
+ method: 'GET',
197
+ path: '/api/v1/products',
198
+ responses: [],
199
+ },
200
+ ],
201
+ metadata: {
202
+ status: 'active',
203
+ tags: ['data', 'inventory'],
204
+ },
205
+ });
206
+ },
207
+ };
208
+
209
+ // GraphQL Plugin
210
+ const graphqlPlugin: Plugin = {
211
+ name: 'graphql-plugin',
212
+ init: async (ctx) => {
213
+ const registry = ctx.getService<ApiRegistry>('api-registry');
214
+
215
+ registry.registerApi({
216
+ id: 'graphql_api',
217
+ name: 'GraphQL API',
218
+ type: 'graphql',
219
+ version: 'v1',
220
+ basePath: '/graphql',
221
+ endpoints: [
222
+ {
223
+ id: 'query',
224
+ path: '/graphql',
225
+ summary: 'GraphQL Query Endpoint',
226
+ responses: [],
227
+ },
228
+ ],
229
+ metadata: {
230
+ status: 'active',
231
+ tags: ['query', 'flexible'],
232
+ },
233
+ });
234
+ },
235
+ };
236
+
237
+ // Analytics Plugin - Beta API
238
+ const analyticsPlugin: Plugin = {
239
+ name: 'analytics-plugin',
240
+ init: async (ctx) => {
241
+ const registry = ctx.getService<ApiRegistry>('api-registry');
242
+
243
+ registry.registerApi({
244
+ id: 'analytics_api',
245
+ name: 'Analytics API',
246
+ type: 'rest',
247
+ version: 'v1',
248
+ basePath: '/api/v1/analytics',
249
+ endpoints: [
250
+ {
251
+ id: 'get_reports',
252
+ method: 'GET',
253
+ path: '/api/v1/analytics/reports',
254
+ responses: [],
255
+ },
256
+ ],
257
+ metadata: {
258
+ status: 'beta',
259
+ tags: ['analytics', 'reporting'],
260
+ },
261
+ });
262
+ },
263
+ };
264
+
265
+ kernel.use(dataPlugin);
266
+ kernel.use(graphqlPlugin);
267
+ kernel.use(analyticsPlugin);
268
+ await kernel.bootstrap();
269
+
270
+ const registry = kernel.getService<ApiRegistry>('api-registry');
271
+
272
+ // Discovery 1: Find all REST APIs
273
+ console.log('All REST APIs:');
274
+ const restApis = registry.findApis({ type: 'rest' });
275
+ restApis.apis.forEach((api) => console.log(` - ${api.name}`));
276
+
277
+ // Discovery 2: Find active APIs
278
+ console.log('\nActive APIs:');
279
+ const activeApis = registry.findApis({ status: 'active' });
280
+ console.log(` Total: ${activeApis.total}`);
281
+
282
+ // Discovery 3: Find data-related APIs
283
+ console.log('\nData-related APIs:');
284
+ const dataApis = registry.findApis({ tags: ['data'] });
285
+ dataApis.apis.forEach((api) => console.log(` - ${api.name}`));
286
+
287
+ // Discovery 4: Search by name
288
+ console.log('\nSearch for "analytics":');
289
+ const analyticsApis = registry.findApis({ search: 'analytics' });
290
+ analyticsApis.apis.forEach((api) => console.log(` - ${api.name} (${api.metadata?.status})`));
291
+
292
+ await kernel.shutdown();
293
+ }
294
+
295
+ // Example 3: Route Conflict Resolution
296
+ async function example3_ConflictResolution() {
297
+ console.log('\n=== Example 3: Route Conflict Resolution ===\n');
298
+
299
+ const kernel = new ObjectKernel();
300
+
301
+ // Use priority-based conflict resolution
302
+ kernel.use(
303
+ createApiRegistryPlugin({
304
+ conflictResolution: 'priority',
305
+ })
306
+ );
307
+
308
+ // Core Plugin - High priority
309
+ const corePlugin: Plugin = {
310
+ name: 'core-plugin',
311
+ init: async (ctx) => {
312
+ const registry = ctx.getService<ApiRegistry>('api-registry');
313
+
314
+ registry.registerApi({
315
+ id: 'core_data_api',
316
+ name: 'Core Data API',
317
+ type: 'rest',
318
+ version: 'v1',
319
+ basePath: '/api',
320
+ endpoints: [
321
+ {
322
+ id: 'core_data',
323
+ method: 'GET',
324
+ path: '/api/data/:object',
325
+ priority: 900, // High priority
326
+ summary: 'Core data endpoint (generic)',
327
+ responses: [],
328
+ },
329
+ ],
330
+ });
331
+
332
+ ctx.logger.info('Core API registered with priority 900');
333
+ },
334
+ };
335
+
336
+ // Custom Plugin - Medium priority
337
+ const customPlugin: Plugin = {
338
+ name: 'custom-plugin',
339
+ init: async (ctx) => {
340
+ const registry = ctx.getService<ApiRegistry>('api-registry');
341
+
342
+ registry.registerApi({
343
+ id: 'custom_data_api',
344
+ name: 'Custom Data API',
345
+ type: 'rest',
346
+ version: 'v1',
347
+ basePath: '/api',
348
+ endpoints: [
349
+ {
350
+ id: 'custom_data',
351
+ method: 'GET',
352
+ path: '/api/data/:object',
353
+ priority: 300, // Lower priority
354
+ summary: 'Custom data endpoint (specialized)',
355
+ responses: [],
356
+ },
357
+ ],
358
+ });
359
+
360
+ ctx.logger.info('Custom API registered with priority 300');
361
+ },
362
+ };
363
+
364
+ kernel.use(corePlugin);
365
+ kernel.use(customPlugin);
366
+ await kernel.bootstrap();
367
+
368
+ const registry = kernel.getService<ApiRegistry>('api-registry');
369
+
370
+ // Check which endpoint won
371
+ const winner = registry.findEndpointByRoute('GET', '/api/data/:object');
372
+ console.log('\nConflict Resolution Result:');
373
+ console.log(` Route: GET /api/data/:object`);
374
+ console.log(` Winner: ${winner?.api.name}`);
375
+ console.log(` Endpoint: ${winner?.endpoint.summary}`);
376
+ console.log(` Priority: ${winner?.endpoint.priority}`);
377
+
378
+ await kernel.shutdown();
379
+ }
380
+
381
+ // Example 4: Plugin-specific APIs with Custom Protocol
382
+ async function example4_CustomProtocol() {
383
+ console.log('\n=== Example 4: Custom Protocol Support ===\n');
384
+
385
+ const kernel = new ObjectKernel();
386
+ kernel.use(createApiRegistryPlugin());
387
+
388
+ const websocketPlugin: Plugin = {
389
+ name: 'websocket-plugin',
390
+ init: async (ctx) => {
391
+ const registry = ctx.getService<ApiRegistry>('api-registry');
392
+
393
+ registry.registerApi({
394
+ id: 'realtime_api',
395
+ name: 'Real-time WebSocket API',
396
+ type: 'websocket',
397
+ version: 'v1',
398
+ basePath: '/ws',
399
+ endpoints: [
400
+ {
401
+ id: 'customer_updates',
402
+ path: '/ws/customers',
403
+ summary: 'Customer update notifications',
404
+ protocolConfig: {
405
+ subProtocol: 'websocket',
406
+ eventName: 'customer.updated',
407
+ direction: 'server-to-client',
408
+ },
409
+ responses: [],
410
+ },
411
+ {
412
+ id: 'order_updates',
413
+ path: '/ws/orders',
414
+ summary: 'Order update notifications',
415
+ protocolConfig: {
416
+ subProtocol: 'websocket',
417
+ eventName: 'order.updated',
418
+ direction: 'bidirectional',
419
+ },
420
+ responses: [],
421
+ },
422
+ ],
423
+ metadata: {
424
+ status: 'active',
425
+ tags: ['realtime', 'websocket'],
426
+ pluginSource: 'websocket-plugin',
427
+ },
428
+ });
429
+ },
430
+ };
431
+
432
+ kernel.use(websocketPlugin);
433
+ await kernel.bootstrap();
434
+
435
+ const registry = kernel.getService<ApiRegistry>('api-registry');
436
+ const wsApis = registry.findApis({ type: 'websocket' });
437
+
438
+ console.log('WebSocket APIs:');
439
+ wsApis.apis.forEach((api) => {
440
+ console.log(`\n${api.name}:`);
441
+ api.endpoints.forEach((endpoint) => {
442
+ console.log(` - ${endpoint.summary}`);
443
+ console.log(` Event: ${endpoint.protocolConfig?.eventName}`);
444
+ console.log(` Direction: ${endpoint.protocolConfig?.direction}`);
445
+ });
446
+ });
447
+
448
+ await kernel.shutdown();
449
+ }
450
+
451
+ // Example 5: Dynamic Schema Linking with ObjectQL
452
+ async function example5_DynamicSchemas() {
453
+ console.log('\n=== Example 5: Dynamic Schema Linking ===\n');
454
+
455
+ const kernel = new ObjectKernel();
456
+ kernel.use(createApiRegistryPlugin());
457
+
458
+ const dynamicPlugin: Plugin = {
459
+ name: 'dynamic-plugin',
460
+ init: async (ctx) => {
461
+ const registry = ctx.getService<ApiRegistry>('api-registry');
462
+
463
+ registry.registerApi({
464
+ id: 'dynamic_customer_api',
465
+ name: 'Dynamic Customer API',
466
+ type: 'rest',
467
+ version: 'v1',
468
+ basePath: '/api/v1/customers',
469
+ endpoints: [
470
+ {
471
+ id: 'get_customer_dynamic',
472
+ method: 'GET',
473
+ path: '/api/v1/customers/:id',
474
+ summary: 'Get customer (with dynamic schema)',
475
+ responses: [
476
+ {
477
+ statusCode: 200,
478
+ description: 'Customer retrieved',
479
+ // Dynamic schema linked to ObjectQL
480
+ //
481
+ // IMPORTANT: The API Registry stores this ObjectQL reference as-is.
482
+ // The actual schema resolution (expanding the reference into a full JSON Schema)
483
+ // is performed by downstream tools:
484
+ // - API Gateway: For runtime request/response validation
485
+ // - OpenAPI/Swagger Generator: For API documentation generation
486
+ // - GraphQL Schema Builder: For GraphQL type generation
487
+ //
488
+ // The Registry's responsibility is to STORE the reference metadata,
489
+ // not to resolve or transform it.
490
+ schema: {
491
+ $ref: {
492
+ objectId: 'customer', // References ObjectQL object
493
+ excludeFields: ['password_hash', 'internal_notes'], // Exclude sensitive fields
494
+ includeRelated: ['account', 'primary_contact'], // Include related objects
495
+ },
496
+ },
497
+ },
498
+ ],
499
+ },
500
+ ],
501
+ });
502
+
503
+ ctx.logger.info('Dynamic Customer API registered with ObjectQL schema references');
504
+ },
505
+ };
506
+
507
+ kernel.use(dynamicPlugin);
508
+ await kernel.bootstrap();
509
+
510
+ const registry = kernel.getService<ApiRegistry>('api-registry');
511
+ const endpoint = registry.getEndpoint('dynamic_customer_api', 'get_customer_dynamic');
512
+
513
+ console.log('Dynamic Endpoint:');
514
+ console.log(` Path: ${endpoint?.path}`);
515
+ console.log(` Summary: ${endpoint?.summary}`);
516
+
517
+ if (endpoint?.responses?.[0]?.schema && '$ref' in endpoint.responses[0].schema) {
518
+ const ref = endpoint.responses[0].schema.$ref;
519
+ console.log('\n Schema Reference (stored as metadata):');
520
+ console.log(` Object: ${ref.objectId}`);
521
+ console.log(` Excluded Fields: ${ref.excludeFields?.join(', ')}`);
522
+ console.log(` Included Related: ${ref.includeRelated?.join(', ')}`);
523
+ console.log('\n ℹ️ Note: Schema resolution is handled by gateway/documentation tools,');
524
+ console.log(' not by the API Registry itself.');
525
+ }
526
+
527
+ await kernel.shutdown();
528
+ }
529
+
530
+ // Run all examples
531
+ async function main() {
532
+ try {
533
+ await example1_BasicApiRegistration();
534
+ await example2_MultiPluginDiscovery();
535
+ await example3_ConflictResolution();
536
+ await example4_CustomProtocol();
537
+ await example5_DynamicSchemas();
538
+
539
+ console.log('\n=== All examples completed successfully! ===\n');
540
+ } catch (error) {
541
+ console.error('Example failed:', error);
542
+ process.exit(1);
543
+ }
544
+ }
545
+
546
+ // Only run if this file is executed directly
547
+ if (import.meta.url === `file://${process.argv[1]}`) {
548
+ main();
549
+ }
550
+
551
+ export {
552
+ example1_BasicApiRegistration,
553
+ example2_MultiPluginDiscovery,
554
+ example3_ConflictResolution,
555
+ example4_CustomProtocol,
556
+ example5_DynamicSchemas,
557
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/core",
3
- "version": "0.8.2",
3
+ "version": "0.9.1",
4
4
  "description": "Microkernel Core for ObjectStack",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -14,7 +14,7 @@
14
14
  "pino": "^8.17.0",
15
15
  "pino-pretty": "^10.3.0",
16
16
  "zod": "^4.3.6",
17
- "@objectstack/spec": "0.8.2"
17
+ "@objectstack/spec": "0.9.1"
18
18
  },
19
19
  "peerDependencies": {
20
20
  "pino": "^8.0.0"