@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/LICENSE +93 -202
- package/README.md +2 -1
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +120 -42
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +121 -42
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|