@objectstack/plugin-auth 6.7.0 → 6.8.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
@@ -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
  ...(() => {
@@ -830,6 +873,22 @@ var AuthManager = class {
830
873
  // never seed `sys_environment`) keep working: any lookup error
831
874
  // is treated as "no envs to protect".
832
875
  organizationHooks: {
876
+ // Gate fresh organization creation behind `OS_MULTI_ORG_ENABLED`.
877
+ // The plugin itself is always installed (so list/update/invite endpoints
878
+ // keep responding); only the `create` operation is denied when the
879
+ // deployment is provisioned in single-org mode. Default is enabled
880
+ // to preserve historical behaviour.
881
+ beforeCreateOrganization: async () => {
882
+ const flag = String(
883
+ globalThis?.process?.env?.OS_MULTI_ORG_ENABLED ?? "true"
884
+ ).toLowerCase();
885
+ if (flag === "false") {
886
+ const { APIError } = await import("better-auth/api");
887
+ throw new APIError("FORBIDDEN", {
888
+ message: "Creating additional organizations is disabled on this deployment."
889
+ });
890
+ }
891
+ },
833
892
  beforeUpdateOrganization: async ({ organization: organization2, member }) => {
834
893
  const newSlug = organization2?.slug;
835
894
  const orgId = member?.organizationId;
@@ -1203,20 +1262,40 @@ var AuthManager = class {
1203
1262
  }
1204
1263
  }
1205
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;
1206
1267
  const emailPassword = {
1207
1268
  enabled: emailPasswordConfig.enabled !== false,
1208
1269
  // Default to true
1209
- disableSignUp: emailPasswordConfig.disableSignUp ?? false,
1270
+ disableSignUp: disableSignUpFromEnv ?? emailPasswordConfig.disableSignUp ?? false,
1210
1271
  requireEmailVerification: emailPasswordConfig.requireEmailVerification ?? false
1211
1272
  };
1212
1273
  const pluginConfig = this.config.plugins ?? {};
1274
+ const multiOrgEnabled = String(
1275
+ globalThis?.process?.env?.OS_MULTI_ORG_ENABLED ?? "true"
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);
1213
1289
  const features = {
1214
1290
  twoFactor: pluginConfig.twoFactor ?? false,
1215
1291
  passkeys: pluginConfig.passkeys ?? false,
1216
1292
  magicLink: pluginConfig.magicLink ?? false,
1217
1293
  organization: pluginConfig.organization ?? true,
1294
+ multiOrgEnabled,
1218
1295
  oidcProvider: pluginConfig.oidcProvider ?? false,
1219
- deviceAuthorization: pluginConfig.deviceAuthorization ?? false
1296
+ deviceAuthorization: pluginConfig.deviceAuthorization ?? false,
1297
+ ...termsUrl ? { termsUrl } : {},
1298
+ ...privacyUrl ? { privacyUrl } : {}
1220
1299
  };
1221
1300
  return {
1222
1301
  emailPassword,
@@ -1324,7 +1403,7 @@ var AuthPlugin = class {
1324
1403
  // @objectstack/platform-objects/apps). plugin-auth is the natural
1325
1404
  // owner of its registration since it loads first among the trio
1326
1405
  // (auth + security + audit) that supplies the underlying objects.
1327
- apps: [SETUP_APP],
1406
+ apps: [SETUP_APP, STUDIO_APP],
1328
1407
  // List views for each Setup-nav object are defined on the schema
1329
1408
  // itself via the canonical `listViews` map (e.g.
1330
1409
  // sys_user.listViews.{all_users,unverified,two_factor}). Registering