@objectstack/runtime 6.8.1 → 6.9.0

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
@@ -1486,7 +1486,7 @@ declare class HttpDispatcher {
1486
1486
  * Handle AI service routes (/ai/chat, /ai/models, /ai/conversations, etc.)
1487
1487
  * Resolves the AI service and its built-in route handlers, then dispatches.
1488
1488
  */
1489
- handleAI(subPath: string, method: string, body: any, query: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1489
+ handleAI(subPath: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1490
1490
  /**
1491
1491
  * Main Dispatcher Entry Point
1492
1492
  * Routes the request to the appropriate handler based on path and precedence
package/dist/index.d.ts CHANGED
@@ -1486,7 +1486,7 @@ declare class HttpDispatcher {
1486
1486
  * Handle AI service routes (/ai/chat, /ai/models, /ai/conversations, etc.)
1487
1487
  * Resolves the AI service and its built-in route handlers, then dispatches.
1488
1488
  */
1489
- handleAI(subPath: string, method: string, body: any, query: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1489
+ handleAI(subPath: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1490
1490
  /**
1491
1491
  * Main Dispatcher Entry Point
1492
1492
  * Routes the request to the appropriate handler based on path and precedence
package/dist/index.js CHANGED
@@ -1445,6 +1445,66 @@ var init_app_plugin = __esm({
1445
1445
  } catch (err) {
1446
1446
  ctx.logger.error("[AppPlugin] Failed to schedule approval-process registration", err, { appId });
1447
1447
  }
1448
+ try {
1449
+ const jobs = Array.isArray(this.bundle.jobs) ? this.bundle.jobs : Array.isArray((this.bundle.manifest || {}).jobs) ? this.bundle.manifest.jobs : [];
1450
+ if (jobs.length > 0) {
1451
+ ctx.hook("kernel:ready", async () => {
1452
+ let svc;
1453
+ try {
1454
+ svc = ctx.getService("job");
1455
+ } catch {
1456
+ }
1457
+ if (!svc || typeof svc.schedule !== "function") {
1458
+ ctx.logger.warn("[AppPlugin] job service not registered \u2014 skipping declarative jobs", {
1459
+ appId,
1460
+ jobCount: jobs.length
1461
+ });
1462
+ return;
1463
+ }
1464
+ const fnMap = collectBundleFunctions(this.bundle);
1465
+ let ok = 0;
1466
+ for (const job of jobs) {
1467
+ const jobName = job?.name;
1468
+ if (!jobName) {
1469
+ ctx.logger.warn("[AppPlugin] skipping job without name", { appId, job });
1470
+ continue;
1471
+ }
1472
+ if (job.enabled === false) {
1473
+ ctx.logger.debug("[AppPlugin] job disabled \u2014 skipping", { appId, job: jobName });
1474
+ continue;
1475
+ }
1476
+ const handler = fnMap[job.handler];
1477
+ if (typeof handler !== "function") {
1478
+ ctx.logger.warn("[AppPlugin] job handler not found in bundle.functions \u2014 skipping", {
1479
+ appId,
1480
+ job: jobName,
1481
+ handler: job.handler
1482
+ });
1483
+ continue;
1484
+ }
1485
+ try {
1486
+ await svc.schedule(
1487
+ jobName,
1488
+ job.schedule,
1489
+ async (jobCtx) => {
1490
+ await handler({ ...jobCtx, jobId: jobName, bundle: this.bundle });
1491
+ }
1492
+ );
1493
+ ok++;
1494
+ } catch (err) {
1495
+ ctx.logger.warn("[AppPlugin] Failed to schedule job", {
1496
+ appId,
1497
+ job: jobName,
1498
+ error: err?.message ?? String(err)
1499
+ });
1500
+ }
1501
+ }
1502
+ ctx.logger.info("[AppPlugin] Scheduled background jobs", { appId, count: ok });
1503
+ });
1504
+ }
1505
+ } catch (err) {
1506
+ ctx.logger.error("[AppPlugin] Failed to schedule background-job registration", err, { appId });
1507
+ }
1448
1508
  this.emitCatalogEvent(ctx, "app:registered", sys);
1449
1509
  await this.loadTranslations(ctx, appId);
1450
1510
  const seedDatasets = [];
@@ -1517,7 +1577,7 @@ var init_app_plugin = __esm({
1517
1577
  } catch (e) {
1518
1578
  ctx.logger.warn("[Seeder] Failed to register seed-datasets/seed-replayer service", { error: e?.message });
1519
1579
  }
1520
- const multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
1580
+ const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
1521
1581
  if (multiTenant) {
1522
1582
  ctx.logger.info("[Seeder] multi-tenant mode \u2014 skipping inline seed; per-org replay will run on sys_organization insert");
1523
1583
  } else {
@@ -1572,10 +1632,22 @@ var init_app_plugin = __esm({
1572
1632
  };
1573
1633
  this.bundle = bundle;
1574
1634
  this.projectContext = projectContext;
1575
- const sys = bundle.manifest || bundle;
1576
- const appId = sys.id || sys.name || "unnamed-app";
1635
+ const sys = bundle?.manifest || bundle;
1636
+ const appId = sys?.id || sys?.name;
1637
+ if (!appId) {
1638
+ const bundleKeys = bundle && typeof bundle === "object" ? Object.keys(bundle).slice(0, 20).join(",") : typeof bundle;
1639
+ const sysKeys = sys && typeof sys === "object" ? Object.keys(sys).slice(0, 20).join(",") : typeof sys;
1640
+ const ctxHint = projectContext ? ` projectContext=${JSON.stringify({
1641
+ environmentId: projectContext.environmentId,
1642
+ packageId: projectContext.packageId,
1643
+ source: projectContext.source
1644
+ })}` : "";
1645
+ throw new Error(
1646
+ `[AppPlugin] bundle is missing manifest.id and manifest.name \u2014 cannot register as a plugin. bundleKeys=[${bundleKeys}] sysKeys=[${sysKeys}]${ctxHint}`
1647
+ );
1648
+ }
1577
1649
  this.name = `plugin.app.${appId}`;
1578
- this.version = sys.version;
1650
+ this.version = sys?.version;
1579
1651
  }
1580
1652
  /**
1581
1653
  * Emit a kernel hook so the control-plane `AppCatalogService` can
@@ -2437,8 +2509,12 @@ async function resolveExecutionContext(opts) {
2437
2509
  if (!userId) {
2438
2510
  try {
2439
2511
  const authService = await opts.getService("auth");
2512
+ let api = authService?.api;
2513
+ if (!api && typeof authService?.getApi === "function") {
2514
+ api = await authService.getApi();
2515
+ }
2440
2516
  const headersInstance = toHeaders(headers);
2441
- const sessionData = await authService?.api?.getSession?.({ headers: headersInstance });
2517
+ const sessionData = await api?.getSession?.({ headers: headersInstance });
2442
2518
  userId = sessionData?.user?.id ?? sessionData?.session?.userId;
2443
2519
  tenantId = tenantId ?? sessionData?.session?.activeOrganizationId;
2444
2520
  ctx.accessToken = sessionData?.session?.token ?? ctx.accessToken;
@@ -5283,7 +5359,7 @@ var _HttpDispatcher = class _HttpDispatcher {
5283
5359
  * Handle AI service routes (/ai/chat, /ai/models, /ai/conversations, etc.)
5284
5360
  * Resolves the AI service and its built-in route handlers, then dispatches.
5285
5361
  */
5286
- async handleAI(subPath, method, body, query, _context) {
5362
+ async handleAI(subPath, method, body, query, context) {
5287
5363
  let aiService;
5288
5364
  try {
5289
5365
  aiService = await this.resolveService("ai");
@@ -5327,7 +5403,23 @@ var _HttpDispatcher = class _HttpDispatcher {
5327
5403
  if (route.method !== method) continue;
5328
5404
  const params = matchRoute(route.path, fullPath);
5329
5405
  if (params === null) continue;
5330
- const result = await route.handler({ body, params, query });
5406
+ const ec = context.executionContext;
5407
+ const user = ec?.userId ? {
5408
+ userId: ec.userId,
5409
+ id: ec.userId,
5410
+ displayName: ec.userDisplayName ?? ec.userName ?? ec.userId,
5411
+ email: ec.userEmail,
5412
+ roles: Array.isArray(ec.roles) ? ec.roles : [],
5413
+ permissions: Array.isArray(ec.permissions) ? ec.permissions : [],
5414
+ organizationId: ec.tenantId
5415
+ } : void 0;
5416
+ const result = await route.handler({
5417
+ body,
5418
+ params,
5419
+ query,
5420
+ headers: context.request?.headers,
5421
+ user
5422
+ });
5331
5423
  if (result.stream && result.events) {
5332
5424
  return {
5333
5425
  handled: true,
@@ -5832,13 +5924,22 @@ function resolveErrorReporter(ctx, override) {
5832
5924
  }
5833
5925
 
5834
5926
  // src/dispatcher-plugin.ts
5835
- function mountRouteOnServer(route, server, routePath, securityHeaders) {
5927
+ function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUser) {
5836
5928
  const handler = async (req, res) => {
5837
5929
  try {
5930
+ let user;
5931
+ if (resolveUser) {
5932
+ try {
5933
+ user = await resolveUser(req.headers ?? {});
5934
+ } catch {
5935
+ }
5936
+ }
5838
5937
  const result = await route.handler({
5839
5938
  body: req.body,
5840
5939
  params: req.params,
5841
- query: req.query
5940
+ query: req.query,
5941
+ headers: req.headers,
5942
+ user
5842
5943
  });
5843
5944
  if (result.stream && result.events) {
5844
5945
  res.status(result.status);
@@ -6575,6 +6676,32 @@ function createDispatcherPlugin(config = {}) {
6575
6676
  }
6576
6677
  }
6577
6678
  ctx.logger.info("Dispatcher bridge routes registered", { prefix, enableProjectScoping, projectResolution });
6679
+ const resolveRequestUser = async (headers) => {
6680
+ try {
6681
+ const authService = ctx.getService("auth");
6682
+ if (!authService) return void 0;
6683
+ let api = authService.api;
6684
+ if (!api && typeof authService.getApi === "function") {
6685
+ api = await authService.getApi();
6686
+ }
6687
+ if (!api?.getSession) return void 0;
6688
+ const headersInstance = headers instanceof Headers ? headers : new Headers(headers);
6689
+ const sessionData = await api.getSession({ headers: headersInstance });
6690
+ const userId = sessionData?.user?.id ?? sessionData?.session?.userId;
6691
+ if (!userId) return void 0;
6692
+ return {
6693
+ userId,
6694
+ id: userId,
6695
+ displayName: sessionData?.user?.name ?? sessionData?.user?.email ?? userId,
6696
+ email: sessionData?.user?.email,
6697
+ roles: [],
6698
+ permissions: [],
6699
+ organizationId: sessionData?.session?.activeOrganizationId
6700
+ };
6701
+ } catch {
6702
+ return void 0;
6703
+ }
6704
+ };
6578
6705
  const toScopedPath = (routePath) => {
6579
6706
  if (routePath.startsWith(prefix)) {
6580
6707
  const tail = routePath.slice(prefix.length);
@@ -6587,11 +6714,11 @@ function createDispatcherPlugin(config = {}) {
6587
6714
  const routePath = route.path.startsWith("/api/v1") ? route.path : `${prefix}${route.path}`;
6588
6715
  let count = 0;
6589
6716
  if (enableProjectScoping && projectResolution === "required") {
6590
- if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders)) count++;
6717
+ if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
6591
6718
  } else {
6592
- if (mountRouteOnServer(route, server, routePath, securityHeaders)) count++;
6719
+ if (mountRouteOnServer(route, server, routePath, securityHeaders, resolveRequestUser)) count++;
6593
6720
  if (enableProjectScoping) {
6594
- if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders)) count++;
6721
+ if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
6595
6722
  }
6596
6723
  }
6597
6724
  return count;
@@ -7653,39 +7780,7 @@ var ArtifactKernelFactory = class {
7653
7780
  // intentionally do NOT pass crossSubDomainCookies here
7654
7781
  // so cookies stay isolated per project subdomain.
7655
7782
  trustedOrigins: trustedOriginsList.length ? trustedOriginsList : void 0,
7656
- ...oidcProviders ? { oidcProviders } : {},
7657
- // Auto-provision a personal organization for every new
7658
- // user. SecurityPlugin's ObjectQL middleware does this
7659
- // for direct `ql.insert` calls, but better-auth's
7660
- // adapter writes through `dataEngine` directly,
7661
- // bypassing that middleware — so JIT-created SSO users
7662
- // would otherwise land on the empty "create
7663
- // organization" screen on first login.
7664
- databaseHooks: {
7665
- user: {
7666
- create: {
7667
- after: async (user) => {
7668
- try {
7669
- const ql = kernel.getService("objectql");
7670
- if (!ql) return;
7671
- const [{ ensureUserHasOrganization, cloneTenantSeedData }] = await Promise.all([
7672
- import("@objectstack/plugin-security")
7673
- ]);
7674
- await ensureUserHasOrganization(ql, user, {
7675
- logger: this.logger,
7676
- cloneSeedData: cloneTenantSeedData
7677
- });
7678
- } catch (e) {
7679
- this.logger.warn?.("[ArtifactKernelFactory] auto-org provisioning hook failed", {
7680
- environmentId,
7681
- userId: user?.id,
7682
- error: e?.message
7683
- });
7684
- }
7685
- }
7686
- }
7687
- }
7688
- }
7783
+ ...oidcProviders ? { oidcProviders } : {}
7689
7784
  }));
7690
7785
  if (oidcProviders) {
7691
7786
  this.logger.info?.("[ArtifactKernelFactory] platform SSO wired", {
@@ -7704,7 +7799,7 @@ var ArtifactKernelFactory = class {
7704
7799
  }
7705
7800
  try {
7706
7801
  const { SecurityPlugin } = await import("@objectstack/plugin-security");
7707
- const multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
7802
+ const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
7708
7803
  await kernel.use(new SecurityPlugin({ multiTenant }));
7709
7804
  } catch (err) {
7710
7805
  this.logger.warn?.("[ArtifactKernelFactory] SecurityPlugin not registered", {
@@ -7713,8 +7808,15 @@ var ArtifactKernelFactory = class {
7713
7808
  });
7714
7809
  }
7715
7810
  const projectName = project.hostname ?? environmentId;
7716
- const bundle = artifact.metadata;
7717
- const sys = bundle?.manifest ?? bundle;
7811
+ const artifactAny = artifact;
7812
+ const topLevelManifest = artifactAny?.manifest && typeof artifactAny.manifest === "object" ? artifactAny.manifest : null;
7813
+ const topLevelFunctions = Array.isArray(artifactAny?.functions) ? artifactAny.functions : [];
7814
+ const bundle = {
7815
+ ...artifact.metadata ?? {},
7816
+ ...topLevelManifest ? { manifest: topLevelManifest } : {},
7817
+ functions: topLevelFunctions
7818
+ };
7819
+ const sys = bundle.manifest ?? bundle;
7718
7820
  const packageId = sys?.packageId ?? sys?.package_id ?? bundle?.packageId;
7719
7821
  const i18nCfg = bundle?.i18n ?? sys?.i18n ?? {};
7720
7822
  const trArr = Array.isArray(bundle?.translations) ? bundle.translations : Array.isArray(sys?.translations) ? sys.translations : [];
@@ -8982,7 +9084,7 @@ var MarketplaceInstallLocalPlugin = class {
8982
9084
  }
8983
9085
  }
8984
9086
  if (opts.seedNow && datasets.length > 0) {
8985
- const multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
9087
+ const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
8986
9088
  try {
8987
9089
  const ql = ctx.getService("objectql");
8988
9090
  let metadata;