@objectstack/runtime 9.11.0 → 10.2.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
@@ -117,6 +117,19 @@ interface StandaloneStackResult {
117
117
  requires?: string[];
118
118
  objects?: any[];
119
119
  manifest?: any;
120
+ /**
121
+ * App-declared RBAC metadata, surfaced so the CLI (`serve`/`dev`/`start`)
122
+ * can wire it without a host `objectstack.config.ts`. In particular the
123
+ * `serve` command reads `permissions[]` to honour an app-declared default
124
+ * profile (ADR-0056 D7 — `appDefaultProfileName` → SecurityPlugin
125
+ * `fallbackPermissionSet`) and reads both `roles[]` and `permissions[]` to
126
+ * register application org roles with Better-Auth. Without these the
127
+ * artifact-serve path silently fell back to the built-in `member_default`
128
+ * (owner-only), so an `isDefault` profile declared purely in app metadata
129
+ * was ignored under `objectstack dev`.
130
+ */
131
+ permissions?: any[];
132
+ roles?: any[];
120
133
  }
121
134
  declare function createStandaloneStack(config?: StandaloneStackConfig): Promise<StandaloneStackResult>;
122
135
 
package/dist/index.d.ts CHANGED
@@ -117,6 +117,19 @@ interface StandaloneStackResult {
117
117
  requires?: string[];
118
118
  objects?: any[];
119
119
  manifest?: any;
120
+ /**
121
+ * App-declared RBAC metadata, surfaced so the CLI (`serve`/`dev`/`start`)
122
+ * can wire it without a host `objectstack.config.ts`. In particular the
123
+ * `serve` command reads `permissions[]` to honour an app-declared default
124
+ * profile (ADR-0056 D7 — `appDefaultProfileName` → SecurityPlugin
125
+ * `fallbackPermissionSet`) and reads both `roles[]` and `permissions[]` to
126
+ * register application org roles with Better-Auth. Without these the
127
+ * artifact-serve path silently fell back to the built-in `member_default`
128
+ * (owner-only), so an `isDefault` profile declared purely in app metadata
129
+ * was ignored under `objectstack dev`.
130
+ */
131
+ permissions?: any[];
132
+ roles?: any[];
120
133
  }
121
134
  declare function createStandaloneStack(config?: StandaloneStackConfig): Promise<StandaloneStackResult>;
122
135
 
package/dist/index.js CHANGED
@@ -963,6 +963,65 @@ var init_app_plugin = __esm({
963
963
  error: err?.message ?? String(err)
964
964
  });
965
965
  }
966
+ try {
967
+ const dsDefs = this.bundle.datasources;
968
+ const dsList = Array.isArray(dsDefs) ? dsDefs : dsDefs && typeof dsDefs === "object" ? Object.entries(dsDefs).map(([name, def]) => ({ name, ...def })) : [];
969
+ if (dsList.length > 0) {
970
+ let connection;
971
+ try {
972
+ connection = ctx.getService("datasource-connection");
973
+ } catch {
974
+ connection = void 0;
975
+ }
976
+ if (typeof connection?.connectDeclared === "function") {
977
+ const objects = Array.isArray(this.bundle.objects) ? this.bundle.objects : [];
978
+ const results = await connection.connectDeclared({ datasources: dsList, objects });
979
+ const connected = results.filter((r) => r.status === "connected");
980
+ if (connected.length > 0) {
981
+ ctx.logger.info("Auto-connected declared datasources", {
982
+ appId,
983
+ connected: connected.map((r) => r.name)
984
+ });
985
+ }
986
+ } else {
987
+ ctx.logger.debug("No datasource-connection service \u2014 declared datasources stay metadata-only", { appId });
988
+ }
989
+ }
990
+ } catch (err) {
991
+ ctx.logger.error(
992
+ `[AppPlugin] declared-datasource auto-connect failed for app '${appId}': ${err?.message ?? String(err)}`
993
+ );
994
+ throw err;
995
+ }
996
+ try {
997
+ const metadata = ctx.getService("metadata");
998
+ if (typeof metadata?.registerInMemory === "function") {
999
+ const securityBundle = this.bundle.manifest ? { ...this.bundle.manifest, ...this.bundle } : this.bundle;
1000
+ const SECURITY_FIELDS = [
1001
+ ["roles", "role"],
1002
+ ["permissions", "permission"],
1003
+ ["sharingRules", "sharing_rule"],
1004
+ ["policies", "policy"]
1005
+ ];
1006
+ let count = 0;
1007
+ for (const [field, type] of SECURITY_FIELDS) {
1008
+ const arr = securityBundle?.[field];
1009
+ if (!Array.isArray(arr)) continue;
1010
+ for (const item of arr) {
1011
+ if (!item?.name) continue;
1012
+ metadata.registerInMemory(type, item.name, item);
1013
+ count += 1;
1014
+ }
1015
+ }
1016
+ if (count > 0) {
1017
+ ctx.logger.info("Registered stack-declared security metadata", { appId, count });
1018
+ }
1019
+ }
1020
+ } catch (err) {
1021
+ ctx.logger.warn("[AppPlugin] failed to register security metadata", {
1022
+ error: err?.message ?? String(err)
1023
+ });
1024
+ }
966
1025
  const stackBundle = this.bundle.default || this.bundle;
