@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.d.cts CHANGED
@@ -98,6 +98,7 @@ declare const StandaloneStackConfigSchema: z.ZodObject<{
98
98
  }>>;
99
99
  environmentId: z.ZodOptional<z.ZodString>;
100
100
  artifactPath: z.ZodOptional<z.ZodString>;
101
+ projectRoot: z.ZodOptional<z.ZodString>;
101
102
  }, z.core.$strip>;
102
103
  type StandaloneStackConfig = z.input<typeof StandaloneStackConfigSchema>;
103
104
  interface StandaloneStackResult {
@@ -1485,7 +1486,7 @@ declare class HttpDispatcher {
1485
1486
  * Handle AI service routes (/ai/chat, /ai/models, /ai/conversations, etc.)
1486
1487
  * Resolves the AI service and its built-in route handlers, then dispatches.
1487
1488
  */
1488
- 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>;
1489
1490
  /**
1490
1491
  * Main Dispatcher Entry Point
1491
1492
  * Routes the request to the appropriate handler based on path and precedence
package/dist/index.d.ts CHANGED
@@ -98,6 +98,7 @@ declare const StandaloneStackConfigSchema: z.ZodObject<{
98
98
  }>>;
99
99
  environmentId: z.ZodOptional<z.ZodString>;
100
100
  artifactPath: z.ZodOptional<z.ZodString>;
101
+ projectRoot: z.ZodOptional<z.ZodString>;
101
102
  }, z.core.$strip>;
102
103
  type StandaloneStackConfig = z.input<typeof StandaloneStackConfigSchema>;
103
104
  interface StandaloneStackResult {
@@ -1485,7 +1486,7 @@ declare class HttpDispatcher {
1485
1486
  * Handle AI service routes (/ai/chat, /ai/models, /ai/conversations, etc.)
1486
1487
  * Resolves the AI service and its built-in route handlers, then dispatches.
1487
1488
  */
1488
- 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>;
1489
1490
  /**
1490
1491
  * Main Dispatcher Entry Point
1491
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
@@ -2122,7 +2194,19 @@ var StandaloneStackConfigSchema = z.object({
2122
2194
  databaseAuthToken: z.string().optional(),
2123
2195
  databaseDriver: z.enum(["sqlite", "sqlite-wasm", "turso", "memory", "postgres", "mongodb"]).optional(),
2124
2196
  environmentId: z.string().optional(),
2125
- artifactPath: z.string().optional()
2197
+ artifactPath: z.string().optional(),
2198
+ /**
2199
+ * Project root directory. When set (typically by the CLI after locating
2200
+ * `objectstack.config.ts`), the default sqlite database is placed under
2201
+ * `<projectRoot>/.objectstack/data/standalone.db` instead of the global
2202
+ * `~/.objectstack/data/standalone.db`. This keeps per-project data
2203
+ * scoped to the project folder so different examples / apps don't
2204
+ * share a single database by accident.
2205
+ *
2206
+ * Explicit `databaseUrl` / `OS_DATABASE_URL` / `OS_HOME` still take
2207
+ * precedence over this default.
2208
+ */
2209
+ projectRoot: z.string().optional()
2126
2210
  });
