@objectstack/plugin-auth 8.0.1 → 9.0.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.mjs CHANGED
@@ -5,7 +5,8 @@ import {
5
5
  SETUP_NAV_CONTRIBUTIONS,
6
6
  STUDIO_APP,
7
7
  ACCOUNT_APP,
8
- SystemOverviewDashboard
8
+ SystemOverviewDashboard,
9
+ SystemOverviewDatasets
9
10
  } from "@objectstack/platform-objects/apps";
10
11
  import { SysOrganizationDetailPage, SysUserDetailPage } from "@objectstack/platform-objects/pages";
11
12
 
@@ -511,6 +512,18 @@ function installWebContainerRequestStatePolyfill() {
511
512
  );
512
513
  }
513
514
  }
515
+ function readBooleanEnv(name, legacyName) {
516
+ const env = globalThis?.process?.env;
517
+ const raw = env?.[name] ?? (legacyName ? env?.[legacyName] : void 0);
518
+ if (raw == null) return void 0;
519
+ const normalized = String(raw).trim().toLowerCase();
520
+ return !["0", "false", "off", "no"].includes(normalized);
521
+ }
522
+ function readDisableSignUpEnv() {
523
+ const signupEnabled = readBooleanEnv("OS_AUTH_SIGNUP_ENABLED");
524
+ if (signupEnabled != null) return !signupEnabled;
525
+ return readBooleanEnv("OS_DISABLE_SIGNUP");
526
+ }
514
527
  var AuthManager = class {
515
528
  constructor(config) {
516
529
  this.auth = null;
@@ -594,13 +607,10 @@ var AuthManager = class {
594
607
  // Social / OAuth providers
595
608
  ...this.config.socialProviders ? { socialProviders: this.config.socialProviders } : {},
596
609
  // Email and password configuration.
597
- // `disableSignUp`: the env var `OS_DISABLE_SIGNUP=true` overrides
598
- // the config-file value so deployments can flip the toggle without
599
- // a code change (`getPublicConfig()` applies the same precedence so
600
- // `/auth/config` stays consistent with the server enforcement).
610
+ // `disableSignUp`: env overrides config/settings so deployments can
611
+ // lock the registration policy without relying on UI state.
601
612
  emailAndPassword: (() => {
602
- const disableSignUpEnv = globalThis?.process?.env?.OS_DISABLE_SIGNUP;
603
- const disableSignUpFromEnv = disableSignUpEnv != null ? String(disableSignUpEnv).toLowerCase() === "true" : void 0;
613
+ const disableSignUpFromEnv = readDisableSignUpEnv();
604
614
  const effectiveDisableSignUp = disableSignUpFromEnv ?? this.config.emailAndPassword?.disableSignUp;
605
615
  return {
606
616
  enabled: this.config.emailAndPassword?.enabled ?? true,
@@ -815,9 +825,10 @@ var AuthManager = class {
815
825
  const plugins = [];
816
826
  const oidcEnv = globalThis?.process?.env?.OS_OIDC_PROVIDER_ENABLED;
817
827
  const oidcFromEnv = oidcEnv != null ? String(oidcEnv).toLowerCase() === "true" : void 0;
828
+ const twoFactorFromEnv = readBooleanEnv("OS_AUTH_TWO_FACTOR");
818
829
  const enabled = {
819
830
  organization: pluginConfig.organization ?? true,
820
- twoFactor: pluginConfig.twoFactor ?? false,
831
+ twoFactor: twoFactorFromEnv ?? pluginConfig.twoFactor ?? false,
821
832
  passkeys: pluginConfig.passkeys ?? false,
822
833
  magicLink: pluginConfig.magicLink ?? false,
823
834
  oidcProvider: oidcFromEnv ?? pluginConfig.oidcProvider ?? false,
@@ -1088,16 +1099,19 @@ var AuthManager = class {
1088
1099
  });
1089
1100
  return (Array.isArray(members) ? members : []).some((m) => {
1090
1101
  const raw = typeof m?.role === "string" ? m.role : "";
1091
- const roles = raw.split(",").map((s) => s.trim().toLowerCase());
1092
- return roles.includes("owner") || roles.includes("admin");
1102
+ const roles2 = raw.split(",").map((s) => s.trim().toLowerCase());
1103
+ return roles2.includes("owner") || roles2.includes("admin");
1093
1104
  });
1094
1105
  } catch {
1095
1106
  return false;
1096
1107
  }
1097
1108
  };
1098
1109
  const promote = await isPlatformAdmin() || await isActiveOrgAdmin();
1099
- if (!promote) return { user, session };
1100
- return { user: { ...user, role: "admin" }, session };
1110
+ const storedRole = typeof user.role === "string" ? user.role : "";
1111
+ const roles = storedRole.split(",").map((s) => s.trim()).filter(Boolean);
1112
+ if (promote && !roles.includes("admin")) roles.push("admin");
1113
+ if (!promote) return { user: { ...user, roles }, session };
1114
+ return { user: { ...user, role: "admin", roles }, session };
1101
1115
  }));
1102
1116
  }
1103
1117
  return plugins;
@@ -1159,6 +1173,39 @@ var AuthManager = class {
1159
1173
  }
1160
1174
  this.config = { ...this.config, baseUrl: url };
1161
1175
  }
