@objectstack/plugin-auth 8.0.0 → 9.0.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.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,
@@ -1159,6 +1170,39 @@ var AuthManager = class {
1159
1170
  }
1160
1171
  this.config = { ...this.config, baseUrl: url };
1161
1172
  }
1173
+ /**
1174
+ * Merge runtime configuration into the manager.
1175
+ *
1176
+ * Settings-backed auth policy can change after the manager is constructed.
1177
+ * better-auth itself is created lazily, so changing config before the first
1178
+ * request is enough. If an instance already exists, reset it so the next
1179
+ * request rebuilds with the new policy.
1180
+ */
1181
+ applyConfigPatch(patch) {
1182
+ const next = {
1183
+ ...this.config,
1184
+ ...patch,
1185
+ ...patch.emailAndPassword ? {
1186
+ emailAndPassword: {
1187
+ ...this.config.emailAndPassword ?? {},
1188
+ ...patch.emailAndPassword
1189
+ }
1190
+ } : {},
1191
+ ...patch.plugins ? {
1192
+ plugins: {
1193
+ ...this.config.plugins ?? {},
1194
+ ...patch.plugins
1195
+ }
1196
+ } : {}
1197
+ };
1198
+ if ("socialProviders" in patch) {
1199
+ next.socialProviders = patch.socialProviders;
1200
+ }
1201
+ this.config = next;
1202
+ if (this.auth && !patch.authInstance) {
1203
+ this.auth = null;
1204
+ }
1205
+ }
1162
1206
  /**
1163
1207
  * Inject (or replace) the outbound email service used by better-auth
1164
1208
  * callbacks. Safe to call after construction but BEFORE the first
@@ -1291,8 +1335,7 @@ var AuthManager = class {
1291
1335
  }
1292
1336
  }
1293
1337
  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;
1338
+ const disableSignUpFromEnv = readDisableSignUpEnv();
1296
1339
  const emailPassword = {
1297
1340
  enabled: emailPasswordConfig.enabled !== false,
1298
1341
  // Default to true
@@ -1317,14 +1360,16 @@ var AuthManager = class {
1317
1360
  const privacyUrl = resolveLegalUrl(rawPrivacyUrl, DEFAULT_PRIVACY_URL);
1318
1361
  const oidcEnv = globalThis?.process?.env?.OS_OIDC_PROVIDER_ENABLED;
1319
1362
  const oidcFromEnv = oidcEnv != null ? String(oidcEnv).toLowerCase() === "true" : void 0;
1363
+ const twoFactorFromEnv = readBooleanEnv("OS_AUTH_TWO_FACTOR");
1320
1364
  const features = {
1321
- twoFactor: pluginConfig.twoFactor ?? false,
1365
+ twoFactor: twoFactorFromEnv ?? pluginConfig.twoFactor ?? false,
1322
1366
  passkeys: pluginConfig.passkeys ?? false,
1323
1367
  magicLink: pluginConfig.magicLink ?? false,
1324
1368
  organization: pluginConfig.organization ?? true,
1325
1369
  multiOrgEnabled,
1326
1370
  oidcProvider: oidcFromEnv ?? pluginConfig.oidcProvider ?? false,
1327
1371
  deviceAuthorization: pluginConfig.deviceAuthorization ?? false,
1372
+ admin: pluginConfig.admin ?? false,
1328
1373
  ...termsUrl ? { termsUrl } : {},
1329
1374
  ...privacyUrl ? { privacyUrl } : {}
1330
1375
  };
@@ -1443,6 +1488,30 @@ var AuthPlugin = class {
1443
1488
  ...options
1444
1489
  };
1445
1490
  }
1491
+ /**
1492
+ * Open-source provider fallback: enable Google sign-in from conventional
1493
+ * provider env vars when the application did not configure Google itself.
1494
+ * Enterprise / product packages can contribute richer provider sets through
1495
+ * the `auth:configure` hook below.
1496
+ */
1497
+ applyEnvSocialProviderFallbacks(config) {
1498
+ const env = globalThis?.process?.env;
1499
+ if (String(env?.OS_AUTH_GOOGLE_ENABLED ?? "true").toLowerCase() === "false") return;
1500
+ const googleClientId = env?.GOOGLE_CLIENT_ID;
1501
+ const googleClientSecret = env?.GOOGLE_CLIENT_SECRET;
1502
+ if (!googleClientId || !googleClientSecret) return;
1503
+ const socialProviders = {
1504
+ ...config.socialProviders ?? {}
1505
+ };
1506
+ if (!socialProviders.google) {
1507
+ socialProviders.google = {
1508
+ clientId: googleClientId,
1509
+ clientSecret: googleClientSecret,
1510
+ enabled: true
1511
+ };
1512
+ config.socialProviders = socialProviders;
1513
+ }
1514
+ }
1446
1515
  async init(ctx) {
1447
1516
  ctx.logger.info("Initializing Auth Plugin...");
1448
1517
  if (!this.options.secret) {
@@ -1452,10 +1521,14 @@ var AuthPlugin = class {
1452
1521
  if (!dataEngine) {
1453
1522
  ctx.logger.warn("No data engine service found - auth will use in-memory storage");
1454
1523
  }
1455
- this.authManager = new AuthManager({
1524
+ const authConfig = {
1456
1525
  ...this.options,
1457
1526
  dataEngine
1458
- });
1527
+ };
1528
+ this.applyEnvSocialProviderFallbacks(authConfig);
1529
+ await ctx.trigger("auth:configure", authConfig, ctx);
1530
+ this.configuredSocialProviders = authConfig.socialProviders ? { ...authConfig.socialProviders } : void 0;
1531
+ this.authManager = new AuthManager(authConfig);
1459
1532
  ctx.registerService("auth", this.authManager);
1460
1533
  ctx.getService("manifest").register({
1461
1534
  ...authPluginManifestHeader,
@@ -1484,7 +1557,9 @@ var AuthPlugin = class {
1484
1557
  // (e.g. legacy `users.view` had phone/status/active columns that do
1485
1558
  // not exist on sys_user). Schema-embedded listViews is the single
1486
1559
  // source of truth.
1487
- dashboards: [SystemOverviewDashboard]
1560
+ dashboards: [SystemOverviewDashboard],
1561
+ // ADR-0021 — datasets backing the System Overview dashboard's widgets.
1562
+ datasets: SystemOverviewDatasets
1488
1563
  });
1489
1564
  ctx.logger.info("Auth Plugin initialized successfully");
1490
1565
  }
@@ -1496,6 +1571,7 @@ var AuthPlugin = class {
1496
1571
  if (this.options.registerRoutes) {
1497
1572
  ctx.hook("kernel:ready", async () => {
1498
1573
  if (this.authManager) {
1574
+ await this.bindAuthSettings(ctx);
1499
1575
  try {
1500
1576
  const emailSvc = ctx.getService("email");
1501
1577
  if (emailSvc) {
@@ -1582,6 +1658,97 @@ var AuthPlugin = class {
1582
1658
  }
1583
1659
  ctx.logger.info("Auth Plugin started successfully");
1584
1660
  }
1661
+ /**
1662
+ * Bind the small open-source auth settings namespace to better-auth config.
1663
+ *
1664
+ * Only explicit settings values (stored or OS_AUTH_* env overrides) affect
1665
+ * runtime config. Manifest defaults are UI defaults and do not mask code or
1666
+ * deployment configuration.
1667
+ */
1668
+ async bindAuthSettings(ctx) {
1669
+ if (!this.authManager) return;
1670
+ let settings;
1671
+ try {
1672
+ settings = ctx.getService("settings");
1673
+ } catch {
1674
+ return;
1675
+ }
1676
+ if (!settings || typeof settings.getNamespace !== "function") return;
1677
+ const applySettings = async () => {
1678
+ if (!this.authManager) return;
1679
+ try {
1680
+ const payload = await settings.getNamespace("auth");
1681
+ const values = {};
1682
+ const sources = {};
1683
+ for (const [key, entry] of Object.entries(payload.values)) {
1684
+ values[key] = entry?.value;
1685
+ sources[key] = entry?.source;
1686
+ }
1687
+ const isExplicit = (key) => (sources[key] ?? "default") !== "default";
1688
+ const asBoolean = (value, fallback) => {
1689
+ if (typeof value === "boolean") return value;
1690
+ if (typeof value === "string") return value.toLowerCase() !== "false";
1691
+ if (typeof value === "number") return value !== 0;
1692
+ return fallback;
1693
+ };
1694
+ const asTrimmedString = (value) => {
1695
+ if (typeof value !== "string") return void 0;
1696
+ const trimmed = value.trim();
1697
+ return trimmed ? trimmed : void 0;
1698
+ };
1699
+ const patch = {};
1700
+ const emailAndPassword = {};
1701
+ if (isExplicit("email_password_enabled")) {
1702
+ emailAndPassword.enabled = asBoolean(values.email_password_enabled, true);
1703
+ }
1704
+ if (isExplicit("signup_enabled")) {
1705
+ emailAndPassword.disableSignUp = !asBoolean(values.signup_enabled, true);
1706
+ }
1707
+ if (isExplicit("require_email_verification")) {
1708
+ emailAndPassword.requireEmailVerification = asBoolean(
1709
+ values.require_email_verification,
1710
+ false
1711
+ );
1712
+ }
1713
+ if (Object.keys(emailAndPassword).length > 0) {
1714
+ patch.emailAndPassword = emailAndPassword;
1715
+ }
1716
+ if (isExplicit("google_enabled") || isExplicit("google_client_id") || isExplicit("google_client_secret")) {
1717
+ const socialProviders = {
1718
+ ...this.configuredSocialProviders ?? {}
1719
+ };
1720
+ const env = globalThis?.process?.env;
1721
+ const googleEnabledFromEnv = env?.OS_AUTH_GOOGLE_ENABLED != null ? asBoolean(env.OS_AUTH_GOOGLE_ENABLED, true) : void 0;
1722
+ const googleClientId = asTrimmedString(values.google_client_id) ?? env?.GOOGLE_CLIENT_ID;
1723
+ const googleClientSecret = asTrimmedString(values.google_client_secret) ?? env?.GOOGLE_CLIENT_SECRET;
1724
+ if (googleEnabledFromEnv ?? (isExplicit("google_enabled") ? asBoolean(values.google_enabled, true) : true)) {
1725
+ if (!socialProviders.google && googleClientId && googleClientSecret) {
1726
+ socialProviders.google = {
1727
+ clientId: googleClientId,
1728
+ clientSecret: googleClientSecret,
1729
+ enabled: true
1730
+ };
1731
+ }
1732
+ } else {
1733
+ delete socialProviders.google;
1734
+ }
1735
+ patch.socialProviders = Object.keys(socialProviders).length > 0 ? socialProviders : void 0;
1736
+ }
1737
+ if (Object.keys(patch).length > 0) {
1738
+ this.authManager.applyConfigPatch(patch);
1739
+ }
1740
+ } catch (err) {
1741
+ ctx.logger.warn("Auth: failed to apply auth settings: " + (err?.message ?? err));
1742
+ }
1743
+ };
1744
+ await applySettings();
1745
+ if (typeof settings.subscribe === "function") {
1746
+ settings.subscribe("auth", () => {
1747
+ void applySettings();
1748
+ });
1749
+ ctx.logger.info("Auth: bound to settings namespace=auth");
1750
+ }
1751
+ }
1585
1752
  async destroy() {
1586
1753
  this.authManager = null;
1587
1754
  }