@objectstack/runtime 6.8.0 → 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.cjs CHANGED
@@ -1468,6 +1468,66 @@ var init_app_plugin = __esm({
1468
1468
  } catch (err) {
1469
1469
  ctx.logger.error("[AppPlugin] Failed to schedule approval-process registration", err, { appId });
1470
1470
  }
1471
+ try {
1472
+ const jobs = Array.isArray(this.bundle.jobs) ? this.bundle.jobs : Array.isArray((this.bundle.manifest || {}).jobs) ? this.bundle.manifest.jobs : [];
1473
+ if (jobs.length > 0) {
1474
+ ctx.hook("kernel:ready", async () => {
1475
+ let svc;
1476
+ try {
1477
+ svc = ctx.getService("job");
1478
+ } catch {
1479
+ }
1480
+ if (!svc || typeof svc.schedule !== "function") {
1481
+ ctx.logger.warn("[AppPlugin] job service not registered \u2014 skipping declarative jobs", {
1482
+ appId,
1483
+ jobCount: jobs.length
1484
+ });
1485
+ return;
1486
+ }
1487
+ const fnMap = collectBundleFunctions(this.bundle);
1488
+ let ok = 0;
1489
+ for (const job of jobs) {
1490
+ const jobName = job?.name;
1491
+ if (!jobName) {
1492
+ ctx.logger.warn("[AppPlugin] skipping job without name", { appId, job });
1493
+ continue;
1494
+ }
1495
+ if (job.enabled === false) {
1496
+ ctx.logger.debug("[AppPlugin] job disabled \u2014 skipping", { appId, job: jobName });
1497
+ continue;
1498
+ }
1499
+ const handler = fnMap[job.handler];
1500
+ if (typeof handler !== "function") {
1501
+ ctx.logger.warn("[AppPlugin] job handler not found in bundle.functions \u2014 skipping", {
1502
+ appId,
1503
+ job: jobName,
1504
+ handler: job.handler
1505
+ });
1506
+ continue;
1507
+ }
1508
+ try {
1509
+ await svc.schedule(
1510
+ jobName,
1511
+ job.schedule,
1512
+ async (jobCtx) => {
1513
+ await handler({ ...jobCtx, jobId: jobName, bundle: this.bundle });
1514
+ }
1515
+ );
1516
+ ok++;
1517
+ } catch (err) {
1518
+ ctx.logger.warn("[AppPlugin] Failed to schedule job", {
1519
+ appId,
1520
+ job: jobName,
1521
+ error: err?.message ?? String(err)
1522
+ });
1523
+ }
1524
+ }
1525
+ ctx.logger.info("[AppPlugin] Scheduled background jobs", { appId, count: ok });
1526
+ });
1527
+ }
1528
+ } catch (err) {
1529
+ ctx.logger.error("[AppPlugin] Failed to schedule background-job registration", err, { appId });
1530
+ }
1471
1531
  this.emitCatalogEvent(ctx, "app:registered", sys);
1472
1532
  await this.loadTranslations(ctx, appId);
1473
1533
  const seedDatasets = [];
@@ -1540,7 +1600,7 @@ var init_app_plugin = __esm({
1540
1600
  } catch (e) {
1541
1601
  ctx.logger.warn("[Seeder] Failed to register seed-datasets/seed-replayer service", { error: e?.message });
1542
1602
  }
1543
- const multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
1603
+ const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
1544
1604
  if (multiTenant) {
1545
1605
  ctx.logger.info("[Seeder] multi-tenant mode \u2014 skipping inline seed; per-org replay will run on sys_organization insert");
1546
1606
  } else {
@@ -1595,10 +1655,22 @@ var init_app_plugin = __esm({
1595
1655
  };
1596
1656
  this.bundle = bundle;
1597
1657
  this.projectContext = projectContext;
1598
- const sys = bundle.manifest || bundle;
1599
- const appId = sys.id || sys.name || "unnamed-app";
1658
+ const sys = bundle?.manifest || bundle;
1659
+ const appId = sys?.id || sys?.name;
1660
+ if (!appId) {
1661
+ const bundleKeys = bundle && typeof bundle === "object" ? Object.keys(bundle).slice(0, 20).join(",") : typeof bundle;
1662
+ const sysKeys = sys && typeof sys === "object" ? Object.keys(sys).slice(0, 20).join(",") : typeof sys;
1663
+ const ctxHint = projectContext ? ` projectContext=${JSON.stringify({
1664
+ environmentId: projectContext.environmentId,
1665
+ packageId: projectContext.packageId,
1666
+ source: projectContext.source
1667
+ })}` : "";
1668
+ throw new Error(
1669
+ `[AppPlugin] bundle is missing manifest.id and manifest.name \u2014 cannot register as a plugin. bundleKeys=[${bundleKeys}] sysKeys=[${sysKeys}]${ctxHint}`
1670
+ );
1671
+ }
1600
1672
  this.name = `plugin.app.${appId}`;
1601
- this.version = sys.version;
1673
+ this.version = sys?.version;
1602
1674
  }
1603
1675
  /**
1604
1676
  * Emit a kernel hook so the control-plane `AppCatalogService` can
@@ -2213,7 +2285,19 @@ var StandaloneStackConfigSchema = import_zod.z.object({
2213
2285
  databaseAuthToken: import_zod.z.string().optional(),
2214
2286
  databaseDriver: import_zod.z.enum(["sqlite", "sqlite-wasm", "turso", "memory", "postgres", "mongodb"]).optional(),
2215
2287
  environmentId: import_zod.z.string().optional(),
2216
- artifactPath: import_zod.z.string().optional()
2288
+ artifactPath: import_zod.z.string().optional(),
2289
+ /**
2290
+ * Project root directory. When set (typically by the CLI after locating
2291
+ * `objectstack.config.ts`), the default sqlite database is placed under
2292
+ * `<projectRoot>/.objectstack/data/standalone.db` instead of the global
2293
+ * `~/.objectstack/data/standalone.db`. This keeps per-project data
2294
+ * scoped to the project folder so different examples / apps don't
2295
+ * share a single database by accident.
2296
+ *
2297
+ * Explicit `databaseUrl` / `OS_DATABASE_URL` / `OS_HOME` still take
2298
+ * precedence over this default.
2299
+ */
2300
+ projectRoot: import_zod.z.string().optional()
2217
2301
  });
2218
2302
  function detectDriverFromUrl(dbUrl) {
2219
2303
  if (/^memory:\/\//i.test(dbUrl)) return "memory";
@@ -2238,7 +2322,7 @@ async function createStandaloneStack(config) {
2238
2322
  const environmentId = cfg.environmentId ?? process.env.OS_ENVIRONMENT_ID ?? "proj_local";
2239
2323
  const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? (0, import_node_path2.resolve)(cwd, "dist/objectstack.json");
2240
2324
  const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : (0, import_node_path2.resolve)(cwd, artifactPathInput);
2241
- const dbUrl = cfg.databaseUrl ?? process.env.OS_DATABASE_URL?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? `file:${(0, import_node_path2.resolve)(resolveObjectStackHome(), "data/standalone.db")}`;
2325
+ const dbUrl = cfg.databaseUrl ?? process.env.OS_DATABASE_URL?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? (process.env.OS_HOME?.trim() ? `file:${(0, import_node_path2.resolve)(resolveObjectStackHome(), "data/standalone.db")}` : cfg.projectRoot ? `file:${(0, import_node_path2.resolve)(cfg.projectRoot, ".objectstack/data/standalone.db")}` : `file:${(0, import_node_path2.resolve)(resolveObjectStackHome(), "data/standalone.db")}`);
2242
2326
  const dbAuthToken = cfg.databaseAuthToken ?? process.env.OS_DATABASE_AUTH_TOKEN?.trim() ?? process.env.TURSO_AUTH_TOKEN?.trim();
2243
2327
  const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
2244
2328
  const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
@@ -2456,6 +2540,13 @@ function toHeaders(input) {
2456
2540
  }
2457
2541
  return h;
2458
2542
  }
2543
+ function safeJsonParse2(s, fallback) {
2544
+ try {
2545
+ return JSON.parse(s);
2546
+ } catch {
2547
+ return fallback;
2548
+ }
2549
+ }
2459
2550
  async function tryFind(ql, object, where, limit = 100) {
2460
2551
  if (!ql || typeof ql.find !== "function") return [];
2461
2552
  try {
@@ -2471,6 +2562,7 @@ async function resolveExecutionContext(opts) {
2471
2562
  const ctx = {
2472
2563
  roles: [],
2473
2564
  permissions: [],
2565
+ systemPermissions: [],
2474
2566
  isSystem: false
2475
2567
  };
2476
2568
  let userId;
@@ -2508,8 +2600,12 @@ async function resolveExecutionContext(opts) {
2508
2600
  if (!userId) {
2509
2601
  try {
2510
2602
  const authService = await opts.getService("auth");
2603
+ let api = authService?.api;
2604
+ if (!api && typeof authService?.getApi === "function") {
2605
+ api = await authService.getApi();
2606
+ }
2511
2607
  const headersInstance = toHeaders(headers);
2512
- const sessionData = await authService?.api?.getSession?.({ headers: headersInstance });
2608
+ const sessionData = await api?.getSession?.({ headers: headersInstance });
2513
2609
  userId = sessionData?.user?.id ?? sessionData?.session?.userId;
2514
2610
  tenantId = tenantId ?? sessionData?.session?.activeOrganizationId;
2515
2611
  ctx.accessToken = sessionData?.session?.token ?? ctx.accessToken;
@@ -2579,10 +2675,38 @@ async function resolveExecutionContext(opts) {
2579
2675
  { id: { $in: Array.from(psIds) } },
2580
2676
  500
2581
2677
  );
2678
+ const tabRank = {
2679
+ hidden: 0,
2680
+ default_off: 1,
2681
+ default_on: 2,
2682
+ visible: 3
2683
+ };
2684
+ const mergedTabs = {};
2582
2685
  for (const ps of psRows) {
2583
2686
  if (ps.name && !ctx.permissions.includes(ps.name)) {
2584
2687
  ctx.permissions.push(ps.name);
2585
2688
  }
2689
+ const sysPerms = typeof ps.system_permissions === "string" ? safeJsonParse2(ps.system_permissions, []) : ps.system_permissions ?? ps.systemPermissions;
2690
+ if (Array.isArray(sysPerms)) {
2691
+ for (const p of sysPerms) {
2692
+ if (typeof p === "string" && !ctx.systemPermissions.includes(p)) {
2693
+ ctx.systemPermissions.push(p);
2694
+ }
2695
+ }
2696
+ }
2697
+ const tabs = typeof ps.tab_permissions === "string" ? safeJsonParse2(ps.tab_permissions, {}) : ps.tab_permissions ?? ps.tabPermissions;
2698
+ if (tabs && typeof tabs === "object") {
2699
+ for (const [app, val] of Object.entries(tabs)) {
2700
+ if (typeof val !== "string" || !(val in tabRank)) continue;
2701
+ const cur = mergedTabs[app];
2702
+ if (!cur || tabRank[val] > tabRank[cur]) {
2703
+ mergedTabs[app] = val;
2704
+ }
2705
+ }
2706
+ }
2707
+ }
2708
+ if (Object.keys(mergedTabs).length > 0) {
2709
+ ctx.tabPermissions = mergedTabs;
2586
2710
  }
2587
2711
  }
2588
2712
  return ctx;
@@ -5326,7 +5450,7 @@ var _HttpDispatcher = class _HttpDispatcher {
5326
5450
  * Handle AI service routes (/ai/chat, /ai/models, /ai/conversations, etc.)
5327
5451
  * Resolves the AI service and its built-in route handlers, then dispatches.
5328
5452
  */
5329
- async handleAI(subPath, method, body, query, _context) {
5453
+ async handleAI(subPath, method, body, query, context) {
5330
5454
  let aiService;
5331
5455
  try {
5332
5456
  aiService = await this.resolveService("ai");
@@ -5370,7 +5494,23 @@ var _HttpDispatcher = class _HttpDispatcher {
5370
5494
  if (route.method !== method) continue;
5371
5495
  const params = matchRoute(route.path, fullPath);
5372
5496
  if (params === null) continue;
5373
- const result = await route.handler({ body, params, query });
5497
+ const ec = context.executionContext;
5498
+ const user = ec?.userId ? {
5499
+ userId: ec.userId,
5500
+ id: ec.userId,
5501
+ displayName: ec.userDisplayName ?? ec.userName ?? ec.userId,
5502
+ email: ec.userEmail,
5503
+ roles: Array.isArray(ec.roles) ? ec.roles : [],
5504
+ permissions: Array.isArray(ec.permissions) ? ec.permissions : [],
5505
+ organizationId: ec.tenantId
5506
+ } : void 0;
5507
+ const result = await route.handler({
5508
+ body,
5509
+ params,
5510
+ query,
5511
+ headers: context.request?.headers,
5512
+ user
5513
+ });
5374
5514
  if (result.stream && result.events) {
5375
5515
  return {
5376
5516
  handled: true,
@@ -5865,13 +6005,22 @@ function resolveErrorReporter(ctx, override) {
5865
6005
  }
5866
6006
 
5867
6007
  // src/dispatcher-plugin.ts
5868
- function mountRouteOnServer(route, server, routePath, securityHeaders) {
6008
+ function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUser) {
5869
6009
  const handler = async (req, res) => {
5870
6010
  try {
6011
+ let user;
6012
+ if (resolveUser) {
6013
+ try {
6014
+ user = await resolveUser(req.headers ?? {});
6015
+ } catch {
6016
+ }
6017
+ }
5871
6018
  const result = await route.handler({
5872
6019
  body: req.body,
5873
6020
  params: req.params,
5874
- query: req.query
6021
+ query: req.query,
6022
+ headers: req.headers,
6023
+ user
5875
6024
  });
5876
6025
  if (result.stream && result.events) {
5877
6026
  res.status(result.status);
@@ -6608,6 +6757,32 @@ function createDispatcherPlugin(config = {}) {
6608
6757
  }
6609
6758
  }
6610
6759
  ctx.logger.info("Dispatcher bridge routes registered", { prefix, enableProjectScoping, projectResolution });
6760
+ const resolveRequestUser = async (headers) => {
6761
+ try {
6762
+ const authService = ctx.getService("auth");
6763
+ if (!authService) return void 0;
6764
+ let api = authService.api;
6765
+ if (!api && typeof authService.getApi === "function") {
6766
+ api = await authService.getApi();
6767
+ }
6768
+ if (!api?.getSession) return void 0;
6769
+ const headersInstance = headers instanceof Headers ? headers : new Headers(headers);
6770
+ const sessionData = await api.getSession({ headers: headersInstance });
6771
+ const userId = sessionData?.user?.id ?? sessionData?.session?.userId;
6772
+ if (!userId) return void 0;
6773
+ return {
6774
+ userId,
6775
+ id: userId,
6776
+ displayName: sessionData?.user?.name ?? sessionData?.user?.email ?? userId,
6777
+ email: sessionData?.user?.email,
6778
+ roles: [],
6779
+ permissions: [],
6780
+ organizationId: sessionData?.session?.activeOrganizationId
6781
+ };
6782
+ } catch {
6783
+ return void 0;
6784
+ }
6785
+ };
6611
6786
  const toScopedPath = (routePath) => {
6612
6787
  if (routePath.startsWith(prefix)) {
6613
6788
  const tail = routePath.slice(prefix.length);
@@ -6620,11 +6795,11 @@ function createDispatcherPlugin(config = {}) {
6620
6795
  const routePath = route.path.startsWith("/api/v1") ? route.path : `${prefix}${route.path}`;
6621
6796
  let count = 0;
6622
6797
  if (enableProjectScoping && projectResolution === "required") {
6623
- if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders)) count++;
6798
+ if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
6624
6799
  } else {
6625
- if (mountRouteOnServer(route, server, routePath, securityHeaders)) count++;
6800
+ if (mountRouteOnServer(route, server, routePath, securityHeaders, resolveRequestUser)) count++;
6626
6801
  if (enableProjectScoping) {
6627
- if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders)) count++;
6802
+ if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
6628
6803
  }
6629
6804
  }
6630
6805
  return count;
@@ -7686,39 +7861,7 @@ var ArtifactKernelFactory = class {
7686
7861
  // intentionally do NOT pass crossSubDomainCookies here
7687
7862
  // so cookies stay isolated per project subdomain.
7688
7863
  trustedOrigins: trustedOriginsList.length ? trustedOriginsList : void 0,
7689
- ...oidcProviders ? { oidcProviders } : {},
7690
- // Auto-provision a personal organization for every new
7691
- // user. SecurityPlugin's ObjectQL middleware does this
7692
- // for direct `ql.insert` calls, but better-auth's
7693
- // adapter writes through `dataEngine` directly,
7694
- // bypassing that middleware — so JIT-created SSO users
7695
- // would otherwise land on the empty "create
7696
- // organization" screen on first login.
7697
- databaseHooks: {
7698
- user: {
7699
- create: {
7700
- after: async (user) => {
7701
- try {
7702
- const ql = kernel.getService("objectql");
7703
- if (!ql) return;
7704
- const [{ ensureUserHasOrganization, cloneTenantSeedData }] = await Promise.all([
7705
- import("@objectstack/plugin-security")
7706
- ]);
7707
- await ensureUserHasOrganization(ql, user, {
7708
- logger: this.logger,
7709
- cloneSeedData: cloneTenantSeedData
7710
- });
7711
- } catch (e) {
7712
- this.logger.warn?.("[ArtifactKernelFactory] auto-org provisioning hook failed", {
7713
- environmentId,
7714
- userId: user?.id,
7715
- error: e?.message
7716
- });
7717
- }
7718
- }
7719
- }
7720
- }
7721
- }
7864
+ ...oidcProviders ? { oidcProviders } : {}
7722
7865
  }));
7723
7866
  if (oidcProviders) {
7724
7867
  this.logger.info?.("[ArtifactKernelFactory] platform SSO wired", {
@@ -7737,7 +7880,7 @@ var ArtifactKernelFactory = class {
7737
7880
  }
7738
7881
  try {
7739
7882
  const { SecurityPlugin } = await import("@objectstack/plugin-security");
7740
- const multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
7883
+ const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
7741
7884
  await kernel.use(new SecurityPlugin({ multiTenant }));
7742
7885
  } catch (err) {
7743
7886
  this.logger.warn?.("[ArtifactKernelFactory] SecurityPlugin not registered", {
@@ -7746,8 +7889,15 @@ var ArtifactKernelFactory = class {
7746
7889
  });
7747
7890
  }
7748
7891
  const projectName = project.hostname ?? environmentId;
7749
- const bundle = artifact.metadata;
7750
- const sys = bundle?.manifest ?? bundle;
7892
+ const artifactAny = artifact;
7893
+ const topLevelManifest = artifactAny?.manifest && typeof artifactAny.manifest === "object" ? artifactAny.manifest : null;
7894
+ const topLevelFunctions = Array.isArray(artifactAny?.functions) ? artifactAny.functions : [];
7895
+ const bundle = {
7896
+ ...artifact.metadata ?? {},
7897
+ ...topLevelManifest ? { manifest: topLevelManifest } : {},
7898
+ functions: topLevelFunctions
7899
+ };
7900
+ const sys = bundle.manifest ?? bundle;
7751
7901
  const packageId = sys?.packageId ?? sys?.package_id ?? bundle?.packageId;
7752
7902
  const i18nCfg = bundle?.i18n ?? sys?.i18n ?? {};
7753
7903
  const trArr = Array.isArray(bundle?.translations) ? bundle.translations : Array.isArray(sys?.translations) ? sys.translations : [];
@@ -9015,7 +9165,7 @@ var MarketplaceInstallLocalPlugin = class {
9015
9165
  }
9016
9166
  }
9017
9167
  if (opts.seedNow && datasets.length > 0) {
9018
- const multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
9168
+ const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
9019
9169
  try {
9020
9170
  const ql = ctx.getService("objectql");
9021
9171
  let metadata;