1176
+ /**
1177
+ * Merge runtime configuration into the manager.
1178
+ *
1179
+ * Settings-backed auth policy can change after the manager is constructed.
1180
+ * better-auth itself is created lazily, so changing config before the first
1181
+ * request is enough. If an instance already exists, reset it so the next
1182
+ * request rebuilds with the new policy.
1183
+ */
1184
+ applyConfigPatch(patch) {
1185
+ const next = {
1186
+ ...this.config,
1187
+ ...patch,
1188
+ ...patch.emailAndPassword ? {
1189
+ emailAndPassword: {
1190
+ ...this.config.emailAndPassword ?? {},
1191
+ ...patch.emailAndPassword
1192
+ }
1193
+ } : {},
1194
+ ...patch.plugins ? {
1195
+ plugins: {
1196
+ ...this.config.plugins ?? {},
1197
+ ...patch.plugins
1198
+ }
1199
+ } : {}
1200
+ };
1201
+ if ("socialProviders" in patch) {
1202
+ next.socialProviders = patch.socialProviders;
1203
+ }
1204
+ this.config = next;
1205
+ if (this.auth && !patch.authInstance) {
1206
+ this.auth = null;
1207
+ }
1208
+ }
1162
1209
  /**
1163
1210
  * Inject (or replace) the outbound email service used by better-auth
1164
1211
  * callbacks. Safe to call after construction but BEFORE the first
@@ -1291,8 +1338,7 @@ var AuthManager = class {
1291
1338
  }
1292
1339
  }
1293
1340
  const emailPasswordConfig = this.config.emailAndPassword ?? {};
1294
- const disableSignUpEnv = globalThis?.process?.env?.OS_DISABLE_SIGNUP;
1295
- const disableSignUpFromEnv = disableSignUpEnv != null ? String(disableSignUpEnv).toLowerCase() === "true" : void 0;
1341
+ const disableSignUpFromEnv = readDisableSignUpEnv();
1296
1342
  const emailPassword = {
1297
1343
  enabled: emailPasswordConfig.enabled !== false,
1298
1344
  // Default to true
@@ -1317,14 +1363,16 @@ var AuthManager = class {
1317
1363
  const privacyUrl = resolveLegalUrl(rawPrivacyUrl, DEFAULT_PRIVACY_URL);
1318
1364
  const oidcEnv = globalThis?.process?.env?.OS_OIDC_PROVIDER_ENABLED;
1319
1365
  const oidcFromEnv = oidcEnv != null ? String(oidcEnv).toLowerCase() === "true" : void 0;
1366
+ const twoFactorFromEnv = readBooleanEnv("OS_AUTH_TWO_FACTOR");
1320
1367
  const features = {
1321
- twoFactor: pluginConfig.twoFactor ?? false,
1368
+ twoFactor: twoFactorFromEnv ?? pluginConfig.twoFactor ?? false,
1322
1369
  passkeys: pluginConfig.passkeys ?? false,
1323
1370
  magicLink: pluginConfig.magicLink ?? false,
1324
1371
  organization: pluginConfig.organization ?? true,
1325
1372
  multiOrgEnabled,
1326
1373
  oidcProvider: oidcFromEnv ?? pluginConfig.oidcProvider ?? false,
1327
1374
  deviceAuthorization: pluginConfig.deviceAuthorization ?? false,
1375
+ admin: pluginConfig.admin ?? false,
1328
1376
  ...termsUrl ? { termsUrl } : {},
1329
1377
  ...privacyUrl ? { privacyUrl } : {}
1330
1378
  };
@@ -1443,6 +1491,30 @@ var AuthPlugin = class {
1443
1491
  ...options
1444
1492
  };
1445
1493
  }
1494
+ /**
1495
+ * Open-source provider fallback: enable Google sign-in from conventional
1496
+ * provider env vars when the application did not configure Google itself.
1497
+ * Enterprise / product packages can contribute richer provider sets through
1498
+ * the `auth:configure` hook below.
1499
+ */
1500
+ applyEnvSocialProviderFallbacks(config) {
1501
+ const env = globalThis?.process?.env;
1502
+ if (String(env?.OS_AUTH_GOOGLE_ENABLED ?? "true").toLowerCase() === "false") return;
1503
+ const googleClientId = env?.GOOGLE_CLIENT_ID;
1504
+ const googleClientSecret = env?.GOOGLE_CLIENT_SECRET;
1505
+ if (!googleClientId || !googleClientSecret) return;
1506
+ const socialProviders = {
1507
+ ...config.socialProviders ?? {}
1508
+ };
1509
+ if (!socialProviders.google) {
1510
+ socialProviders.google = {
1511
+ clientId: googleClientId,
1512
+ clientSecret: googleClientSecret,
1513
+ enabled: true
1514
+ };
1515
+ config.socialProviders = socialProviders;
1516
+ }
1517
+ }
1446
1518
  async init(ctx) {
1447
1519
  ctx.logger.info("Initializing Auth Plugin...");
1448
1520
  if (!this.options.secret) {
@@ -1452,10 +1524,14 @@ var AuthPlugin = class {
1452
1524
  if (!dataEngine) {
1453
1525
  ctx.logger.warn("No data engine service found - auth will use in-memory storage");
1454
1526
  }
1455
- this.authManager = new AuthManager({
1527
+ const authConfig = {
1456
1528
  ...this.options,
1457
1529
  dataEngine
1458
- });
1530
+ };
1531
+ this.applyEnvSocialProviderFallbacks(authConfig);
1532
+ await ctx.trigger("auth:configure", authConfig, ctx);
1533
+ this.configuredSocialProviders = authConfig.socialProviders ? { ...authConfig.socialProviders } : void 0;
1534
+ this.authManager = new AuthManager(authConfig);
1459
1535
  ctx.registerService("auth", this.authManager);
1460
1536
  ctx.getService("manifest").register({
1461
1537
  ...authPluginManifestHeader,
@@ -1484,7 +1560,9 @@ var AuthPlugin = class {
1484
1560
  // (e.g. legacy `users.view` had phone/status/active columns that do
1485
1561
  // not exist on sys_user). Schema-embedded listViews is the single
1486
1562
  // source of truth.
1487
- dashboards: [SystemOverviewDashboard]
1563
+ dashboards: [SystemOverviewDashboard],
1564
+ // ADR-0021 — datasets backing the System Overview dashboard's widgets.
1565
+ datasets: SystemOverviewDatasets
1488
1566
  });
1489
1567
  ctx.logger.info("Auth Plugin initialized successfully");
1490
1568
  }
@@ -1496,6 +1574,7 @@ var AuthPlugin = class {
1496
1574
  if (this.options.registerRoutes) {
1497
1575
  ctx.hook("kernel:ready", async () => {
1498
1576
  if (this.authManager) {
1577
+ await this.bindAuthSettings(ctx);
1499
1578
  try {
1500
1579
  const emailSvc = ctx.getService("email");
1501
1580
  if (emailSvc) {
@@ -1582,6 +1661,97 @@ var AuthPlugin = class {
1582
1661
  }
1583
1662
  ctx.logger.info("Auth Plugin started successfully");
1584
1663
  }
1664
+ /**
1665
+ * Bind the small open-source auth settings namespace to better-auth config.
1666
+ *
1667
+ * Only explicit settings values (stored or OS_AUTH_* env overrides) affect
1668
+ * runtime config. Manifest defaults are UI defaults and do not mask code or
1669
+ * deployment configuration.
1670
+ */
1671
+ async bindAuthSettings(ctx) {
1672
+ if (!this.authManager) return;
1673
+ let settings;
1674
+ try {
1675
+ settings = ctx.getService("settings");
1676
+ } catch {
1677
+ return;
1678
+ }
1679
+ if (!settings || typeof settings.getNamespace !== "function") return;
1680
+ const applySettings = async () => {
1681
+ if (!this.authManager) return;
1682
+ try {
1683
+ const payload = await settings.getNamespace("auth");
1684
+ const values = {};
1685
+ const sources = {};
1686
+ for (const [key, entry] of Object.entries(payload.values)) {
1687
+ values[key] = entry?.value;
1688
+ sources[key] = entry?.source;
1689
+ }
1690
+ const isExplicit = (key) => (sources[key] ?? "default") !== "default";
1691
+ const asBoolean = (value, fallback) => {
1692
+ if (typeof value === "boolean") return value;
1693
+ if (typeof value === "string") return value.toLowerCase() !== "false";
1694
+ if (typeof value === "number") return value !== 0;
1695
+ return fallback;
1696
+ };
1697
+ const asTrimmedString = (value) => {
1698
+ if (typeof value !== "string") return void 0;
1699
+ const trimmed = value.trim();
1700
+ return trimmed ? trimmed : void 0;
1701
+ };
1702
+ const patch = {};
1703
+ const emailAndPassword = {};
1704
+ if (isExplicit("email_password_enabled")) {
1705
+ emailAndPassword.enabled = asBoolean(values.email_password_enabled, true);
1706
+ }
1707
+ if (isExplicit("signup_enabled")) {
1708
+ emailAndPassword.disableSignUp = !asBoolean(values.signup_enabled, true);
1709
+ }
1710
+ if (isExplicit("require_email_verification")) {
1711
+ emailAndPassword.requireEmailVerification = asBoolean(
1712
+ values.require_email_verification,
1713
+ false
1714
+ );
1715
+ }
1716
+ if (Object.keys(emailAndPassword).length > 0) {
1717
+ patch.emailAndPassword = emailAndPassword;
1718
+ }
1719
+ if (isExplicit("google_enabled") || isExplicit("google_client_id") || isExplicit("google_client_secret")) {
1720
+ const socialProviders = {
1721
+ ...this.configuredSocialProviders ?? {}
1722
+ };
1723
+ const env = globalThis?.process?.env;
1724
+ const googleEnabledFromEnv = env?.OS_AUTH_GOOGLE_ENABLED != null ? asBoolean(env.OS_AUTH_GOOGLE_ENABLED, true) : void 0;
1725
+ const googleClientId = asTrimmedString(values.google_client_id) ?? env?.GOOGLE_CLIENT_ID;
1726
+ const googleClientSecret = asTrimmedString(values.google_client_secret) ?? env?.GOOGLE_CLIENT_SECRET;
1727
+ if (googleEnabledFromEnv ?? (isExplicit("google_enabled") ? asBoolean(values.google_enabled, true) : true)) {
1728
+ if (!socialProviders.google && googleClientId && googleClientSecret) {
1729
+ socialProviders.google = {
1730
+ clientId: googleClientId,
1731
+ clientSecret: googleClientSecret,
1732
+ enabled: true
1733
+ };
1734
+ }
1735
+ } else {
1736
+ delete socialProviders.google;
1737
+ }
1738
+ patch.socialProviders = Object.keys(socialProviders).length > 0 ? socialProviders : void 0;
1739
+ }
1740
+ if (Object.keys(patch).length > 0) {
1741
+ this.authManager.applyConfigPatch(patch);
1742
+ }
1743
+ } catch (err) {
1744
+ ctx.logger.warn("Auth: failed to apply auth settings: " + (err?.message ?? err));
1745
+ }
1746
+ };
1747
+ await applySettings();
1748
+ if (typeof settings.subscribe === "function") {
1749
+ settings.subscribe("auth", () => {
1750
+ void applySettings();
1751
+ });
1752
+ ctx.logger.info("Auth: bound to settings namespace=auth");
1753
+ }
1754
+ }
1585
1755
  async destroy() {
1586
1756
  this.authManager = null;
1587
1757
  }