@objectstack/plugin-auth 6.7.1 → 6.8.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
@@ -1,6 +1,7 @@
1
1
  // src/auth-plugin.ts
2
2
  import {
3
3
  SETUP_APP,
4
+ STUDIO_APP,
4
5
  SystemOverviewDashboard,
5
6
  SetupAppTranslations
6
7
  } from "@objectstack/platform-objects/apps";
@@ -527,6 +528,7 @@ var AuthManager = class {
527
528
  */
528
529
  async createAuthInstance() {
529
530
  const { betterAuth } = await import("better-auth");
531
+ const { createAuthMiddleware } = await import("better-auth/api");
530
532
  const plugins = await this.buildPluginList();
531
533
  const passwordHasher = await this.resolvePasswordHasher();
532
534
  const betterAuthConfig = {
@@ -585,46 +587,55 @@ var AuthManager = class {
585
587
  },
586
588
  // Social / OAuth providers
587
589
  ...this.config.socialProviders ? { socialProviders: this.config.socialProviders } : {},
588
- // Email and password configuration
589
- emailAndPassword: {
590
- enabled: this.config.emailAndPassword?.enabled ?? true,
591
- ...passwordHasher ? { password: passwordHasher } : {},
592
- ...this.config.emailAndPassword?.disableSignUp != null ? { disableSignUp: this.config.emailAndPassword.disableSignUp } : {},
593
- ...this.config.emailAndPassword?.requireEmailVerification != null ? { requireEmailVerification: this.config.emailAndPassword.requireEmailVerification } : {},
594
- ...this.config.emailAndPassword?.minPasswordLength != null ? { minPasswordLength: this.config.emailAndPassword.minPasswordLength } : {},
595
- ...this.config.emailAndPassword?.maxPasswordLength != null ? { maxPasswordLength: this.config.emailAndPassword.maxPasswordLength } : {},
596
- ...this.config.emailAndPassword?.resetPasswordTokenExpiresIn != null ? { resetPasswordTokenExpiresIn: this.config.emailAndPassword.resetPasswordTokenExpiresIn } : {},
597
- ...this.config.emailAndPassword?.autoSignIn != null ? { autoSignIn: this.config.emailAndPassword.autoSignIn } : {},
598
- ...this.config.emailAndPassword?.revokeSessionsOnPasswordReset != null ? { revokeSessionsOnPasswordReset: this.config.emailAndPassword.revokeSessionsOnPasswordReset } : {},
599
- sendResetPassword: async ({ user, url, token }) => {
600
- const email = this.getEmailService();
601
- if (!email) {
602
- console.warn(
603
- `[AuthManager] Password-reset requested for ${user.email} but no email service is wired. URL: ${url}`
604
- );
605
- return;
606
- }
607
- const ttlSec = this.config.emailAndPassword?.resetPasswordTokenExpiresIn ?? 60 * 60;
608
- try {
609
- await email.sendTemplate({
610
- template: "auth.password_reset",
611
- to: { address: user.email, ...user.name ? { name: user.name } : {} },
612
- data: {
613
- user: { name: user.name || user.email, email: user.email, id: user.id },
614
- resetUrl: url,
615
- token,
616
- expiresInMinutes: Math.round(ttlSec / 60),
617
- appName: this.getAppName()
618
- },
619
- relatedObject: "sys_user",
620
- relatedId: user.id
621
- });
622
- } catch (err) {
623
- console.error(`[AuthManager] sendResetPassword failed: ${err?.message ?? err}`);
624
- throw err;
590
+ // Email and password configuration.
591
+ // `disableSignUp`: the env var `OS_DISABLE_SIGNUP=true` overrides
592
+ // the config-file value so deployments can flip the toggle without
593
+ // a code change (`getPublicConfig()` applies the same precedence so
594
+ // `/auth/config` stays consistent with the server enforcement).
595
+ emailAndPassword: (() => {
596
+ const disableSignUpEnv = globalThis?.process?.env?.OS_DISABLE_SIGNUP;
597
+ const disableSignUpFromEnv = disableSignUpEnv != null ? String(disableSignUpEnv).toLowerCase() === "true" : void 0;
598
+ const effectiveDisableSignUp = disableSignUpFromEnv ?? this.config.emailAndPassword?.disableSignUp;
599
+ return {
600
+ enabled: this.config.emailAndPassword?.enabled ?? true,
601
+ ...passwordHasher ? { password: passwordHasher } : {},
602
+ ...effectiveDisableSignUp != null ? { disableSignUp: effectiveDisableSignUp } : {},
603
+ ...this.config.emailAndPassword?.requireEmailVerification != null ? { requireEmailVerification: this.config.emailAndPassword.requireEmailVerification } : {},
604
+ ...this.config.emailAndPassword?.minPasswordLength != null ? { minPasswordLength: this.config.emailAndPassword.minPasswordLength } : {},
605
+ ...this.config.emailAndPassword?.maxPasswordLength != null ? { maxPasswordLength: this.config.emailAndPassword.maxPasswordLength } : {},
606
+ ...this.config.emailAndPassword?.resetPasswordTokenExpiresIn != null ? { resetPasswordTokenExpiresIn: this.config.emailAndPassword.resetPasswordTokenExpiresIn } : {},
607
+ ...this.config.emailAndPassword?.autoSignIn != null ? { autoSignIn: this.config.emailAndPassword.autoSignIn } : {},
608
+ ...this.config.emailAndPassword?.revokeSessionsOnPasswordReset != null ? { revokeSessionsOnPasswordReset: this.config.emailAndPassword.revokeSessionsOnPasswordReset } : {},
609
+ sendResetPassword: async ({ user, url, token }) => {
610
+ const email = this.getEmailService();
611
+ if (!email) {
612
+ console.warn(
613
+ `[AuthManager] Password-reset requested for ${user.email} but no email service is wired. URL: ${url}`
614
+ );
615
+ return;
616
+ }
617
+ const ttlSec = this.config.emailAndPassword?.resetPasswordTokenExpiresIn ?? 60 * 60;
618
+ try {
619
+ await email.sendTemplate({
620
+ template: "auth.password_reset",
621
+ to: { address: user.email, ...user.name ? { name: user.name } : {} },
622
+ data: {
623
+ user: { name: user.name || user.email, email: user.email, id: user.id },
624
+ resetUrl: url,
625
+ token,
626
+ expiresInMinutes: Math.round(ttlSec / 60),
627
+ appName: this.getAppName()
628
+ },
629
+ relatedObject: "sys_user",
630
+ relatedId: user.id
631
+ });
632
+ } catch (err) {
633
+ console.error(`[AuthManager] sendResetPassword failed: ${err?.message ?? err}`);
634
+ throw err;
635
+ }
625
636
  }
626
- }
627
- },
637
+ };
638
+ })(),
628
639
  // Email verification
629
640
  ...this.config.emailVerification || this.config.emailService ? {
630
641
  emailVerification: {
@@ -676,6 +687,38 @@ var AuthManager = class {
676
687
  // for SSO JIT-provisioning too, unlike kernel-level ObjectQL
677
688
  // middleware which better-auth's adapter bypasses).
678
689
  ...this.config.databaseHooks ? { databaseHooks: this.config.databaseHooks } : {},
690
+ // Bootstrap bypass for `disableSignUp`. The first-run owner wizard
691
+ // (`/_account/setup`) calls `POST /auth/sign-up/email` to create
692
+ // the very first user — if `OS_DISABLE_SIGNUP=true` is set on a
693
+ // fresh install we'd lock the operator out of their own instance.
694
+ // Solution: when the request hits `/sign-up/email` AND no users
695
+ // exist yet, temporarily flip `disableSignUp` off for *this*
696
+ // request's context. Once the owner is created the next request
697
+ // sees `userCount > 0` and the toggle is enforced again.
698
+ hooks: {
699
+ before: createAuthMiddleware(async (ctx) => {
700
+ if (ctx?.path !== "/sign-up/email") return;
701
+ const ep = ctx?.context?.options?.emailAndPassword;
702
+ if (!ep?.disableSignUp) return;
703
+ try {
704
+ const adapter = ctx.context.adapter;
705
+ const existing = await adapter.findOne({ model: "user", where: [] });
706
+ if (!existing) {
707
+ ctx.context.__osDisableSignUpOrig = ep.disableSignUp;
708
+ ep.disableSignUp = false;
709
+ }
710
+ } catch {
711
+ }
712
+ }),
713
+ after: createAuthMiddleware(async (ctx) => {
714
+ if (ctx?.path !== "/sign-up/email") return;
715
+ const ep = ctx?.context?.options?.emailAndPassword;
716
+ if (ep && ctx.context.__osDisableSignUpOrig !== void 0) {
717
+ ep.disableSignUp = ctx.context.__osDisableSignUpOrig;
718
+ delete ctx.context.__osDisableSignUpOrig;
719
+ }
720
+ })
721
+ },
679
722
  // Trusted origins for CSRF protection (supports wildcards like "https://*.example.com")
680
723
  // Auto-includes origins from CORS_ORIGIN env var so CORS and CSRF stay in sync.
681
724
  ...(() => {
@@ -1219,16 +1262,30 @@ var AuthManager = class {
1219
1262
  }
1220
1263
  }
1221
1264
  const emailPasswordConfig = this.config.emailAndPassword ?? {};
1265
+ const disableSignUpEnv = globalThis?.process?.env?.OS_DISABLE_SIGNUP;
1266
+ const disableSignUpFromEnv = disableSignUpEnv != null ? String(disableSignUpEnv).toLowerCase() === "true" : void 0;
1222
1267
  const emailPassword = {
1223
1268
  enabled: emailPasswordConfig.enabled !== false,
1224
1269
  // Default to true
1225
- disableSignUp: emailPasswordConfig.disableSignUp ?? false,
1270
+ disableSignUp: disableSignUpFromEnv ?? emailPasswordConfig.disableSignUp ?? false,
1226
1271
  requireEmailVerification: emailPasswordConfig.requireEmailVerification ?? false
1227
1272
  };
1228
1273
  const pluginConfig = this.config.plugins ?? {};
1229
1274
  const multiOrgEnabled = String(
1230
1275
  globalThis?.process?.env?.OS_MULTI_ORG_ENABLED ?? "true"
1231
1276
  ).toLowerCase() !== "false";
1277
+ const DEFAULT_TERMS_URL = "https://objectstack.ai/terms";
1278
+ const DEFAULT_PRIVACY_URL = "https://objectstack.ai/privacy";
1279
+ const rawTermsUrl = globalThis?.process?.env?.OS_TERMS_URL;
1280
+ const rawPrivacyUrl = globalThis?.process?.env?.OS_PRIVACY_URL;
1281
+ const resolveLegalUrl = (raw, fallback) => {
1282
+ if (typeof raw !== "string") return fallback;
1283
+ const trimmed = raw.trim();
1284
+ if (trimmed === "") return void 0;
1285
+ return trimmed;
1286
+ };
1287
+ const termsUrl = resolveLegalUrl(rawTermsUrl, DEFAULT_TERMS_URL);
1288
+ const privacyUrl = resolveLegalUrl(rawPrivacyUrl, DEFAULT_PRIVACY_URL);
1232
1289
  const features = {
1233
1290
  twoFactor: pluginConfig.twoFactor ?? false,
1234
1291
  passkeys: pluginConfig.passkeys ?? false,
@@ -1236,7 +1293,9 @@ var AuthManager = class {
1236
1293
  organization: pluginConfig.organization ?? true,
1237
1294
  multiOrgEnabled,
1238
1295
  oidcProvider: pluginConfig.oidcProvider ?? false,
1239
- deviceAuthorization: pluginConfig.deviceAuthorization ?? false
1296
+ deviceAuthorization: pluginConfig.deviceAuthorization ?? false,
1297
+ ...termsUrl ? { termsUrl } : {},
1298
+ ...privacyUrl ? { privacyUrl } : {}
1240
1299
  };
1241
1300
  return {
1242
1301
  emailPassword,
@@ -1344,7 +1403,7 @@ var AuthPlugin = class {
1344
1403
  // @objectstack/platform-objects/apps). plugin-auth is the natural
1345
1404
  // owner of its registration since it loads first among the trio
1346
1405
  // (auth + security + audit) that supplies the underlying objects.
1347
- apps: [SETUP_APP],
1406
+ apps: [SETUP_APP, STUDIO_APP],
1348
1407
  // List views for each Setup-nav object are defined on the schema
1349
1408
  // itself via the canonical `listViews` map (e.g.
1350
1409
  // sys_user.listViews.{all_users,unverified,two_factor}). Registering