@objectstack/runtime 3.2.0 → 3.2.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/dist/index.d.cts CHANGED
@@ -465,6 +465,12 @@ declare class HttpDispatcher {
465
465
  * path: sub-path after /auth/
466
466
  */
467
467
  handleAuth(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
468
+ /**
469
+ * Provides mock auth responses for core better-auth endpoints when
470
+ * AuthPlugin is not loaded (e.g. MSW/browser-only environments).
471
+ * This ensures registration/sign-in flows do not 404 in mock mode.
472
+ */
473
+ private mockAuthFallback;
468
474
  /**
469
475
  * Handles Metadata requests
470
476
  * Standard: /metadata/:type/:name
@@ -481,6 +487,18 @@ declare class HttpDispatcher {
481
487
  * path: sub-path after /analytics/
482
488
  */
483
489
  handleAnalytics(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
490
+ /**
491
+ * Handles i18n requests
492
+ * path: sub-path after /i18n/
493
+ *
494
+ * Routes:
495
+ * GET /locales → getLocales
496
+ * GET /translations/:locale → getTranslations (locale from path)
497
+ * GET /translations?locale=xx → getTranslations (locale from query)
498
+ * GET /labels/:object/:locale → getFieldLabels (both from path)
499
+ * GET /labels/:object?locale=xx → getFieldLabels (locale from query)
500
+ */
501
+ handleI18n(path: string, method: string, query: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult>;
484
502
  /**
485
503
  * Handles Package Management requests
486
504
  *
@@ -530,6 +548,12 @@ declare class HttpDispatcher {
530
548
  handleAutomation(path: string, method: string, body: any, context: HttpProtocolContext, query?: any): Promise<HttpDispatcherResult>;
531
549
  private getServicesMap;
532
550
  private getService;
551
+ /**
552
+ * Resolve any service by name, supporting async factories.
553
+ * Fallback chain: getServiceAsync → getService (sync) → context.getService → services map.
554
+ * Only returns when a non-null service is found; otherwise falls through to the next step.
555
+ */
556
+ private resolveService;
533
557
  /**
534
558
  * Get the ObjectQL service which provides access to SchemaRegistry.
535
559
  * Tries multiple access patterns since kernel structure varies.
package/dist/index.d.ts CHANGED
@@ -465,6 +465,12 @@ declare class HttpDispatcher {
465
465
  * path: sub-path after /auth/
466
466
  */
467
467
  handleAuth(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
468
+ /**
469
+ * Provides mock auth responses for core better-auth endpoints when
470
+ * AuthPlugin is not loaded (e.g. MSW/browser-only environments).
471
+ * This ensures registration/sign-in flows do not 404 in mock mode.
472
+ */
473
+ private mockAuthFallback;
468
474
  /**
469
475
  * Handles Metadata requests
470
476
  * Standard: /metadata/:type/:name
@@ -481,6 +487,18 @@ declare class HttpDispatcher {
481
487
  * path: sub-path after /analytics/
482
488
  */
483
489
  handleAnalytics(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
490
+ /**
491
+ * Handles i18n requests
492
+ * path: sub-path after /i18n/
493
+ *
494
+ * Routes:
495
+ * GET /locales → getLocales
496
+ * GET /translations/:locale → getTranslations (locale from path)
497
+ * GET /translations?locale=xx → getTranslations (locale from query)
498
+ * GET /labels/:object/:locale → getFieldLabels (both from path)
499
+ * GET /labels/:object?locale=xx → getFieldLabels (locale from query)
500
+ */
501
+ handleI18n(path: string, method: string, query: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult>;
484
502
  /**
485
503
  * Handles Package Management requests
486
504
  *
@@ -530,6 +548,12 @@ declare class HttpDispatcher {
530
548
  handleAutomation(path: string, method: string, body: any, context: HttpProtocolContext, query?: any): Promise<HttpDispatcherResult>;
531
549
  private getServicesMap;
532
550
  private getService;
551
+ /**
552
+ * Resolve any service by name, supporting async factories.
553
+ * Fallback chain: getServiceAsync → getService (sync) → context.getService → services map.
554
+ * Only returns when a non-null service is found; otherwise falls through to the next step.
555
+ */
556
+ private resolveService;
533
557
  /**
534
558
  * Get the ObjectQL service which provides access to SchemaRegistry.
535
559
  * Tries multiple access patterns since kernel structure varies.
package/dist/index.js CHANGED
@@ -171,6 +171,16 @@ var AppPlugin = class {
171
171
  // src/http-dispatcher.ts
172
172
  import { getEnv } from "@objectstack/core";
173
173
  import { CoreServiceName } from "@objectstack/spec/system";
174
+ function randomUUID() {
175
+ if (globalThis.crypto && typeof globalThis.crypto.randomUUID === "function") {
176
+ return globalThis.crypto.randomUUID();
177
+ }
178
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
179
+ const r = Math.random() * 16 | 0;
180
+ const v = c === "x" ? r : r & 3 | 8;
181
+ return v.toString(16);
182
+ });
183
+ }
174
184
  var HttpDispatcher = class {
175
185
  // Casting to any to access dynamic props like broker, services, graphql
176
186
  constructor(kernel) {
@@ -313,9 +323,64 @@ var HttpDispatcher = class {
313
323
  }
314
324
  const normalizedPath = path.replace(/^\/+/, "");
315
325
  if (normalizedPath === "login" && method.toUpperCase() === "POST") {
316
- const broker = this.ensureBroker();
317
- const data = await broker.call("auth.login", body, { request: context.request });
318
- return { handled: true, response: { status: 200, body: data } };
326
+ try {
327
+ const broker = this.ensureBroker();
328
+ const data = await broker.call("auth.login", body, { request: context.request });
329
+ return { handled: true, response: { status: 200, body: data } };
330
+ } catch (error) {
331
+ const statusCode = error?.statusCode ?? error?.status;
332
+ if (statusCode !== 500 || !error?.message?.includes("Broker not available")) {
333
+ throw error;
334
+ }
335
+ }
336
+ }
337
+ return this.mockAuthFallback(normalizedPath, method, body);
338
+ }
339
+ /**
340
+ * Provides mock auth responses for core better-auth endpoints when
341
+ * AuthPlugin is not loaded (e.g. MSW/browser-only environments).
342
+ * This ensures registration/sign-in flows do not 404 in mock mode.
343
+ */
344
+ mockAuthFallback(path, method, body) {
345
+ const m = method.toUpperCase();
346
+ const MOCK_SESSION_EXPIRY_MS = 864e5;
347
+ if ((path === "sign-up/email" || path === "register") && m === "POST") {
348
+ const id = `mock_${randomUUID()}`;
349
+ return {
350
+ handled: true,
351
+ response: {
352
+ status: 200,
353
+ body: {
354
+ user: { id, name: body?.name || "Mock User", email: body?.email || "mock@test.local", emailVerified: false, createdAt: (/* @__PURE__ */ new Date()).toISOString(), updatedAt: (/* @__PURE__ */ new Date()).toISOString() },
355
+ session: { id: `session_${id}`, userId: id, token: `mock_token_${id}`, expiresAt: new Date(Date.now() + MOCK_SESSION_EXPIRY_MS).toISOString() }
356
+ }
357
+ }
358
+ };
359
+ }
360
+ if ((path === "sign-in/email" || path === "login") && m === "POST") {
361
+ const id = `mock_${randomUUID()}`;
362
+ return {
363
+ handled: true,
364
+ response: {
365
+ status: 200,
366
+ body: {
367
+ user: { id, name: "Mock User", email: body?.email || "mock@test.local", emailVerified: true, createdAt: (/* @__PURE__ */ new Date()).toISOString(), updatedAt: (/* @__PURE__ */ new Date()).toISOString() },
368
+ session: { id: `session_${id}`, userId: id, token: `mock_token_${id}`, expiresAt: new Date(Date.now() + MOCK_SESSION_EXPIRY_MS).toISOString() }
369
+ }
370
+ }
371
+ };
372
+ }
373
+ if (path === "get-session" && m === "GET") {
374
+ return {
375
+ handled: true,
376
+ response: { status: 200, body: { session: null, user: null } }
377
+ };
378
+ }
379
+ if (path === "sign-out" && m === "POST") {
380
+ return {
381
+ handled: true,
382
+ response: { status: 200, body: { success: true } }
383
+ };
319
384
  }
320
385
  return { handled: false };
321
386
  }
@@ -328,7 +393,7 @@ var HttpDispatcher = class {
328
393
  const broker = this.ensureBroker();
329
394
  const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
330
395
  if (parts[0] === "types") {
331
- const protocol = this.kernel?.context?.getService ? await this.kernel.context.getService("protocol") : null;
396
+ const protocol = await this.resolveService("protocol");
332
397
  if (protocol && typeof protocol.getMetaTypes === "function") {
333
398
  const result = await protocol.getMetaTypes({});
334
399
  return { handled: true, response: this.success(result) };
@@ -358,7 +423,7 @@ var HttpDispatcher = class {
358
423
  if (parts.length === 2) {
359
424
  const [type, name] = parts;
360
425
  if (method === "PUT" && body) {
361
- const protocol = this.kernel?.context?.getService ? await this.kernel.context.getService("protocol") : null;
426
+ const protocol = await this.resolveService("protocol");
362
427
  if (protocol && typeof protocol.saveMetaItem === "function") {
363
428
  try {
364
429
  const result = await protocol.saveMetaItem({ type, name, item: body });
@@ -380,7 +445,7 @@ var HttpDispatcher = class {
380
445
  return { handled: true, response: this.success(data2) };
381
446
  }
382
447
  const singularType = type.endsWith("s") ? type.slice(0, -1) : type;
383
- const protocol = this.kernel?.context?.getService ? await this.kernel.context.getService("protocol") : null;
448
+ const protocol = await this.resolveService("protocol");
384
449
  if (protocol && typeof protocol.getMetaItem === "function") {
385
450
  try {
386
451
  const data2 = await protocol.getMetaItem({ type: singularType, name });
@@ -398,7 +463,7 @@ var HttpDispatcher = class {
398
463
  if (parts.length === 1) {
399
464
  const typeOrName = parts[0];
400
465
  const packageId = query?.package || void 0;
401
- const protocol = this.kernel?.context?.getService ? await this.kernel.context.getService("protocol") : null;
466
+ const protocol = await this.resolveService("protocol");
402
467
  if (protocol && typeof protocol.getMetaItems === "function") {
403
468
  try {
404
469
  const data = await protocol.getMetaItems({ type: typeOrName, packageId });
@@ -427,7 +492,7 @@ var HttpDispatcher = class {
427
492
  }
428
493
  }
429
494
  if (parts.length === 0) {
430
- const protocol = this.kernel?.context?.getService ? await this.kernel.context.getService("protocol") : null;
495
+ const protocol = await this.resolveService("protocol");
431
496
  if (protocol && typeof protocol.getMetaTypes === "function") {
432
497
  const result = await protocol.getMetaTypes({});
433
498
  return { handled: true, response: this.success(result) };
@@ -519,6 +584,53 @@ var HttpDispatcher = class {
519
584
  }
520
585
  return { handled: false };
521
586
  }
587
+ /**
588
+ * Handles i18n requests
589
+ * path: sub-path after /i18n/
590
+ *
591
+ * Routes:
592
+ * GET /locales → getLocales
593
+ * GET /translations/:locale → getTranslations (locale from path)
594
+ * GET /translations?locale=xx → getTranslations (locale from query)
595
+ * GET /labels/:object/:locale → getFieldLabels (both from path)
596
+ * GET /labels/:object?locale=xx → getFieldLabels (locale from query)
597
+ */
598
+ async handleI18n(path, method, query, _context) {
599
+ const i18nService = await this.getService(CoreServiceName.enum.i18n);
600
+ if (!i18nService) return { handled: true, response: this.error("i18n service not available", 501) };
601
+ const m = method.toUpperCase();
602
+ const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
603
+ if (m !== "GET") return { handled: false };
604
+ if (parts[0] === "locales" && parts.length === 1) {
605
+ const locales = i18nService.getLocales();
606
+ return { handled: true, response: this.success({ locales }) };
607
+ }
608
+ if (parts[0] === "translations") {
609
+ const locale = parts[1] ? decodeURIComponent(parts[1]) : query?.locale;
610
+ if (!locale) return { handled: true, response: this.error("Missing locale parameter", 400) };
611
+ const translations = i18nService.getTranslations(locale);
612
+ return { handled: true, response: this.success({ locale, translations }) };
613
+ }
614
+ if (parts[0] === "labels" && parts.length >= 2) {
615
+ const objectName = decodeURIComponent(parts[1]);
616
+ const locale = parts[2] ? decodeURIComponent(parts[2]) : query?.locale;
617
+ if (!locale) return { handled: true, response: this.error("Missing locale parameter", 400) };
618
+ if (typeof i18nService.getFieldLabels === "function") {
619
+ const labels2 = i18nService.getFieldLabels(objectName, locale);
620
+ return { handled: true, response: this.success({ object: objectName, locale, labels: labels2 }) };
621
+ }
622
+ const translations = i18nService.getTranslations(locale);
623
+ const prefix = `o.${objectName}.fields.`;
624
+ const labels = {};
625
+ for (const [key, value] of Object.entries(translations)) {
626
+ if (key.startsWith(prefix)) {
627
+ labels[key.substring(prefix.length)] = value;
628
+ }
629
+ }
630
+ return { handled: true, response: this.success({ object: objectName, locale, labels }) };
631
+ }
632
+ return { handled: false };
633
+ }
522
634
  /**
523
635
  * Handles Package Management requests
524
636
  *
@@ -709,7 +821,7 @@ var HttpDispatcher = class {
709
821
  if (parts[0] === "view" && parts[1]) {
710
822
  const objectName = parts[1];
711
823
  const type = parts[2] || query?.type || "list";
712
- const protocol = this.kernel?.context?.getService ? await this.kernel.context.getService("protocol") : null;
824
+ const protocol = await this.resolveService("protocol");
713
825
  if (protocol && typeof protocol.getUiView === "function") {
714
826
  try {
715
827
  const result = await protocol.getUiView({ object: objectName, type });
@@ -823,33 +935,48 @@ var HttpDispatcher = class {
823
935
  return this.kernel.services || {};
824
936
  }
825
937
  async getService(name) {
826
- if (typeof this.kernel.getService === "function") {
827
- return await this.kernel.getService(name);
828
- }
829
- const services = this.getServicesMap();
830
- return services[name];
938
+ return this.resolveService(name);
831
939
  }
832
940
  /**
833
- * Get the ObjectQL service which provides access to SchemaRegistry.
834
- * Tries multiple access patterns since kernel structure varies.
941
+ * Resolve any service by name, supporting async factories.
942
+ * Fallback chain: getServiceAsync getService (sync) context.getService → services map.
943
+ * Only returns when a non-null service is found; otherwise falls through to the next step.
835
944
  */
836
- async getObjectQLService() {
945
+ async resolveService(name) {
946
+ if (typeof this.kernel.getServiceAsync === "function") {
947
+ try {
948
+ const svc = await this.kernel.getServiceAsync(name);
949
+ if (svc != null) return svc;
950
+ } catch {
951
+ }
952
+ }
837
953
  if (typeof this.kernel.getService === "function") {
838
954
  try {
839
- const svc = await this.kernel.getService("objectql");
840
- if (svc?.registry) return svc;
955
+ const svc = await this.kernel.getService(name);
956
+ if (svc != null) return svc;
841
957
  } catch {
842
958
  }
843
959
  }
844
960
  if (this.kernel?.context?.getService) {
845
961
  try {
846
- const svc = await this.kernel.context.getService("objectql");
847
- if (svc?.registry) return svc;
962
+ const svc = await this.kernel.context.getService(name);
963
+ if (svc != null) return svc;
848
964
  } catch {
849
965
  }
850
966
  }
851
967
  const services = this.getServicesMap();
852
- if (services["objectql"]?.registry) return services["objectql"];
968
+ return services[name];
969
+ }
970
+ /**
971
+ * Get the ObjectQL service which provides access to SchemaRegistry.
972
+ * Tries multiple access patterns since kernel structure varies.
973
+ */
974
+ async getObjectQLService() {
975
+ try {
976
+ const svc = await this.resolveService("objectql");
977
+ if (svc?.registry) return svc;
978
+ } catch {
979
+ }
853
980
  return null;
854
981
  }
855
982
  capitalize(s) {
@@ -895,6 +1022,9 @@ var HttpDispatcher = class {
895
1022
  if (cleanPath.startsWith("/packages")) {
896
1023
  return this.handlePackages(cleanPath.substring(9), method, body, query, context);
897
1024
  }
1025
+ if (cleanPath.startsWith("/i18n")) {
1026
+ return this.handleI18n(cleanPath.substring(5), method, query, context);
1027
+ }
898
1028
  if (cleanPath === "/openapi.json" && method === "GET") {
899
1029
  const broker = this.ensureBroker();
900
1030
  try {