@objectstack/runtime 4.0.2 → 4.0.4

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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { ObjectKernel, getEnv, resolveLocale } from '@objectstack/core';
4
4
  import { CoreServiceName } from '@objectstack/spec/system';
5
+ import { pluralToSingular } from '@objectstack/spec/shared';
5
6
 
6
7
  /** Browser-safe UUID generator — prefers Web Crypto, falls back to RFC 4122 v4 */
7
8
  function randomUUID(): string {
@@ -39,7 +40,7 @@ export interface HttpDispatcherResult {
39
40
  * ```
40
41
  */
41
42
  export class HttpDispatcher {
42
- private kernel: any; // Casting to any to access dynamic props like broker, services, graphql
43
+ private kernel: any; // Casting to any to access dynamic props like services, graphql
43
44
 
44
45
  constructor(kernel: ObjectKernel) {
45
46
  this.kernel = kernel;
@@ -78,11 +79,82 @@ export class HttpDispatcher {
78
79
  };
79
80
  }
80
81
 
81
- private ensureBroker() {
82
- if (!this.kernel.broker) {
83
- throw { statusCode: 500, message: 'Kernel Broker not available' };
82
+ /**
83
+ * Direct data service dispatch — replaces broker.call('data.*').
84
+ * Tries protocol service first (supports expand/populate), falls back to ObjectQL.
85
+ */
86
+ private async callData(action: string, params: any): Promise<any> {
87
+ const protocol = await this.resolveService('protocol');
88
+ const qlService = await this.getObjectQLService();
89
+ const ql = qlService ?? await this.resolveService('objectql');
90
+
91
+ if (action === 'create') {
92
+ if (ql) {
93
+ const res = await ql.insert(params.object, params.data);
94
+ const record = { ...params.data, ...res };
95
+ return { object: params.object, id: record.id, record };
96
+ }
97
+ throw { statusCode: 503, message: 'Data service not available' };
98
+ }
99
+
100
+ if (action === 'get') {
101
+ if (protocol && typeof protocol.getData === 'function') {
102
+ return await protocol.getData({ object: params.object, id: params.id, expand: params.expand, select: params.select });
103
+ }
104
+ if (ql) {
105
+ let all = await ql.find(params.object);
106
+ if (!all) all = [];
107
+ const match = all.find((i: any) => i.id === params.id);
108
+ return match ? { object: params.object, id: params.id, record: match } : null;
109
+ }
110
+ throw { statusCode: 503, message: 'Data service not available' };
84
111
  }
85
- return this.kernel.broker;
112
+
113
+ if (action === 'update') {
114
+ if (ql && params.id) {
115
+ let all = await ql.find(params.object);
116
+ if (all && (all as any).value) all = (all as any).value;
117
+ if (!all) all = [];
118
+ const existing = all.find((i: any) => i.id === params.id);
119
+ if (!existing) throw new Error('[ObjectStack] Not Found');
120
+ await ql.update(params.object, params.data, { where: { id: params.id } });
121
+ return { object: params.object, id: params.id, record: { ...existing, ...params.data } };
122
+ }
123
+ throw { statusCode: 503, message: 'Data service not available' };
124
+ }
125
+
126
+ if (action === 'delete') {
127
+ if (ql) {
128
+ await ql.delete(params.object, { where: { id: params.id } });
129
+ return { object: params.object, id: params.id, deleted: true };
130
+ }
131
+ throw { statusCode: 503, message: 'Data service not available' };
132
+ }
133
+
134
+ if (action === 'query' || action === 'find') {
135
+ if (protocol && typeof protocol.findData === 'function') {
136
+ // Build query: use explicit params.query if provided, otherwise extract query fields from params
137
+ const query = params.query || (() => {
138
+ const { object, ...rest } = params;
139
+ return rest;
140
+ })();
141
+ return await protocol.findData({ object: params.object, query });
142
+ }
143
+ if (ql) {
144
+ let all = await ql.find(params.object);
145
+ if (!Array.isArray(all) && all && (all as any).value) all = (all as any).value;
146
+ if (!all) all = [];
147
+ return { object: params.object, records: all, total: all.length };
148
+ }
149
+ throw { statusCode: 503, message: 'Data service not available' };
150
+ }
151
+
152
+ if (action === 'batch') {
153
+ // Batch operations — not yet supported via direct service dispatch
154
+ return { object: params.object, results: [] };
155
+ }
156
+
157
+ throw { statusCode: 400, message: `Unknown data action: ${action}` };
86
158
  }
87
159
 
88
160
  /**
@@ -248,24 +320,8 @@ export class HttpDispatcher {
248
320
  return { handled: true, result: response };
249
321
  }
250
322
 
251
- // 2. Legacy Login via broker
323
+ // 2. Mock fallback for MSW/test environments when no auth service is registered
252
324
  const normalizedPath = path.replace(/^\/+/, '');
253
- if (normalizedPath === 'login' && method.toUpperCase() === 'POST') {
254
- try {
255
- const broker = this.ensureBroker();
256
- const data = await broker.call('auth.login', body, { request: context.request });
257
- return { handled: true, response: { status: 200, body: data } };
258
- } catch (error: any) {
259
- // Only fall through to mock when the broker is truly unavailable
260
- // (ensureBroker throws statusCode 500 when kernel.broker is null)
261
- const statusCode = error?.statusCode ?? error?.status;
262
- if (statusCode !== 500 || !error?.message?.includes('Broker not available')) {
263
- throw error;
264
- }
265
- }
266
- }
267
-
268
- // 3. Mock fallback for MSW/test environments when no auth service is registered
269
325
  return this.mockAuthFallback(normalizedPath, method, body);
270
326
  }
271
327
 
@@ -332,30 +388,29 @@ export class HttpDispatcher {
332
388
  * Standard: /metadata/:type/:name
333
389
  * Fallback for backward compat: /metadata (all objects), /metadata/:objectName (get object)
334
390
  */
335
- async handleMetadata(path: string, context: HttpProtocolContext, method?: string, body?: any, query?: any): Promise<HttpDispatcherResult> {
336
- // Broker is used as a fallback — not required upfront.
337
- // This allows metadata to be served when only the protocol service
338
- // or ObjectQL service is available (e.g. lightweight / serverless setups).
339
- const broker = this.kernel.broker ?? null;
391
+ async handleMetadata(path: string, _context: HttpProtocolContext, method?: string, body?: any, query?: any): Promise<HttpDispatcherResult> {
340
392
  const parts = path.replace(/^\/+/, '').split('/').filter(Boolean);
341
393
 
342
394
  // GET /metadata/types
343
395
  if (parts[0] === 'types') {
344
- // Try protocol service for dynamic types
396
+ // PRIORITY 1: Try MetadataService directly (includes both typeRegistry with agent/tool AND runtime-registered types)
397
+ const metadataService = await this.resolveService('metadata');
398
+
399
+ if (metadataService && typeof (metadataService as any).getRegisteredTypes === 'function') {
400
+ try {
401
+ const types = await (metadataService as any).getRegisteredTypes();
402
+ return { handled: true, response: this.success({ types }) };
403
+ } catch (e: any) {
404
+ // Log error but continue to fallbacks
405
+ console.warn('[HttpDispatcher] MetadataService.getRegisteredTypes() failed:', e.message);
406
+ }
407
+ }
408
+ // PRIORITY 2: Try protocol service (returns SchemaRegistry types only - missing agent/tool)
345
409
  const protocol = await this.resolveService('protocol');
346
410
  if (protocol && typeof protocol.getMetaTypes === 'function') {
347
411
  const result = await protocol.getMetaTypes({});
348
412
  return { handled: true, response: this.success(result) };
349
413
  }
350
- // Fallback: ask broker for registered types
351
- if (broker) {
352
- try {
353
- const data = await broker.call('metadata.types', {}, { request: context.request });
354
- return { handled: true, response: this.success(data) };
355
- } catch {
356
- // fall through to hardcoded defaults
357
- }
358
- }
359
414
  // Last resort: hardcoded defaults
360
415
  return { handled: true, response: this.success({ types: ['object', 'app', 'plugin'] }) };
361
416
  }
@@ -369,14 +424,13 @@ export class HttpDispatcher {
369
424
  if (data === undefined) return { handled: true, response: this.error('Not found', 404) };
370
425
  return { handled: true, response: this.success(data) };
371
426
  }
372
- // Broker fallback
373
- if (broker) {
427
+ // Fallback — try MetadataService via resolveService
428
+ const metaSvc = await this.resolveService('metadata');
429
+ if (metaSvc && typeof (metaSvc as any).getPublished === 'function') {
374
430
  try {
375
- const data = await broker.call('metadata.getPublished', { type, name }, { request: context.request });
376
- return { handled: true, response: this.success(data) };
377
- } catch (e: any) {
378
- return { handled: true, response: this.error(e.message, 404) };
379
- }
431
+ const fallbackData = await (metaSvc as any).getPublished(type, name);
432
+ if (fallbackData !== undefined) return { handled: true, response: this.success(fallbackData) };
433
+ } catch { /* fall through */ }
380
434
  }
381
435
  return { handled: true, response: this.error('Not found', 404) };
382
436
  }
@@ -401,13 +455,14 @@ export class HttpDispatcher {
401
455
  }
402
456
  }
403
457
 
404
- // Fallback to broker if protocol not available (legacy)
405
- if (broker) {
458
+ // Fallback: try MetadataService directly
459
+ const metaSvc = await this.resolveService('metadata');
460
+ if (metaSvc && typeof (metaSvc as any).saveItem === 'function') {
406
461
  try {
407
- const data = await broker.call('metadata.saveItem', { type, name, item: body }, { request: context.request });
408
- return { handled: true, response: this.success(data) };
462
+ const data = await (metaSvc as any).saveItem(type, name, body);
463
+ return { handled: true, response: this.success(data) };
409
464
  } catch (e: any) {
410
- return { handled: true, response: this.error(e.message || 'Save not supported', 501) };
465
+ return { handled: true, response: this.error(e.message || 'Save not supported', 501) };
411
466
  }
412
467
  }
413
468
  return { handled: true, response: this.error('Save not supported', 501) };
@@ -416,11 +471,7 @@ export class HttpDispatcher {
416
471
  try {
417
472
  // Try specific calls based on type
418
473
  if (type === 'objects' || type === 'object') {
419
- if (broker) {
420
- const data = await broker.call('metadata.getObject', { objectName: name }, { request: context.request });
421
- return { handled: true, response: this.success(data) };
422
- }
423
- // Try ObjectQL service directly when broker is unavailable
474
+ // Try ObjectQL service directly
424
475
  const qlService = await this.getObjectQLService();
425
476
  if (qlService?.registry) {
426
477
  const data = qlService.registry.getObject(name);
@@ -429,9 +480,8 @@ export class HttpDispatcher {
429
480
  return { handled: true, response: this.error('Not found', 404) };
430
481
  }
431
482
 
432
- // If type is singular (e.g. 'app'), use it directly
433
- // If plural (e.g. 'apps'), slice it
434
- const singularType = type.endsWith('s') ? type.slice(0, -1) : type;
483
+ // Normalize plural URL paths to singular registry type names
484
+ const singularType = pluralToSingular(type);
435
485
 
436
486
  // Try Protocol Service First (Preferred)
437
487
  const protocol = await this.resolveService('protocol');
@@ -441,15 +491,16 @@ export class HttpDispatcher {
441
491
  return { handled: true, response: this.success(data) };
442
492
  } catch (e: any) {
443
493
  // Protocol might throw if not found or not supported
444
- // Fallback to broker?
445
494
  }
446
495
  }
447
496
 
448
- // Generic call for other types if supported via Broker (Legacy)
449
- if (broker) {
450
- const method = `metadata.get${this.capitalize(singularType)}`;
451
- const data = await broker.call(method, { name }, { request: context.request });
452
- return { handled: true, response: this.success(data) };
497
+ // Try MetadataService for runtime-registered types
498
+ const metaSvc = await this.resolveService('metadata');
499
+ if (metaSvc && typeof (metaSvc as any).getItem === 'function') {
500
+ try {
501
+ const data = await (metaSvc as any).getItem(singularType, name);
502
+ if (data) return { handled: true, response: this.success(data) };
503
+ } catch { /* not found */ }
453
504
  }
454
505
  return { handled: true, response: this.error('Not found', 404) };
455
506
  } catch (e: any) {
@@ -464,7 +515,7 @@ export class HttpDispatcher {
464
515
  const typeOrName = parts[0];
465
516
  // Extract optional package filter from query string
466
517
  const packageId = query?.package || undefined;
467
-
518
+
468
519
  // Try protocol service first for any type
469
520
  const protocol = await this.resolveService('protocol');
470
521
  if (protocol && typeof protocol.getMetaItems === 'function') {
@@ -479,31 +530,28 @@ export class HttpDispatcher {
479
530
  }
480
531
  }
481
532
 
482
- // Try broker for the type
483
- if (broker) {
533
+ // Try MetadataService directly for runtime-registered metadata (agents, tools, etc.)
534
+ const metadataService = await this.getService(CoreServiceName.enum.metadata);
535
+ if (metadataService && typeof (metadataService as any).list === 'function') {
484
536
  try {
485
- if (typeOrName === 'objects') {
486
- const data = await broker.call('metadata.objects', { packageId }, { request: context.request });
487
- return { handled: true, response: this.success(data) };
537
+ let items = await (metadataService as any).list(typeOrName);
538
+ // Respect package filter: MetadataService.list() returns ALL items,
539
+ // so filter by _packageId when a specific package is requested.
540
+ if (packageId && items && items.length > 0) {
541
+ items = items.filter((item: any) => item?._packageId === packageId);
488
542
  }
489
- const data = await broker.call(`metadata.${typeOrName}`, { packageId }, { request: context.request });
490
- if (data !== null && data !== undefined) {
491
- return { handled: true, response: this.success(data) };
543
+ if (items && items.length > 0) {
544
+ return { handled: true, response: this.success({ type: typeOrName, items }) };
492
545
  }
493
- } catch {
494
- // Broker doesn't support this action, fall through
495
- }
496
-
497
- // Legacy: /metadata/:objectName (treat as single object lookup)
498
- try {
499
- const data = await broker.call('metadata.getObject', { objectName: typeOrName }, { request: context.request });
500
- return { handled: true, response: this.success(data) };
501
546
  } catch (e: any) {
502
- return { handled: true, response: this.error(e.message, 404) };
547
+ // MetadataService doesn't know this type or failed, continue to other fallbacks
548
+ // Sanitize typeOrName to prevent log injection (CodeQL warning)
549
+ const sanitizedType = String(typeOrName).replace(/[\r\n\t]/g, '');
550
+ console.debug(`[HttpDispatcher] MetadataService.list() failed for type:`, sanitizedType, 'error:', e.message);
503
551
  }
504
552
  }
505
553
 
506
- // No broker — try ObjectQL registry directly for object lookups
554
+ // Try ObjectQL registry directly for object/type lookups
507
555
  const qlService = await this.getObjectQLService();
508
556
  if (qlService?.registry) {
509
557
  if (typeOrName === 'objects') {
@@ -524,21 +572,20 @@ export class HttpDispatcher {
524
572
 
525
573
  // GET /metadata — return available metadata types
526
574
  if (parts.length === 0) {
575
+ // Try MetadataService for registered types
576
+ const metadataService = await this.resolveService('metadata');
577
+ if (metadataService && typeof (metadataService as any).getRegisteredTypes === 'function') {
578
+ try {
579
+ const types = await (metadataService as any).getRegisteredTypes();
580
+ return { handled: true, response: this.success({ types }) };
581
+ } catch { /* fall through */ }
582
+ }
527
583
  // Try protocol service for dynamic types
528
584
  const protocol = await this.resolveService('protocol');
529
585
  if (protocol && typeof protocol.getMetaTypes === 'function') {
530
586
  const result = await protocol.getMetaTypes({});
531
587
  return { handled: true, response: this.success(result) };
532
588
  }
533
- // Fallback: ask broker for registered types
534
- if (broker) {
535
- try {
536
- const data = await broker.call('metadata.types', {}, { request: context.request });
537
- return { handled: true, response: this.success(data) };
538
- } catch {
539
- // fall through to hardcoded defaults
540
- }
541
- }
542
589
  return { handled: true, response: this.success({ types: ['object', 'app', 'plugin'] }) };
543
590
  }
544
591
 
@@ -549,8 +596,7 @@ export class HttpDispatcher {
549
596
  * Handles Data requests
550
597
  * path: sub-path after /data/ (e.g. "contacts", "contacts/123", "contacts/query")
551
598
  */
552
- async handleData(path: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
553
- const broker = this.ensureBroker();
599
+ async handleData(path: string, method: string, body: any, query: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult> {
554
600
  const parts = path.replace(/^\/+/, '').split('/');
555
601
  const objectName = parts[0];
556
602
 
@@ -566,14 +612,14 @@ export class HttpDispatcher {
566
612
 
567
613
  // POST /data/:object/query
568
614
  if (action === 'query' && m === 'POST') {
569
- // Spec: broker returns FindDataResponse = { object, records, total?, hasMore? }
570
- const result = await broker.call('data.query', { object: objectName, ...body }, { request: context.request });
615
+ // Spec: returns FindDataResponse = { object, records, total?, hasMore? }
616
+ const result = await this.callData('query', { object: objectName, ...body });
571
617
  return { handled: true, response: this.success(result) };
572
618
  }
573
619
 
574
620
  // POST /data/:object/batch
575
621
  if (action === 'batch' && m === 'POST') {
576
- const result = await broker.call('data.batch', { object: objectName, ...body }, { request: context.request });
622
+ const result = await this.callData('batch', { object: objectName, ...body });
577
623
  return { handled: true, response: this.success(result) };
578
624
  }
579
625
 
@@ -586,24 +632,24 @@ export class HttpDispatcher {
586
632
  const allowedParams: Record<string, unknown> = {};
587
633
  if (select != null) allowedParams.select = select;
588
634
  if (expand != null) allowedParams.expand = expand;
589
- // Spec: broker returns GetDataResponse = { object, id, record }
590
- const result = await broker.call('data.get', { object: objectName, id, ...allowedParams }, { request: context.request });
635
+ // Spec: returns GetDataResponse = { object, id, record }
636
+ const result = await this.callData('get', { object: objectName, id, ...allowedParams });
591
637
  return { handled: true, response: this.success(result) };
592
638
  }
593
639
 
594
640
  // PATCH /data/:object/:id
595
641
  if (parts.length === 2 && m === 'PATCH') {
596
642
  const id = parts[1];
597
- // Spec: broker returns UpdateDataResponse = { object, id, record }
598
- const result = await broker.call('data.update', { object: objectName, id, data: body }, { request: context.request });
643
+ // Spec: returns UpdateDataResponse = { object, id, record }
644
+ const result = await this.callData('update', { object: objectName, id, data: body });
599
645
  return { handled: true, response: this.success(result) };
600
646
  }
601
647
 
602
648
  // DELETE /data/:object/:id
603
649
  if (parts.length === 2 && m === 'DELETE') {
604
650
  const id = parts[1];
605
- // Spec: broker returns DeleteDataResponse = { object, id, deleted }
606
- const result = await broker.call('data.delete', { object: objectName, id }, { request: context.request });
651
+ // Spec: returns DeleteDataResponse = { object, id, deleted }
652
+ const result = await this.callData('delete', { object: objectName, id });
607
653
  return { handled: true, response: this.success(result) };
608
654
  }
609
655
  } else {
@@ -613,16 +659,16 @@ export class HttpDispatcher {
613
659
  // HTTP GET query params use transport-level names (filter, sort, top,
614
660
  // skip, select, expand) which are normalized here to canonical
615
661
  // QueryAST field names (where, orderBy, limit, offset, fields,
616
- // expand) before forwarding to the broker layer.
662
+ // expand) before forwarding to the data service layer.
617
663
  // The protocol.ts findData() method performs a deeper normalization
618
- // pass, but pre-normalizing here ensures the broker always receives
664
+ // pass, but pre-normalizing here ensures the data service always receives
619
665
  // Spec-canonical keys.
620
666
  const normalized: Record<string, unknown> = { ...query };
621
667
 
622
668
  // filter/filters → where
623
669
  // Note: `filter` is the canonical HTTP *transport* parameter name
624
670
  // (see HttpFindQueryParamsSchema). It is normalized here to the
625
- // canonical *QueryAST* field name `where` before broker dispatch.
671
+ // canonical *QueryAST* field name `where` before data dispatch.
626
672
  // `filters` (plural) is a deprecated alias for `filter`.
627
673
  if (normalized.filter != null || normalized.filters != null) {
628
674
  normalized.where = normalized.where ?? normalized.filter ?? normalized.filters;
@@ -650,15 +696,15 @@ export class HttpDispatcher {
650
696
  delete normalized.skip;
651
697
  }
652
698
 
653
- // Spec: broker returns FindDataResponse = { object, records, total?, hasMore? }
654
- const result = await broker.call('data.query', { object: objectName, query: normalized }, { request: context.request });
699
+ // Spec: returns FindDataResponse = { object, records, total?, hasMore? }
700
+ const result = await this.callData('query', { object: objectName, query: normalized });
655
701
  return { handled: true, response: this.success(result) };
656
702
  }
657
703
 
658
704
  // POST /data/:object (Create)
659
705
  if (m === 'POST') {
660
- // Spec: broker returns CreateDataResponse = { object, id, record }
661
- const result = await broker.call('data.create', { object: objectName, data: body }, { request: context.request });
706
+ // Spec: returns CreateDataResponse = { object, id, record }
707
+ const result = await this.callData('create', { object: objectName, data: body });
662
708
  const res = this.success(result);
663
709
  res.status = 201;
664
710
  return { handled: true, response: res };
@@ -793,10 +839,9 @@ export class HttpDispatcher {
793
839
  * - POST /packages/:id/publish → publish a package (metadata snapshot)
794
840
  * - POST /packages/:id/revert → revert a package to last published state
795
841
  *
796
- * Uses ObjectQL SchemaRegistry directly (via the 'objectql' service)
797
- * with broker fallback for backward compatibility.
842
+ * Uses ObjectQL SchemaRegistry directly (via the 'objectql' service).
798
843
  */
799
- async handlePackages(path: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
844
+ async handlePackages(path: string, method: string, body: any, query: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult> {
800
845
  const m = method.toUpperCase();
801
846
  const parts = path.replace(/^\/+/, '').split('/').filter(Boolean);
802
847
 
@@ -804,11 +849,8 @@ export class HttpDispatcher {
804
849
  const qlService = await this.getObjectQLService();
805
850
  const registry = qlService?.registry;
806
851
 
807
- // If no registry available, try broker as fallback
852
+ // If no registry available, return 503
808
853
  if (!registry) {
809
- if (this.kernel.broker) {
810
- return this.handlePackagesViaBroker(parts, m, body, query, context);
811
- }
812
854
  return { handled: true, response: this.error('Package service not available', 503) };
813
855
  }
814
856
 
@@ -858,11 +900,6 @@ export class HttpDispatcher {
858
900
  const result = await (metadataService as any).publishPackage(id, body || {});
859
901
  return { handled: true, response: this.success(result) };
860
902
  }
861
- // Broker fallback
862
- if (this.kernel.broker) {
863
- const result = await this.kernel.broker.call('metadata.publishPackage', { packageId: id, ...body }, { request: context.request });
864
- return { handled: true, response: this.success(result) };
865
- }
866
903
  return { handled: true, response: this.error('Metadata service not available', 503) };
867
904
  }
868
905
 
@@ -874,11 +911,6 @@ export class HttpDispatcher {
874
911
  await (metadataService as any).revertPackage(id);
875
912
  return { handled: true, response: this.success({ success: true }) };
876
913
  }
877
- // Broker fallback
878
- if (this.kernel.broker) {
879
- await this.kernel.broker.call('metadata.revertPackage', { packageId: id }, { request: context.request });
880
- return { handled: true, response: this.success({ success: true }) };
881
- }
882
914
  return { handled: true, response: this.error('Metadata service not available', 503) };
883
915
  }
884
916
 
@@ -904,47 +936,7 @@ export class HttpDispatcher {
904
936
  return { handled: false };
905
937
  }
906
938
 
907
- /**
908
- * Fallback: handle packages via broker (for backward compatibility)
909
- */
910
- private async handlePackagesViaBroker(parts: string[], m: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
911
- const broker = this.kernel.broker;
912
- try {
913
- if (parts.length === 0 && m === 'GET') {
914
- const result = await broker.call('package.list', query || {}, { request: context.request });
915
- return { handled: true, response: this.success(result) };
916
- }
917
- if (parts.length === 0 && m === 'POST') {
918
- const result = await broker.call('package.install', body, { request: context.request });
919
- const res = this.success(result);
920
- res.status = 201;
921
- return { handled: true, response: res };
922
- }
923
- if (parts.length === 2 && parts[1] === 'enable' && m === 'PATCH') {
924
- const id = decodeURIComponent(parts[0]);
925
- const result = await broker.call('package.enable', { id }, { request: context.request });
926
- return { handled: true, response: this.success(result) };
927
- }
928
- if (parts.length === 2 && parts[1] === 'disable' && m === 'PATCH') {
929
- const id = decodeURIComponent(parts[0]);
930
- const result = await broker.call('package.disable', { id }, { request: context.request });
931
- return { handled: true, response: this.success(result) };
932
- }
933
- if (parts.length === 1 && m === 'GET') {
934
- const id = decodeURIComponent(parts[0]);
935
- const result = await broker.call('package.get', { id }, { request: context.request });
936
- return { handled: true, response: this.success(result) };
937
- }
938
- if (parts.length === 1 && m === 'DELETE') {
939
- const id = decodeURIComponent(parts[0]);
940
- const result = await broker.call('package.uninstall', { id }, { request: context.request });
941
- return { handled: true, response: this.success(result) };
942
- }
943
- } catch (e: any) {
944
- return { handled: true, response: this.error(e.message, e.statusCode || 500) };
945
- }
946
- return { handled: false };
947
- }
939
+
948
940
 
949
941
  /**
950
942
  * Handles Storage requests
@@ -1208,10 +1200,6 @@ export class HttpDispatcher {
1208
1200
  return null;
1209
1201
  }
1210
1202
 
1211
- private capitalize(s: string) {
1212
- return s.charAt(0).toUpperCase() + s.slice(1);
1213
- }
1214
-
1215
1203
  /**
1216
1204
  * Handle AI service routes (/ai/chat, /ai/models, /ai/conversations, etc.)
1217
1205
  * Resolves the AI service and its built-in route handlers, then dispatches.
@@ -1393,10 +1381,12 @@ export class HttpDispatcher {
1393
1381
 
1394
1382
  // OpenAPI Specification
1395
1383
  if (cleanPath === '/openapi.json' && method === 'GET') {
1396
- const broker = this.ensureBroker();
1397
1384
  try {
1398
- const result = await broker.call('metadata.generateOpenApi', {}, { request: context.request });
1399
- return { handled: true, response: this.success(result) };
1385
+ const metaSvc = await this.resolveService('metadata');
1386
+ if (metaSvc && typeof (metaSvc as any).generateOpenApi === 'function') {
1387
+ const result = await (metaSvc as any).generateOpenApi({});
1388
+ return { handled: true, response: this.success(result) };
1389
+ }
1400
1390
  } catch (e) {
1401
1391
  // If not implemented, fall through or return 404
1402
1392
  }
@@ -1418,17 +1408,22 @@ export class HttpDispatcher {
1418
1408
  * Handles Custom API Endpoints defined in metadata
1419
1409
  */
1420
1410
  async handleApiEndpoint(path: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
1421
- const broker = this.ensureBroker();
1422
1411
  try {
1423
1412
  // Attempt to find a matching endpoint in the registry
1424
- // This assumes a 'metadata.matchEndpoint' action exists in the kernel/registry
1425
- // path should include initial slash e.g. /api/v1/customers
1426
- const endpoint = await broker.call('metadata.matchEndpoint', { path, method });
1413
+ const metaSvc = await this.resolveService('metadata');
1414
+ if (!metaSvc || typeof (metaSvc as any).matchEndpoint !== 'function') {
1415
+ return { handled: false };
1416
+ }
1417
+ const endpoint = await (metaSvc as any).matchEndpoint({ path, method });
1427
1418
 
1428
1419
  if (endpoint) {
1429
1420
  // Execute the endpoint target logic
1430
1421
  if (endpoint.type === 'flow') {
1431
- const result = await broker.call('automation.runFlow', {
1422
+ const automationSvc = await this.resolveService('automation');
1423
+ if (!automationSvc || typeof (automationSvc as any).runFlow !== 'function') {
1424
+ return { handled: true, response: this.error('Automation service not available', 503) };
1425
+ }
1426
+ const result = await (automationSvc as any).runFlow({
1432
1427
  flowId: endpoint.target,
1433
1428
  inputs: { ...query, ...body, _request: context.request }
1434
1429
  });
@@ -1436,10 +1431,14 @@ export class HttpDispatcher {
1436
1431
  }
1437
1432
 
1438
1433
  if (endpoint.type === 'script') {
1439
- const result = await broker.call('automation.runScript', {
1434
+ const automationSvc = await this.resolveService('automation');
1435
+ if (!automationSvc || typeof (automationSvc as any).runScript !== 'function') {
1436
+ return { handled: true, response: this.error('Automation service not available', 503) };
1437
+ }
1438
+ const result = await (automationSvc as any).runScript({
1440
1439
  scriptName: endpoint.target,
1441
1440
  context: { ...query, ...body, request: context.request }
1442
- }, { request: context.request });
1441
+ });
1443
1442
  return { handled: true, response: this.success(result) };
1444
1443
  }
1445
1444
 
@@ -1449,25 +1448,22 @@ export class HttpDispatcher {
1449
1448
  const { object, operation } = endpoint.objectParams;
1450
1449
  // Map standard CRUD operations
1451
1450
  if (operation === 'find') {
1452
- const result = await broker.call('data.query', { object, query }, { request: context.request });
1451
+ const result = await this.callData('query', { object, query });
1453
1452
  // Spec: FindDataResponse = { object, records, total?, hasMore? }
1454
1453
  return { handled: true, response: this.success(result.records, { total: result.total }) };
1455
1454
  }
1456
1455
  if (operation === 'get' && query.id) {
1457
- const result = await broker.call('data.get', { object, id: query.id }, { request: context.request });
1456
+ const result = await this.callData('get', { object, id: query.id });
1458
1457
  return { handled: true, response: this.success(result) };
1459
1458
  }
1460
1459
  if (operation === 'create') {
1461
- const result = await broker.call('data.create', { object, data: body }, { request: context.request });
1460
+ const result = await this.callData('create', { object, data: body });
1462
1461
  return { handled: true, response: this.success(result) };
1463
1462
  }
1464
1463
  }
1465
1464
  }
1466
1465
 
1467
1466
  if (endpoint.type === 'proxy') {
1468
- // Simple proxy implementation (requires a network call, which usually is done by a service but here we can stub return)
1469
- // In real implementation this might fetch(endpoint.target)
1470
- // For now, return target info
1471
1467
  return {
1472
1468
  handled: true,
1473
1469
  response: {