@objectstack/plugin-auth 8.0.1 → 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/README.md CHANGED
@@ -24,7 +24,7 @@ Authentication & Identity Plugin for ObjectStack.
24
24
  - ✅ **Session Management** - Automatic session handling
25
25
  - ✅ **Password Reset** - Email-based password reset flow
26
26
  - ✅ **Email Verification** - Email verification workflow
27
- - **2FA** - Two-factor authentication (when enabled)
27
+ - ⚠️ **2FA backend wiring** - better-auth two-factor plugin can be enabled for custom UIs
28
28
  - ✅ **Passkeys** - WebAuthn/Passkey support (when enabled)
29
29
  - ✅ **Magic Links** - Passwordless authentication (when enabled)
30
30
  - ✅ **Organizations** - Multi-tenant support (when enabled)
@@ -91,7 +91,7 @@ new AuthPlugin({
91
91
  baseUrl: 'http://localhost:3000',
92
92
  plugins: {
93
93
  organization: true, // Enable organization/teams
94
- twoFactor: true, // Enable 2FA
94
+ twoFactor: true, // Enable backend 2FA endpoints for a custom UI
95
95
  passkeys: true, // Enable passkey support
96
96
  }
97
97
  })
@@ -105,7 +105,7 @@ The plugin accepts configuration via `AuthConfig` schema from `@objectstack/spec
105
105
  - `baseUrl` - Base URL for auth routes
106
106
  - `databaseUrl` - Database connection string
107
107
  - `providers` - Array of OAuth provider configurations
108
- - `plugins` - Enable additional auth features (organization, 2FA, passkeys, magic link)
108
+ - `plugins` - Enable additional auth features (organization, backend 2FA, passkeys, magic link)
109
109
  - `session` - Session configuration (expiry, update frequency)
110
110
 
111
111
  ## API Routes
@@ -132,9 +132,13 @@ The plugin forwards all requests under `/api/v1/auth/*` directly to better-auth'
132
132
  - `GET /api/v1/auth/authorize/[provider]` - Start OAuth flow
133
133
  - `GET /api/v1/auth/callback/[provider]` - OAuth callback
134
134
 
135
- ### 2FA (when enabled)
136
- - `POST /api/v1/auth/two-factor/enable` - Enable 2FA
137
- - `POST /api/v1/auth/two-factor/verify` - Verify 2FA code
135
+ ### 2FA backend (when enabled)
136
+ - `POST /api/v1/auth/two-factor/enable` - Start 2FA enrollment
137
+ - `POST /api/v1/auth/two-factor/verify-totp` - Verify a TOTP code
138
+
139
+ The open-source Console does not yet provide a complete TOTP enrollment,
140
+ login challenge, and backup-code recovery flow. Keep `plugins.twoFactor`
141
+ disabled unless your application UI handles that full flow.
138
142
 
139
143
  ### Passkeys (when enabled)
140
144
  - `POST /api/v1/auth/passkey/register` - Register a passkey
@@ -158,7 +162,7 @@ This package provides authentication services powered by better-auth. Current im
158
162
  6. ✅ Direct request forwarding to better-auth handler
159
163
  7. ✅ Full better-auth API support
160
164
  8. ✅ OAuth providers (configurable)
161
- 9. 2FA, passkeys, magic links (configurable)
165
+ 9. ⚠️ Backend 2FA wiring, passkeys, magic links (configurable; custom UI required for 2FA)
162
166
  10. ✅ ObjectQL-based database implementation (no ORM required)
163
167
 
164
168
  ### Architecture
package/dist/index.d.mts CHANGED
@@ -85,9 +85,25 @@ declare class AuthPlugin implements Plugin {
85
85
  dependencies: string[];
86
86
  private options;
87
87
  private authManager;
88
+ private configuredSocialProviders;
88
89
  constructor(options?: AuthPluginOptions);
90
+ /**
91
+ * Open-source provider fallback: enable Google sign-in from conventional
92
+ * provider env vars when the application did not configure Google itself.
93
+ * Enterprise / product packages can contribute richer provider sets through
94
+ * the `auth:configure` hook below.
95
+ */
96
+ private applyEnvSocialProviderFallbacks;
89
97
  init(ctx: PluginContext): Promise<void>;