2127
2211
  function detectDriverFromUrl(dbUrl) {
2128
2212
  if (/^memory:\/\//i.test(dbUrl)) return "memory";
@@ -2147,7 +2231,7 @@ async function createStandaloneStack(config) {
2147
2231
  const environmentId = cfg.environmentId ?? process.env.OS_ENVIRONMENT_ID ?? "proj_local";
2148
2232
  const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? resolvePath2(cwd, "dist/objectstack.json");
2149
2233
  const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : resolvePath2(cwd, artifactPathInput);
2150
- const dbUrl = cfg.databaseUrl ?? process.env.OS_DATABASE_URL?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? `file:${resolvePath2(resolveObjectStackHome(), "data/standalone.db")}`;
2234
+ const dbUrl = cfg.databaseUrl ?? process.env.OS_DATABASE_URL?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? (process.env.OS_HOME?.trim() ? `file:${resolvePath2(resolveObjectStackHome(), "data/standalone.db")}` : cfg.projectRoot ? `file:${resolvePath2(cfg.projectRoot, ".objectstack/data/standalone.db")}` : `file:${resolvePath2(resolveObjectStackHome(), "data/standalone.db")}`);
2151
2235
  const dbAuthToken = cfg.databaseAuthToken ?? process.env.OS_DATABASE_AUTH_TOKEN?.trim() ?? process.env.TURSO_AUTH_TOKEN?.trim();
2152
2236
  const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
2153
2237
  const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
@@ -2365,6 +2449,13 @@ function toHeaders(input) {
2365
2449
  }
2366
2450
  return h;
2367
2451
  }
2452
+ function safeJsonParse2(s, fallback) {
2453
+ try {
2454
+ return JSON.parse(s);
2455
+ } catch {
2456
+ return fallback;
2457
+ }
2458
+ }
2368
2459
  async function tryFind(ql, object, where, limit = 100) {
2369
2460
  if (!ql || typeof ql.find !== "function") return [];
2370
2461
  try {
@@ -2380,6 +2471,7 @@ async function resolveExecutionContext(opts) {
2380
2471
  const ctx = {
2381
2472
  roles: [],
2382
2473
  permissions: [],
2474
+ systemPermissions: [],
2383
2475
  isSystem: false
2384
2476
  };
2385
2477
  let userId;
@@ -2417,8 +2509,12 @@ async function resolveExecutionContext(opts) {
2417
2509
  if (!userId) {
2418
2510
  try {
2419
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
+ }
2420
2516
  const headersInstance = toHeaders(headers);
2421
- const sessionData = await authService?.api?.getSession?.({ headers: headersInstance });
2517
+ const sessionData = await api?.getSession?.({ headers: headersInstance });
2422
2518
  userId = sessionData?.user?.id ?? sessionData?.session?.userId;
2423
2519
  tenantId = tenantId ?? sessionData?.session?.activeOrganizationId;
2424
2520
  ctx.accessToken = sessionData?.session?.token ?? ctx.accessToken;
@@ -2488,10 +2584,38 @@ async function resolveExecutionContext(opts) {
2488
2584
  { id: { $in: Array.from(psIds) } },
2489
2585
  500
2490
2586
  );
2587
+ const tabRank = {
2588
+ hidden: 0,
2589
+ default_off: 1,
2590
+ default_on: 2,
2591
+ visible: 3
2592
+ };
2593
+ const mergedTabs = {};
2491
2594
  for (const ps of psRows) {
2492
2595
  if (ps.name && !ctx.permissions.includes(ps.name)) {
2493
2596
  ctx.permissions.push(ps.name);
2494
2597
  }
2598
+ const sysPerms = typeof ps.system_permissions === "string" ? safeJsonParse2(ps.system_permissions, []) : ps.system_permissions ?? ps.systemPermissions;
2599
+ if (Array.isArray(sysPerms)) {
2600
+ for (const p of sysPerms) {
2601
+ if (typeof p === "string" && !ctx.systemPermissions.includes(p)) {
2602
+ ctx.systemPermissions.push(p);
2603
+ }
2604
+ }
2605
+ }
2606
+ const tabs = typeof ps.tab_permissions === "string" ? safeJsonParse2(ps.tab_permissions, {}) : ps.tab_permissions ?? ps.tabPermissions;
2607
+ if (tabs && typeof tabs === "object") {
2608
+ for (const [app, val] of Object.entries(tabs)) {
2609
+ if (typeof val !== "string" || !(val in tabRank)) continue;
2610
+ const cur = mergedTabs[app];
2611
+ if (!cur || tabRank[val] > tabRank[cur]) {
2612
+ mergedTabs[app] = val;
2613
+ }
2614
+ }
2615
+ }
2616
+ }
2617
+ if (Object.keys(mergedTabs).length > 0) {
2618
+ ctx.tabPermissions = mergedTabs;
2495
2619
  }
2496
2620
  }
2497
2621
  return ctx;
@@ -5235,7 +5359,7 @@ var _HttpDispatcher = class _HttpDispatcher {
5235
5359
  * Handle AI service routes (/ai/chat, /ai/models, /ai/conversations, etc.)
5236
5360
  * Resolves the AI service and its built-in route handlers, then dispatches.
5237
5361
  */
5238
- async handleAI(subPath, method, body, query, _context) {
5362
+ async handleAI(subPath, method, body, query, context) {
5239
5363
  let aiService;
5240
5364
  try {
5241
5365
  aiService = await this.resolveService("ai");
@@ -5279,7 +5403,23 @@ var _HttpDispatcher = class _HttpDispatcher {
5279
5403
  if (route.method !== method) continue;
5280
5404
  const params = matchRoute(route.path, fullPath);
5281
5405
  if (params === null) continue;
5282
- 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
+ });
5283
5423
  if (result.stream && result.events) {
5284
5424
  return {
5285
5425
  handled: true,
@@ -5784,13 +5924,22 @@ function resolveErrorReporter(ctx, override) {
5784
5924
  }
5785
5925
 
5786
5926
  // src/dispatcher-plugin.ts
5787
- function mountRouteOnServer(route, server, routePath, securityHeaders) {
5927
+ function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUser) {
5788
5928
  const handler = async (req, res) => {
5789
5929
  try {
5930
+ let user;
5931
+ if (resolveUser) {
5932
+ try {
5933
+ user = await resolveUser(req.headers ?? {});
5934
+ } catch {
5935
+ }
5936
+ }
5790
5937
  const result = await route.handler({
5791
5938
  body: req.body,
5792
5939
  params: req.params,
5793
- query: req.query
5940
+ query: req.query,
5941
+ headers: req.headers,
5942
+ user
5794
5943
  });
5795
5944
  if (result.stream && result.events) {
5796
5945
  res.status(result.status);
@@ -6527,6 +6676,32 @@ function createDispatcherPlugin(config = {}) {
6527
6676
  }
6528
6677
  }
6529
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
+ };
6530
6705
  const toScopedPath = (routePath) => {
6531
6706
  if (routePath.startsWith(prefix)) {
6532
6707
  const tail = routePath.slice(prefix.length);
@@ -6539,11 +6714,11 @@ function createDispatcherPlugin(config = {}) {
6539
6714
  const routePath = route.path.startsWith("/api/v1") ? route.path : `${prefix}${route.path}`;
6540
6715
  let count = 0;
6541
6716
  if (enableProjectScoping && projectResolution === "required") {
6542
- if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders)) count++;
6717
+ if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
6543
6718
  } else {
6544
- if (mountRouteOnServer(route, server, routePath, securityHeaders)) count++;
6719
+ if (mountRouteOnServer(route, server, routePath, securityHeaders, resolveRequestUser)) count++;
6545
6720
  if (enableProjectScoping) {
6546
- if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders)) count++;
6721
+ if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
6547
6722
  }
6548
6723
  }
6549
6724
  return count;
@@ -7605,39 +7780,7 @@ var ArtifactKernelFactory = class {
7605
7780
  // intentionally do NOT pass crossSubDomainCookies here
7606
7781
  // so cookies stay isolated per project subdomain.
7607
7782
  trustedOrigins: trustedOriginsList.length ? trustedOriginsList : void 0,
7608
- ...oidcProviders ? { oidcProviders } : {},
7609
- // Auto-provision a personal organization for every new
7610
- // user. SecurityPlugin's ObjectQL middleware does this
7611
- // for direct `ql.insert` calls, but better-auth's
7612
- // adapter writes through `dataEngine` directly,
7613
- // bypassing that middleware — so JIT-created SSO users
7614
- // would otherwise land on the empty "create
7615
- // organization" screen on first login.
7616
- databaseHooks: {
7617
- user: {
7618
- create: {
7619
- after: async (user) => {
7620
- try {
7621
- const ql = kernel.getService("objectql");
7622
- if (!ql) return;
7623
- const [{ ensureUserHasOrganization, cloneTenantSeedData }] = await Promise.all([
7624
- import("@objectstack/plugin-security")
7625
- ]);
7626
- await ensureUserHasOrganization(ql, user, {
7627
- logger: this.logger,
7628
- cloneSeedData: cloneTenantSeedData
7629
- });
7630
- } catch (e) {
7631
- this.logger.warn?.("[ArtifactKernelFactory] auto-org provisioning hook failed", {
7632
- environmentId,
7633
- userId: user?.id,
7634
- error: e?.message
7635
- });
7636
- }
7637
- }
7638
- }
7639
- }
7640
- }
7783
+ ...oidcProviders ? { oidcProviders } : {}
7641
7784
  }));
7642
7785
  if (oidcProviders) {
7643
7786
  this.logger.info?.("[ArtifactKernelFactory] platform SSO wired", {
@@ -7656,7 +7799,7 @@ var ArtifactKernelFactory = class {
7656
7799
  }
7657
7800
  try {
7658
7801
  const { SecurityPlugin } = await import("@objectstack/plugin-security");
7659
- const multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
7802
+ const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
7660
7803
  await kernel.use(new SecurityPlugin({ multiTenant }));
7661
7804
  } catch (err) {
7662
7805
  this.logger.warn?.("[ArtifactKernelFactory] SecurityPlugin not registered", {
@@ -7665,8 +7808,15 @@ var ArtifactKernelFactory = class {
7665
7808
  });
7666
7809
  }
7667
7810
  const projectName = project.hostname ?? environmentId;
7668
- const bundle = artifact.metadata;
7669
- 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;
7670
7820
  const packageId = sys?.packageId ?? sys?.package_id ?? bundle?.packageId;
7671
7821
  const i18nCfg = bundle?.i18n ?? sys?.i18n ?? {};
7672
7822
  const trArr = Array.isArray(bundle?.translations) ? bundle.translations : Array.isArray(sys?.translations) ? sys.translations : [];
@@ -8934,7 +9084,7 @@ var MarketplaceInstallLocalPlugin = class {
8934
9084
  }
8935
9085
  }
8936
9086
  if (opts.seedNow && datasets.length > 0) {
8937
- const multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
9087
+ const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
8938
9088
  try {
8939
9089
  const ql = ctx.getService("objectql");
8940
9090
  let metadata;