967
1026
  const runtime = stackBundle && typeof stackBundle.onEnable === "function" ? stackBundle : this.bundle;
968
1027
  if (runtime && typeof runtime.onEnable === "function") {
@@ -1501,55 +1560,55 @@ async function createStandaloneStack(config) {
1501
1560
  const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
1502
1561
  const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
1503
1562
  let driverPlugin;
1504
- if (dbDriver === "memory") {
1505
- const { InMemoryDriver } = await import("@objectstack/driver-memory");
1506
- driverPlugin = new DriverPlugin2(new InMemoryDriver());
1507
- } else if (dbDriver === "postgres") {
1508
- const { SqlDriver } = await import("@objectstack/driver-sql");
1509
- driverPlugin = new DriverPlugin2(
1510
- new SqlDriver({
1511
- client: "pg",
1512
- connection: dbUrl,
1513
- pool: { min: 0, max: 5 }
1514
- })
1515
- );
1516
- } else if (dbDriver === "mongodb") {
1517
- let MongoDBDriver;
1518
- try {
1519
- ({ MongoDBDriver } = await import("@objectstack/driver-mongodb"));
1520
- } catch (err) {
1521
- throw new Error(
1522
- `[StandaloneStack] mongodb URL detected but @objectstack/driver-mongodb is not installed. Add it as a dependency or pass an explicit driverPlugin. (${err?.message ?? err})`
1523
- );
1524
- }
1525
- driverPlugin = new DriverPlugin2(new MongoDBDriver({ url: dbUrl }));
1526
- } else if (dbDriver === "sqlite-wasm") {
1563
+ if (dbDriver === "sqlite-wasm") {
1527
1564
  const { SqliteWasmDriver } = await import("@objectstack/driver-sqlite-wasm");
1528
- const filename = dbUrl.replace(/^wasm-sqlite:(\/\/)?/i, "").replace(/^file:(\/\/)?/i, "");
1529
- if (filename && filename !== ":memory:") {
1565
+ const filename = dbUrl.replace(/^wasm-sqlite:(\/\/)?/i, "").replace(/^file:(\/\/)?/i, "") || ":memory:";
1566
+ if (filename !== ":memory:") {
1530
1567
  mkdirSync2(resolvePath2(filename, ".."), { recursive: true });
1531
1568
  }
1532
1569
  driverPlugin = new DriverPlugin2(
1533
1570
  new SqliteWasmDriver({
1534
- filename: filename || ":memory:",
1535
- persist: filename && filename !== ":memory:" ? "on-write" : void 0
1571
+ filename,
1572
+ persist: filename !== ":memory:" ? "on-write" : void 0
1536
1573
  })
1537
1574
  );
1538
1575
  } else {
1539
- const { SqlDriver } = await import("@objectstack/driver-sql");
1540
- const filename = dbUrl.replace(/^file:(\/\/)?/, "");
1541
- if (!filename || /^[a-z][a-z0-9+.-]*:\/\//i.test(filename)) {
1542
- throw new Error(
1543
- `[StandaloneStack] sqlite driver was selected but the URL does not look like a file path: "${dbUrl}". Use file:/path/to/db.sqlite, or set OS_DATABASE_DRIVER explicitly.`
1544
- );
1576
+ const { createDefaultDatasourceDriverFactory } = await import("@objectstack/service-datasource");
1577
+ let driverId;
1578
+ let driverConfig;
1579
+ if (dbDriver === "memory") {
1580
+ driverId = "memory";
1581
+ driverConfig = {};
1582
+ } else if (dbDriver === "postgres") {
1583
+ driverId = "postgres";
1584
+ driverConfig = { url: dbUrl };
1585
+ } else if (dbDriver === "mongodb") {
1586
+ driverId = "mongodb";
1587
+ driverConfig = { url: dbUrl };
1588
+ } else {
1589
+ driverId = "sqlite";
1590
+ const filename = dbUrl.replace(/^file:(\/\/)?/, "");
1591
+ if (!filename || /^[a-z][a-z0-9+.-]*:\/\//i.test(filename)) {
1592
+ throw new Error(
1593
+ `[StandaloneStack] sqlite driver was selected but the URL does not look like a file path: "${dbUrl}". Use file:/path/to/db.sqlite, or set OS_DATABASE_DRIVER explicitly.`
1594
+ );
1595
+ }
1596
+ mkdirSync2(resolvePath2(filename, ".."), { recursive: true });
1597
+ driverConfig = { filename };
1598
+ }
1599
+ let driverHandle;
1600
+ try {
1601
+ driverHandle = await createDefaultDatasourceDriverFactory().create({ driver: driverId, config: driverConfig });
1602
+ } catch (err) {
1603
+ if (dbDriver === "mongodb") {
1604
+ throw new Error(
1605
+ `[StandaloneStack] mongodb URL detected but @objectstack/driver-mongodb is not installed. Add it as a dependency or pass an explicit driverPlugin. (${err?.message ?? err})`
1606
+ );
1607
+ }
1608
+ throw err;
1545
1609
  }
1546
- mkdirSync2(resolvePath2(filename, ".."), { recursive: true });
1547
1610
  driverPlugin = new DriverPlugin2(
1548
- new SqlDriver({
1549
- client: "better-sqlite3",
1550
- connection: { filename },
1551
- useNullAsDefault: true
1552
- })
1611
+ driverHandle?.driver ?? driverHandle
1553
1612
  );
1554
1613
  }
1555
1614
  const artifactBundle = await loadArtifactBundle(artifactPath, {
@@ -1587,6 +1646,8 @@ async function createStandaloneStack(config) {
1587
1646
  const requires = Array.isArray(artifactBundle?.requires) ? artifactBundle.requires.filter((c) => typeof c === "string") : void 0;
1588
1647
  const objects = Array.isArray(artifactBundle?.objects) ? artifactBundle.objects : void 0;
1589
1648
  const manifest = artifactBundle?.manifest;
1649
+ const permissions = Array.isArray(artifactBundle?.permissions) ? artifactBundle.permissions : void 0;
1650
+ const roles = Array.isArray(artifactBundle?.roles) ? artifactBundle.roles : void 0;
1590
1651
  return {
1591
1652
  plugins,
1592
1653
  api: {
@@ -1595,7 +1656,9 @@ async function createStandaloneStack(config) {
1595
1656
  },
1596
1657
  ...requires ? { requires } : {},
1597
1658
  ...objects ? { objects } : {},
1598
- ...manifest ? { manifest } : {}
1659
+ ...manifest ? { manifest } : {},
1660
+ ...permissions ? { permissions } : {},
1661
+ ...roles ? { roles } : {}
1599
1662
  };
1600
1663
  }
1601
1664
  var StandaloneStackConfigSchema;
@@ -1997,25 +2060,33 @@ function coerceLocale(value) {
1997
2060
  const s = typeof value === "string" ? value.trim() : value != null ? String(value).trim() : "";
1998
2061
  return s || void 0;
1999
2062
  }
2063
+ function coerceCurrency(value) {
2064
+ const s = typeof value === "string" ? value.trim().toUpperCase() : "";
2065
+ return /^[A-Z]{3}$/.test(s) ? s : void 0;
2066
+ }
2000
2067
  async function resolveLocalization(opts, ql, sctx) {
2001
2068
  try {
2002
2069
  const settings = await opts.getService("settings");
2003
2070
  if (settings && typeof settings.get === "function") {
2004
- const [tzRes, localeRes] = await Promise.all([
2071
+ const [tzRes, localeRes, currencyRes] = await Promise.all([
2005
2072
  settings.get("localization", "timezone", sctx).catch(() => void 0),
2006
- settings.get("localization", "locale", sctx).catch(() => void 0)
2073
+ settings.get("localization", "locale", sctx).catch(() => void 0),
2074
+ settings.get("localization", "currency", sctx).catch(() => void 0)
2007
2075
  ]);
2008
2076
  const tz = coerceTimeZone(tzRes?.value);
2009
2077
  const locale = coerceLocale(localeRes?.value);
2010
- if (tz || locale) return { timezone: tz ?? "UTC", locale: locale ?? "en-US" };
2078
+ const currency = coerceCurrency(currencyRes?.value);
2079
+ if (tz || locale || currency) return { timezone: tz ?? "UTC", locale: locale ?? "en-US", currency };
2011
2080
  }
2012
2081
  } catch {
2013
2082
  }
2014
2083
  const tzRows = await tryFind(ql, "sys_setting", { namespace: "localization", key: "timezone", scope: "tenant" }, 1);
2015
2084
  const localeRows = await tryFind(ql, "sys_setting", { namespace: "localization", key: "locale", scope: "tenant" }, 1);
2085
+ const currencyRows = await tryFind(ql, "sys_setting", { namespace: "localization", key: "currency", scope: "tenant" }, 1);
2016
2086
  return {
2017
2087
  timezone: coerceTimeZone(tzRows[0]?.value) ?? "UTC",
2018
- locale: coerceLocale(localeRows[0]?.value) ?? "en-US"
2088
+ locale: coerceLocale(localeRows[0]?.value) ?? "en-US",
2089
+ currency: coerceCurrency(currencyRows[0]?.value)
2019
2090
  };
2020
2091
  }
2021
2092
  async function resolveExecutionContext(opts) {
@@ -2070,6 +2141,13 @@ async function resolveExecutionContext(opts) {
2070
2141
  }
2071
2142
  }
2072
2143
  }
2144
+ const userRoleRows = await tryFind(ql, "sys_user_role", { user_id: userId }, 200);
2145
+ for (const ur of userRoleRows) {
2146
+ const org = ur.organization_id ?? null;
2147
+ if (org && tenantId && org !== tenantId) continue;
2148
+ const r = ur.role;
2149
+ if (typeof r === "string" && r && !ctx.roles.includes(r)) ctx.roles.push(r);
2150
+ }
2073
2151
  if (tenantId) {
2074
2152
  const orgMembers = await tryFind(
2075
2153
  ql,
@@ -2156,6 +2234,7 @@ async function resolveExecutionContext(opts) {
2156
2234
  const localization = await resolveLocalization(opts, ql, { tenantId, userId });
2157
2235
  ctx.timezone = localization.timezone;
2158
2236
  ctx.locale = localization.locale;
2237
+ if (localization.currency) ctx.currency = localization.currency;
2159
2238
  return ctx;
2160
2239
  }
2161
2240
  function isPermissionDeniedError(e) {
@@ -2653,7 +2732,7 @@ var _HttpDispatcher = class _HttpDispatcher {
2653
2732
  */
2654
2733
  async enforceProjectMembership(context, path) {
2655
2734
  if (!this.enforceMembership) return null;
2656
- const skipPaths = ["/auth", "/cloud", "/health", "/discovery"];
2735
+ const skipPaths = ["/auth", "/cloud", "/health", "/ready", "/discovery"];
2657
2736
  if (skipPaths.some((p) => path.startsWith(p))) return null;
2658
2737
  if (/(^|\/)share-links\/[^/]+\/(resolve|messages)$/.test(path)) return null;
2659
2738
  const environmentId = context.environmentId;
@@ -4470,6 +4549,10 @@ var _HttpDispatcher = class _HttpDispatcher {
4470
4549
  })
4471
4550
  };
4472
4551
  }
4552
+ if (cleanPath === "/ready" && method === "GET") {
4553
+ const state = typeof this.kernel?.getState === "function" ? this.kernel.getState() : "running";
4554
+ return state === "running" ? { handled: true, response: this.success({ status: "ready", state }) } : { handled: true, response: this.error("Service not ready", 503, { state }) };
4555
+ }
4473
4556
  if (cleanPath.startsWith("/auth")) {
4474
4557
  return this.handleAuth(cleanPath.substring(5), method, body, context);
4475
4558
  }