90
98
  start(ctx: PluginContext): Promise<void>;
99
+ /**
100
+ * Bind the small open-source auth settings namespace to better-auth config.
101
+ *
102
+ * Only explicit settings values (stored or OS_AUTH_* env overrides) affect
103
+ * runtime config. Manifest defaults are UI defaults and do not mask code or
104
+ * deployment configuration.
105
+ */
106
+ private bindAuthSettings;
91
107
  destroy(): Promise<void>;
92
108
  /**
93
109
  * Dev-only admin bootstrap.
@@ -307,6 +323,15 @@ declare class AuthManager {
307
323
  * a warning is emitted.
308
324
  */
309
325
  setRuntimeBaseUrl(url: string): void;
326
+ /**
327
+ * Merge runtime configuration into the manager.
328
+ *
329
+ * Settings-backed auth policy can change after the manager is constructed.
330
+ * better-auth itself is created lazily, so changing config before the first
331
+ * request is enough. If an instance already exists, reset it so the next
332
+ * request rebuilds with the new policy.
333
+ */
334
+ applyConfigPatch(patch: Partial<AuthManagerOptions>): void;
310
335
  /**
311
336
  * Inject (or replace) the outbound email service used by better-auth
312
337
  * callbacks. Safe to call after construction but BEFORE the first
@@ -393,6 +418,7 @@ declare class AuthManager {
393
418
  multiOrgEnabled: boolean;
394
419
  oidcProvider: boolean;
395
420
  deviceAuthorization: boolean;
421
+ admin: boolean;
396
422
  };
397
423
  };
398
424
  /**
package/dist/index.d.ts CHANGED
@@ -85,9 +85,25 @@ declare class AuthPlugin implements Plugin {
85
85
  dependencies: string[];
86
86
  private options;
87
87
  private authManager;
88
+ private configuredSocialProviders;
88
89
  constructor(options?: AuthPluginOptions);
90
+ /**
91
+ * Open-source provider fallback: enable Google sign-in from conventional
92
+ * provider env vars when the application did not configure Google itself.
93
+ * Enterprise / product packages can contribute richer provider sets through
94
+ * the `auth:configure` hook below.
95
+ */
96
+ private applyEnvSocialProviderFallbacks;
89
97
  init(ctx: PluginContext): Promise<void>;
90
98
  start(ctx: PluginContext): Promise<void>;
99
+ /**
100
+ * Bind the small open-source auth settings namespace to better-auth config.
101
+ *
102
+ * Only explicit settings values (stored or OS_AUTH_* env overrides) affect
103
+ * runtime config. Manifest defaults are UI defaults and do not mask code or
104
+ * deployment configuration.
105
+ */
106
+ private bindAuthSettings;
91
107
  destroy(): Promise<void>;
92
108
  /**
93
109
  * Dev-only admin bootstrap.
@@ -307,6 +323,15 @@ declare class AuthManager {
307
323
  * a warning is emitted.
308
324
  */
309
325
  setRuntimeBaseUrl(url: string): void;
326
+ /**
327
+ * Merge runtime configuration into the manager.
328
+ *
329
+ * Settings-backed auth policy can change after the manager is constructed.
330
+ * better-auth itself is created lazily, so changing config before the first
331
+ * request is enough. If an instance already exists, reset it so the next
332
+ * request rebuilds with the new policy.
333
+ */
334
+ applyConfigPatch(patch: Partial<AuthManagerOptions>): void;
310
335
  /**
311
336
  * Inject (or replace) the outbound email service used by better-auth
312
337
  * callbacks. Safe to call after construction but BEFORE the first
@@ -393,6 +418,7 @@ declare class AuthManager {
393
418
  multiOrgEnabled: boolean;
394
419
  oidcProvider: boolean;
395
420
  deviceAuthorization: boolean;
421
+ admin: boolean;
396
422
  };
397
423
  };
398
424
  /**
package/dist/index.js CHANGED
@@ -575,6 +575,18 @@ function installWebContainerRequestStatePolyfill() {
575
575
  );
576
576
  }
577
577
  }
578
+ function readBooleanEnv(name, legacyName) {
579
+ const env = globalThis?.process?.env;
580
+ const raw = env?.[name] ?? (legacyName ? env?.[legacyName] : void 0);
581
+ if (raw == null) return void 0;
582
+ const normalized = String(raw).trim().toLowerCase();
583
+ return !["0", "false", "off", "no"].includes(normalized);
584
+ }
585
+ function readDisableSignUpEnv() {
586
+ const signupEnabled = readBooleanEnv("OS_AUTH_SIGNUP_ENABLED");
587
+ if (signupEnabled != null) return !signupEnabled;
588
+ return readBooleanEnv("OS_DISABLE_SIGNUP");
589
+ }
578
590
  var AuthManager = class {
579
591
  constructor(config) {
580
592
  this.auth = null;
@@ -658,13 +670,10 @@ var AuthManager = class {
658
670
  // Social / OAuth providers
659
671
  ...this.config.socialProviders ? { socialProviders: this.config.socialProviders } : {},
660
672
  // Email and password configuration.
661
- // `disableSignUp`: the env var `OS_DISABLE_SIGNUP=true` overrides
662
- // the config-file value so deployments can flip the toggle without
663
- // a code change (`getPublicConfig()` applies the same precedence so
664
- // `/auth/config` stays consistent with the server enforcement).
673
+ // `disableSignUp`: env overrides config/settings so deployments can
674
+ // lock the registration policy without relying on UI state.
665
675
  emailAndPassword: (() => {
666
- const disableSignUpEnv = globalThis?.process?.env?.OS_DISABLE_SIGNUP;
667
- const disableSignUpFromEnv = disableSignUpEnv != null ? String(disableSignUpEnv).toLowerCase() === "true" : void 0;
676
+ const disableSignUpFromEnv = readDisableSignUpEnv();
668
677
  const effectiveDisableSignUp = disableSignUpFromEnv ?? this.config.emailAndPassword?.disableSignUp;
669
678
  return {
670
679
  enabled: this.config.emailAndPassword?.enabled ?? true,
@@ -879,9 +888,10 @@ var AuthManager = class {
879
888
  const plugins = [];
880
889
  const oidcEnv = globalThis?.process?.env?.OS_OIDC_PROVIDER_ENABLED;
881
890
  const oidcFromEnv = oidcEnv != null ? String(oidcEnv).toLowerCase() === "true" : void 0;
891
+ const twoFactorFromEnv = readBooleanEnv("OS_AUTH_TWO_FACTOR");
882
892
  const enabled = {
883
893
  organization: pluginConfig.organization ?? true,
884
- twoFactor: pluginConfig.twoFactor ?? false,
894
+ twoFactor: twoFactorFromEnv ?? pluginConfig.twoFactor ?? false,
885
895
  passkeys: pluginConfig.passkeys ?? false,
886
896
  magicLink: pluginConfig.magicLink ?? false,
887
897
  oidcProvider: oidcFromEnv ?? pluginConfig.oidcProvider ?? false,
@@ -1223,6 +1233,39 @@ var AuthManager = class {
1223
1233
  }
1224
1234
  this.config = { ...this.config, baseUrl: url };
1225
1235
  }
1236
+ /**
1237
+ * Merge runtime configuration into the manager.
1238
+ *
1239
+ * Settings-backed auth policy can change after the manager is constructed.
1240
+ * better-auth itself is created lazily, so changing config before the first
1241
+ * request is enough. If an instance already exists, reset it so the next
1242
+ * request rebuilds with the new policy.
1243
+ */
1244
+ applyConfigPatch(patch) {
1245
+ const next = {
1246
+ ...this.config,
1247
+ ...patch,
1248
+ ...patch.emailAndPassword ? {
1249
+ emailAndPassword: {
1250
+ ...this.config.emailAndPassword ?? {},
1251
+ ...patch.emailAndPassword
1252
+ }
1253
+ } : {},
1254
+ ...patch.plugins ? {
1255
+ plugins: {
1256
+ ...this.config.plugins ?? {},
1257
+ ...patch.plugins
1258
+ }
1259
+ } : {}
1260
+ };
1261
+ if ("socialProviders" in patch) {
1262
+ next.socialProviders = patch.socialProviders;
1263
+ }
1264
+ this.config = next;
1265
+ if (this.auth && !patch.authInstance) {
1266
+ this.auth = null;
1267
+ }
1268
+ }
1226
1269
  /**
1227
1270
  * Inject (or replace) the outbound email service used by better-auth
1228
1271
  * callbacks. Safe to call after construction but BEFORE the first
@@ -1355,8 +1398,7 @@ var AuthManager = class {
1355
1398
  }
1356
1399
  }
1357
1400
  const emailPasswordConfig = this.config.emailAndPassword ?? {};
1358
- const disableSignUpEnv = globalThis?.process?.env?.OS_DISABLE_SIGNUP;
1359
- const disableSignUpFromEnv = disableSignUpEnv != null ? String(disableSignUpEnv).toLowerCase() === "true" : void 0;
1401
+ const disableSignUpFromEnv = readDisableSignUpEnv();
1360
1402
  const emailPassword = {
1361
1403
  enabled: emailPasswordConfig.enabled !== false,
1362
1404
  // Default to true
@@ -1381,14 +1423,16 @@ var AuthManager = class {
1381
1423
  const privacyUrl = resolveLegalUrl(rawPrivacyUrl, DEFAULT_PRIVACY_URL);
1382
1424
  const oidcEnv = globalThis?.process?.env?.OS_OIDC_PROVIDER_ENABLED;
1383
1425
  const oidcFromEnv = oidcEnv != null ? String(oidcEnv).toLowerCase() === "true" : void 0;
1426
+ const twoFactorFromEnv = readBooleanEnv("OS_AUTH_TWO_FACTOR");
1384
1427
  const features = {
1385
- twoFactor: pluginConfig.twoFactor ?? false,
1428
+ twoFactor: twoFactorFromEnv ?? pluginConfig.twoFactor ?? false,
1386
1429
  passkeys: pluginConfig.passkeys ?? false,
1387
1430
  magicLink: pluginConfig.magicLink ?? false,
1388
1431
  organization: pluginConfig.organization ?? true,
1389
1432
  multiOrgEnabled,
1390
1433
  oidcProvider: oidcFromEnv ?? pluginConfig.oidcProvider ?? false,
1391
1434
  deviceAuthorization: pluginConfig.deviceAuthorization ?? false,
1435
+ admin: pluginConfig.admin ?? false,
1392
1436
  ...termsUrl ? { termsUrl } : {},
1393
1437
  ...privacyUrl ? { privacyUrl } : {}
1394
1438
  };
@@ -1488,6 +1532,30 @@ var AuthPlugin = class {
1488
1532
  ...options
1489
1533
  };
1490
1534
  }
1535
+ /**
1536
+ * Open-source provider fallback: enable Google sign-in from conventional
1537
+ * provider env vars when the application did not configure Google itself.
1538
+ * Enterprise / product packages can contribute richer provider sets through
1539
+ * the `auth:configure` hook below.
1540
+ */
1541
+ applyEnvSocialProviderFallbacks(config) {
1542
+ const env = globalThis?.process?.env;
1543
+ if (String(env?.OS_AUTH_GOOGLE_ENABLED ?? "true").toLowerCase() === "false") return;
1544
+ const googleClientId = env?.GOOGLE_CLIENT_ID;
1545
+ const googleClientSecret = env?.GOOGLE_CLIENT_SECRET;
1546
+ if (!googleClientId || !googleClientSecret) return;
1547
+ const socialProviders = {
1548
+ ...config.socialProviders ?? {}
1549
+ };
1550
+ if (!socialProviders.google) {
1551
+ socialProviders.google = {
1552
+ clientId: googleClientId,
1553
+ clientSecret: googleClientSecret,
1554
+ enabled: true
1555
+ };
1556
+ config.socialProviders = socialProviders;
1557
+ }
1558
+ }
1491
1559
  async init(ctx) {
1492
1560
  ctx.logger.info("Initializing Auth Plugin...");
1493
1561
  if (!this.options.secret) {
@@ -1497,10 +1565,14 @@ var AuthPlugin = class {
1497
1565
  if (!dataEngine) {
1498
1566
  ctx.logger.warn("No data engine service found - auth will use in-memory storage");
1499
1567
  }
1500
- this.authManager = new AuthManager({
1568
+ const authConfig = {
1501
1569
  ...this.options,
1502
1570
  dataEngine
1503
- });
1571
+ };
1572
+ this.applyEnvSocialProviderFallbacks(authConfig);
1573
+ await ctx.trigger("auth:configure", authConfig, ctx);
1574
+ this.configuredSocialProviders = authConfig.socialProviders ? { ...authConfig.socialProviders } : void 0;
1575
+ this.authManager = new AuthManager(authConfig);
1504
1576
  ctx.registerService("auth", this.authManager);
1505
1577
  ctx.getService("manifest").register({
1506
1578
  ...authPluginManifestHeader,
@@ -1529,7 +1601,9 @@ var AuthPlugin = class {
1529
1601
  // (e.g. legacy `users.view` had phone/status/active columns that do
1530
1602
  // not exist on sys_user). Schema-embedded listViews is the single
1531
1603
  // source of truth.
1532
- dashboards: [import_apps.SystemOverviewDashboard]
1604
+ dashboards: [import_apps.SystemOverviewDashboard],
1605
+ // ADR-0021 — datasets backing the System Overview dashboard's widgets.
1606
+ datasets: import_apps.SystemOverviewDatasets
1533
1607
  });
1534
1608
  ctx.logger.info("Auth Plugin initialized successfully");
1535
1609
  }
@@ -1541,6 +1615,7 @@ var AuthPlugin = class {
1541
1615
  if (this.options.registerRoutes) {
1542
1616
  ctx.hook("kernel:ready", async () => {
1543
1617
  if (this.authManager) {
1618
+ await this.bindAuthSettings(ctx);
1544
1619
  try {
1545
1620
  const emailSvc = ctx.getService("email");
1546
1621
  if (emailSvc) {
@@ -1627,6 +1702,97 @@ var AuthPlugin = class {
1627
1702
  }
1628
1703
  ctx.logger.info("Auth Plugin started successfully");
1629
1704
  }
1705
+ /**
1706
+ * Bind the small open-source auth settings namespace to better-auth config.
1707
+ *
1708
+ * Only explicit settings values (stored or OS_AUTH_* env overrides) affect
1709
+ * runtime config. Manifest defaults are UI defaults and do not mask code or
1710
+ * deployment configuration.
1711
+ */
1712
+ async bindAuthSettings(ctx) {
1713
+ if (!this.authManager) return;
1714
+ let settings;
1715
+ try {
1716
+ settings = ctx.getService("settings");
1717
+ } catch {
1718
+ return;
1719
+ }
1720
+ if (!settings || typeof settings.getNamespace !== "function") return;
1721
+ const applySettings = async () => {
1722
+ if (!this.authManager) return;
1723
+ try {
1724
+ const payload = await settings.getNamespace("auth");
1725
+ const values = {};
1726
+ const sources = {};
1727
+ for (const [key, entry] of Object.entries(payload.values)) {
1728
+ values[key] = entry?.value;
1729
+ sources[key] = entry?.source;
1730
+ }
1731
+ const isExplicit = (key) => (sources[key] ?? "default") !== "default";
1732
+ const asBoolean = (value, fallback) => {
1733
+ if (typeof value === "boolean") return value;
1734
+ if (typeof value === "string") return value.toLowerCase() !== "false";
1735
+ if (typeof value === "number") return value !== 0;
1736
+ return fallback;
1737
+ };
1738
+ const asTrimmedString = (value) => {
1739
+ if (typeof value !== "string") return void 0;
1740
+ const trimmed = value.trim();
1741
+ return trimmed ? trimmed : void 0;
1742
+ };
1743
+ const patch = {};
1744
+ const emailAndPassword = {};
1745
+ if (isExplicit("email_password_enabled")) {
1746
+ emailAndPassword.enabled = asBoolean(values.email_password_enabled, true);
1747
+ }
1748
+ if (isExplicit("signup_enabled")) {
1749
+ emailAndPassword.disableSignUp = !asBoolean(values.signup_enabled, true);
1750
+ }
1751
+ if (isExplicit("require_email_verification")) {
1752
+ emailAndPassword.requireEmailVerification = asBoolean(
1753
+ values.require_email_verification,
1754
+ false
1755
+ );
1756
+ }
1757
+ if (Object.keys(emailAndPassword).length > 0) {
1758
+ patch.emailAndPassword = emailAndPassword;
1759
+ }
1760
+ if (isExplicit("google_enabled") || isExplicit("google_client_id") || isExplicit("google_client_secret")) {
1761
+ const socialProviders = {
1762
+ ...this.configuredSocialProviders ?? {}
1763
+ };
1764
+ const env = globalThis?.process?.env;
1765
+ const googleEnabledFromEnv = env?.OS_AUTH_GOOGLE_ENABLED != null ? asBoolean(env.OS_AUTH_GOOGLE_ENABLED, true) : void 0;
1766
+ const googleClientId = asTrimmedString(values.google_client_id) ?? env?.GOOGLE_CLIENT_ID;
1767
+ const googleClientSecret = asTrimmedString(values.google_client_secret) ?? env?.GOOGLE_CLIENT_SECRET;
1768
+ if (googleEnabledFromEnv ?? (isExplicit("google_enabled") ? asBoolean(values.google_enabled, true) : true)) {
1769
+ if (!socialProviders.google && googleClientId && googleClientSecret) {
1770
+ socialProviders.google = {
1771
+ clientId: googleClientId,
1772
+ clientSecret: googleClientSecret,
1773
+ enabled: true
1774
+ };
1775
+ }
1776
+ } else {
1777
+ delete socialProviders.google;
1778
+ }
1779
+ patch.socialProviders = Object.keys(socialProviders).length > 0 ? socialProviders : void 0;
1780
+ }
1781
+ if (Object.keys(patch).length > 0) {
1782
+ this.authManager.applyConfigPatch(patch);
1783
+ }
1784
+ } catch (err) {
1785
+ ctx.logger.warn("Auth: failed to apply auth settings: " + (err?.message ?? err));
1786
+ }
1787
+ };
1788
+ await applySettings();
1789
+ if (typeof settings.subscribe === "function") {
1790
+ settings.subscribe("auth", () => {
1791
+ void applySettings();
1792
+ });
1793
+ ctx.logger.info("Auth: bound to settings namespace=auth");
1794
+ }
1795
+ }
1630
1796
  async destroy() {
1631
1797
  this.authManager = null;
1632
1798
  }