@objectstack/plugin-auth 4.0.5 → 4.1.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.d.mts +112 -1
- package/dist/index.d.ts +112 -1
- package/dist/index.js +372 -37
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +370 -44
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -31,6 +31,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
AUTH_ACCOUNT_CONFIG: () => AUTH_ACCOUNT_CONFIG,
|
|
34
|
+
AUTH_ADMIN_SESSION_FIELDS: () => AUTH_ADMIN_SESSION_FIELDS,
|
|
35
|
+
AUTH_ADMIN_USER_FIELDS: () => AUTH_ADMIN_USER_FIELDS,
|
|
34
36
|
AUTH_DEVICE_CODE_SCHEMA: () => AUTH_DEVICE_CODE_SCHEMA,
|
|
35
37
|
AUTH_INVITATION_SCHEMA: () => AUTH_INVITATION_SCHEMA,
|
|
36
38
|
AUTH_JWKS_SCHEMA: () => AUTH_JWKS_SCHEMA,
|
|
@@ -52,6 +54,7 @@ __export(index_exports, {
|
|
|
52
54
|
AUTH_VERIFICATION_CONFIG: () => AUTH_VERIFICATION_CONFIG,
|
|
53
55
|
AuthManager: () => AuthManager,
|
|
54
56
|
AuthPlugin: () => AuthPlugin,
|
|
57
|
+
buildAdminPluginSchema: () => buildAdminPluginSchema,
|
|
55
58
|
buildDeviceAuthorizationPluginSchema: () => buildDeviceAuthorizationPluginSchema,
|
|
56
59
|
buildJwtPluginSchema: () => buildJwtPluginSchema,
|
|
57
60
|
buildOauthProviderPluginSchema: () => buildOauthProviderPluginSchema,
|
|
@@ -79,6 +82,41 @@ var AUTH_MODEL_TO_PROTOCOL = {
|
|
|
79
82
|
function resolveProtocolName(model) {
|
|
80
83
|
return AUTH_MODEL_TO_PROTOCOL[model] ?? model;
|
|
81
84
|
}
|
|
85
|
+
var LEGACY_DATETIME_FIELDS_BY_MODEL = {
|
|
86
|
+
user: ["created_at", "updated_at"],
|
|
87
|
+
session: ["expires_at", "created_at", "updated_at"],
|
|
88
|
+
account: [
|
|
89
|
+
"access_token_expires_at",
|
|
90
|
+
"refresh_token_expires_at",
|
|
91
|
+
"created_at",
|
|
92
|
+
"updated_at"
|
|
93
|
+
],
|
|
94
|
+
verification: ["expires_at", "created_at", "updated_at"]
|
|
95
|
+
};
|
|
96
|
+
var NUMERIC_STRING_RE = /^-?\d+(\.\d+)?$/;
|
|
97
|
+
function normaliseLegacyDate(value) {
|
|
98
|
+
if (typeof value !== "string") return value;
|
|
99
|
+
if (!NUMERIC_STRING_RE.test(value)) return value;
|
|
100
|
+
const n = parseFloat(value);
|
|
101
|
+
if (!Number.isFinite(n)) return value;
|
|
102
|
+
if (Math.abs(n) < 1e10) return value;
|
|
103
|
+
const d = new Date(n);
|
|
104
|
+
if (Number.isNaN(d.getTime())) return value;
|
|
105
|
+
return d.toISOString();
|
|
106
|
+
}
|
|
107
|
+
function normaliseLegacyDates(model, record) {
|
|
108
|
+
if (!record) return record;
|
|
109
|
+
const cols = LEGACY_DATETIME_FIELDS_BY_MODEL[model];
|
|
110
|
+
if (!cols) return record;
|
|
111
|
+
for (const col of cols) {
|
|
112
|
+
if (col in record) {
|
|
113
|
+
record[col] = normaliseLegacyDate(
|
|
114
|
+
record[col]
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return record;
|
|
119
|
+
}
|
|
82
120
|
function convertWhere(where) {
|
|
83
121
|
const filter = {};
|
|
84
122
|
for (const condition of where) {
|
|
@@ -107,20 +145,24 @@ function createObjectQLAdapterFactory(dataEngine) {
|
|
|
107
145
|
return (0, import_adapters.createAdapterFactory)({
|
|
108
146
|
config: {
|
|
109
147
|
adapterId: "objectql",
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
148
|
+
// We let better-auth handle Date↔string and boolean↔0/1 conversion so
|
|
149
|
+
// that values land in the underlying SQL driver as primitive strings
|
|
150
|
+
// and integers. Some drivers (e.g. libsql over the HTTP transport)
|
|
151
|
+
// otherwise mangle `Date` objects into `"<epoch>.0"` strings that
|
|
152
|
+
// break the client-side session parser.
|
|
153
|
+
supportsBooleans: false,
|
|
154
|
+
supportsDates: false,
|
|
113
155
|
supportsJSON: true
|
|
114
156
|
},
|
|
115
157
|
adapter: () => ({
|
|
116
158
|
create: async ({ model, data, select: _select }) => {
|
|
117
159
|
const result = await dataEngine.insert(model, data);
|
|
118
|
-
return result;
|
|
160
|
+
return normaliseLegacyDates(model, result);
|
|
119
161
|
},
|
|
120
162
|
findOne: async ({ model, where, select, join: _join }) => {
|
|
121
163
|
const filter = convertWhere(where);
|
|
122
164
|
const result = await dataEngine.findOne(model, { where: filter, fields: select });
|
|
123
|
-
return result ? result : null;
|
|
165
|
+
return result ? normaliseLegacyDates(model, result) : null;
|
|
124
166
|
},
|
|
125
167
|
findMany: async ({ model, where, limit, offset, sortBy, join: _join }) => {
|
|
126
168
|
const filter = where ? convertWhere(where) : {};
|
|
@@ -131,7 +173,7 @@ function createObjectQLAdapterFactory(dataEngine) {
|
|
|
131
173
|
offset,
|
|
132
174
|
orderBy
|
|
133
175
|
});
|
|
134
|
-
return results;
|
|
176
|
+
return results.map((r) => normaliseLegacyDates(model, r));
|
|
135
177
|
},
|
|
136
178
|
count: async ({ model, where }) => {
|
|
137
179
|
const filter = where ? convertWhere(where) : {};
|
|
@@ -142,7 +184,7 @@ function createObjectQLAdapterFactory(dataEngine) {
|
|
|
142
184
|
const record = await dataEngine.findOne(model, { where: filter });
|
|
143
185
|
if (!record) return null;
|
|
144
186
|
const result = await dataEngine.update(model, { ...update, id: record.id });
|
|
145
|
-
return result ? result : null;
|
|
187
|
+
return result ? normaliseLegacyDates(model, result) : null;
|
|
146
188
|
},
|
|
147
189
|
updateMany: async ({ model, where, update }) => {
|
|
148
190
|
const filter = convertWhere(where);
|
|
@@ -339,6 +381,13 @@ var AUTH_TWO_FACTOR_SCHEMA = {
|
|
|
339
381
|
var AUTH_TWO_FACTOR_USER_FIELDS = {
|
|
340
382
|
twoFactorEnabled: "two_factor_enabled"
|
|
341
383
|
};
|
|
384
|
+
var AUTH_ADMIN_USER_FIELDS = {
|
|
385
|
+
banReason: "ban_reason",
|
|
386
|
+
banExpires: "ban_expires"
|
|
387
|
+
};
|
|
388
|
+
var AUTH_ADMIN_SESSION_FIELDS = {
|
|
389
|
+
impersonatedBy: "impersonated_by"
|
|
390
|
+
};
|
|
342
391
|
var AUTH_OAUTH_CLIENT_SCHEMA = {
|
|
343
392
|
modelName: import_system2.SystemObjectName.OAUTH_APPLICATION,
|
|
344
393
|
// 'sys_oauth_application'
|
|
@@ -422,6 +471,16 @@ function buildTwoFactorPluginSchema() {
|
|
|
422
471
|
}
|
|
423
472
|
};
|
|
424
473
|
}
|
|
474
|
+
function buildAdminPluginSchema() {
|
|
475
|
+
return {
|
|
476
|
+
user: {
|
|
477
|
+
fields: AUTH_ADMIN_USER_FIELDS
|
|
478
|
+
},
|
|
479
|
+
session: {
|
|
480
|
+
fields: AUTH_ADMIN_SESSION_FIELDS
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
}
|
|
425
484
|
function buildOrganizationPluginSchema() {
|
|
426
485
|
return {
|
|
427
486
|
organization: AUTH_ORGANIZATION_SCHEMA,
|
|
@@ -503,7 +562,41 @@ var AuthManager = class {
|
|
|
503
562
|
...AUTH_USER_CONFIG
|
|
504
563
|
},
|
|
505
564
|
account: {
|
|
506
|
-
...AUTH_ACCOUNT_CONFIG
|
|
565
|
+
...AUTH_ACCOUNT_CONFIG,
|
|
566
|
+
// Allow OIDC/OAuth callbacks to implicitly link the incoming
|
|
567
|
+
// identity to a pre-existing local user when the emails match.
|
|
568
|
+
//
|
|
569
|
+
// ObjectStack's platform SSO ("objectstack-cloud" provider) is the
|
|
570
|
+
// canonical case: cloud is the IdP for every project, so a user
|
|
571
|
+
// arriving via SSO is — by construction — the same person who was
|
|
572
|
+
// auto-seeded as the project owner when the project was created.
|
|
573
|
+
// Without trusting the provider, better-auth's safety check rejects
|
|
574
|
+
// the link with `error=account_not_linked` because the seeded user
|
|
575
|
+
// row has `emailVerified=false` (no actual verification ever runs
|
|
576
|
+
// in the IdP-mediated flow). See packages/plugins/plugin-auth/
|
|
577
|
+
// node_modules/better-auth/dist/oauth2/link-account.mjs:22.
|
|
578
|
+
//
|
|
579
|
+
// Custom-deployment consumers can extend the trusted set via
|
|
580
|
+
// `config.account.accountLinking.trustedProviders`; we always
|
|
581
|
+
// include `objectstack-cloud` because it is the platform IdP.
|
|
582
|
+
accountLinking: {
|
|
583
|
+
enabled: true,
|
|
584
|
+
// better-auth's account-linking gate has TWO independent clauses
|
|
585
|
+
// (see link-account.mjs:22). Trusting the provider only satisfies
|
|
586
|
+
// the first clause; the second — `requireLocalEmailVerified &&
|
|
587
|
+
// !dbUser.user.emailVerified` — still blocks linking when the
|
|
588
|
+
// pre-existing local user row has `emailVerified=false` (the
|
|
589
|
+
// default for owner-seeded rows). Disabling the local-email gate
|
|
590
|
+
// is safe here because the OAuth side is what we actually trust:
|
|
591
|
+
// the incoming identity was verified by the IdP. Consumers who
|
|
592
|
+
// need the stricter behavior can override via config.
|
|
593
|
+
requireLocalEmailVerified: false,
|
|
594
|
+
...this.config?.account?.accountLinking ?? {},
|
|
595
|
+
trustedProviders: Array.from(/* @__PURE__ */ new Set([
|
|
596
|
+
"objectstack-cloud",
|
|
597
|
+
...this.config?.account?.accountLinking?.trustedProviders ?? []
|
|
598
|
+
]))
|
|
599
|
+
}
|
|
507
600
|
},
|
|
508
601
|
verification: {
|
|
509
602
|
...AUTH_VERIFICATION_CONFIG
|
|
@@ -519,15 +612,71 @@ var AuthManager = class {
|
|
|
519
612
|
...this.config.emailAndPassword?.maxPasswordLength != null ? { maxPasswordLength: this.config.emailAndPassword.maxPasswordLength } : {},
|
|
520
613
|
...this.config.emailAndPassword?.resetPasswordTokenExpiresIn != null ? { resetPasswordTokenExpiresIn: this.config.emailAndPassword.resetPasswordTokenExpiresIn } : {},
|
|
521
614
|
...this.config.emailAndPassword?.autoSignIn != null ? { autoSignIn: this.config.emailAndPassword.autoSignIn } : {},
|
|
522
|
-
...this.config.emailAndPassword?.revokeSessionsOnPasswordReset != null ? { revokeSessionsOnPasswordReset: this.config.emailAndPassword.revokeSessionsOnPasswordReset } : {}
|
|
615
|
+
...this.config.emailAndPassword?.revokeSessionsOnPasswordReset != null ? { revokeSessionsOnPasswordReset: this.config.emailAndPassword.revokeSessionsOnPasswordReset } : {},
|
|
616
|
+
sendResetPassword: async ({ user, url, token }) => {
|
|
617
|
+
const email = this.getEmailService();
|
|
618
|
+
if (!email) {
|
|
619
|
+
console.warn(
|
|
620
|
+
`[AuthManager] Password-reset requested for ${user.email} but no email service is wired. URL: ${url}`
|
|
621
|
+
);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const ttlSec = this.config.emailAndPassword?.resetPasswordTokenExpiresIn ?? 60 * 60;
|
|
625
|
+
try {
|
|
626
|
+
await email.sendTemplate({
|
|
627
|
+
template: "auth.password_reset",
|
|
628
|
+
to: { address: user.email, ...user.name ? { name: user.name } : {} },
|
|
629
|
+
data: {
|
|
630
|
+
user: { name: user.name || user.email, email: user.email, id: user.id },
|
|
631
|
+
resetUrl: url,
|
|
632
|
+
token,
|
|
633
|
+
expiresInMinutes: Math.round(ttlSec / 60),
|
|
634
|
+
appName: this.getAppName()
|
|
635
|
+
},
|
|
636
|
+
relatedObject: "sys_user",
|
|
637
|
+
relatedId: user.id
|
|
638
|
+
});
|
|
639
|
+
} catch (err) {
|
|
640
|
+
console.error(`[AuthManager] sendResetPassword failed: ${err?.message ?? err}`);
|
|
641
|
+
throw err;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
523
644
|
},
|
|
524
645
|
// Email verification
|
|
525
|
-
...this.config.emailVerification ? {
|
|
646
|
+
...this.config.emailVerification || this.config.emailService ? {
|
|
526
647
|
emailVerification: {
|
|
527
|
-
...this.config.emailVerification
|
|
528
|
-
...this.config.emailVerification
|
|
529
|
-
...this.config.emailVerification
|
|
530
|
-
...this.config.emailVerification
|
|
648
|
+
...this.config.emailVerification?.sendOnSignUp != null ? { sendOnSignUp: this.config.emailVerification.sendOnSignUp } : {},
|
|
649
|
+
...this.config.emailVerification?.sendOnSignIn != null ? { sendOnSignIn: this.config.emailVerification.sendOnSignIn } : {},
|
|
650
|
+
...this.config.emailVerification?.autoSignInAfterVerification != null ? { autoSignInAfterVerification: this.config.emailVerification.autoSignInAfterVerification } : {},
|
|
651
|
+
...this.config.emailVerification?.expiresIn != null ? { expiresIn: this.config.emailVerification.expiresIn } : {},
|
|
652
|
+
sendVerificationEmail: async ({ user, url, token }) => {
|
|
653
|
+
const email = this.getEmailService();
|
|
654
|
+
if (!email) {
|
|
655
|
+
console.warn(
|
|
656
|
+
`[AuthManager] Verification email requested for ${user.email} but no email service is wired. URL: ${url}`
|
|
657
|
+
);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const ttlSec = this.config.emailVerification?.expiresIn ?? 60 * 60;
|
|
661
|
+
try {
|
|
662
|
+
await email.sendTemplate({
|
|
663
|
+
template: "auth.verify_email",
|
|
664
|
+
to: { address: user.email, ...user.name ? { name: user.name } : {} },
|
|
665
|
+
data: {
|
|
666
|
+
user: { name: user.name || user.email, email: user.email, id: user.id },
|
|
667
|
+
verificationUrl: url,
|
|
668
|
+
token,
|
|
669
|
+
expiresInMinutes: Math.round(ttlSec / 60),
|
|
670
|
+
appName: this.getAppName()
|
|
671
|
+
},
|
|
672
|
+
relatedObject: "sys_user",
|
|
673
|
+
relatedId: user.id
|
|
674
|
+
});
|
|
675
|
+
} catch (err) {
|
|
676
|
+
console.error(`[AuthManager] sendVerificationEmail failed: ${err?.message ?? err}`);
|
|
677
|
+
throw err;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
531
680
|
}
|
|
532
681
|
} : {},
|
|
533
682
|
// Session configuration
|
|
@@ -552,6 +701,8 @@ var AuthManager = class {
|
|
|
552
701
|
}
|
|
553
702
|
if (!origins.length && (!corsOrigin || corsOrigin === "*")) {
|
|
554
703
|
origins.push("http://localhost:*");
|
|
704
|
+
origins.push("http://*.localhost:*");
|
|
705
|
+
origins.push("https://*.localhost:*");
|
|
555
706
|
}
|
|
556
707
|
return origins.length ? { trustedOrigins: origins } : {};
|
|
557
708
|
})(),
|
|
@@ -583,12 +734,34 @@ var AuthManager = class {
|
|
|
583
734
|
passkeys: pluginConfig.passkeys ?? false,
|
|
584
735
|
magicLink: pluginConfig.magicLink ?? false,
|
|
585
736
|
oidcProvider: pluginConfig.oidcProvider ?? false,
|
|
586
|
-
deviceAuthorization: pluginConfig.deviceAuthorization ?? false
|
|
737
|
+
deviceAuthorization: pluginConfig.deviceAuthorization ?? false,
|
|
738
|
+
admin: pluginConfig.admin ?? false
|
|
587
739
|
};
|
|
588
740
|
const { bearer } = await import("better-auth/plugins/bearer");
|
|
589
741
|
plugins.push(bearer());
|
|
590
742
|
if (enabled.organization) {
|
|
591
743
|
const { organization } = await import("better-auth/plugins/organization");
|
|
744
|
+
let customOrgRoles;
|
|
745
|
+
const extra = this.config.additionalOrgRoles;
|
|
746
|
+
if (extra && extra.length > 0) {
|
|
747
|
+
try {
|
|
748
|
+
const accessMod = await import("better-auth/plugins/organization/access");
|
|
749
|
+
const { defaultAc, memberAc, defaultRoles: importedDefaultRoles } = accessMod;
|
|
750
|
+
const defaultRoles = importedDefaultRoles || null;
|
|
751
|
+
if (defaultAc && memberAc && typeof memberAc.statements === "object") {
|
|
752
|
+
const built = defaultRoles ? { ...defaultRoles } : {};
|
|
753
|
+
const stmts = memberAc.statements;
|
|
754
|
+
for (const name of extra) {
|
|
755
|
+
if (!name) continue;
|
|
756
|
+
if (built[name]) continue;
|
|
757
|
+
built[name] = defaultAc.newRole(stmts);
|
|
758
|
+
}
|
|
759
|
+
customOrgRoles = built;
|
|
760
|
+
}
|
|
761
|
+
} catch {
|
|
762
|
+
customOrgRoles = void 0;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
592
765
|
plugins.push(organization({
|
|
593
766
|
schema: buildOrganizationPluginSchema(),
|
|
594
767
|
// Enable the team sub-feature so the framework's `sys_team` /
|
|
@@ -599,15 +772,47 @@ var AuthManager = class {
|
|
|
599
772
|
// portal exposes a Teams page; without this flag those endpoints
|
|
600
773
|
// 404 and the section silently breaks.
|
|
601
774
|
teams: { enabled: true },
|
|
775
|
+
// Without a mailer wired in framework, requiring email verification
|
|
776
|
+
// before accepting invitations dead-ends every invite flow with
|
|
777
|
+
// FORBIDDEN EMAIL_VERIFICATION_REQUIRED…. Default-off here keeps
|
|
778
|
+
// the built-in /accept-invitation route usable for pilots; operators
|
|
779
|
+
// who wire a real mailer can re-enable downstream.
|
|
780
|
+
requireEmailVerificationOnInvitation: false,
|
|
781
|
+
...customOrgRoles ? { roles: customOrgRoles } : {},
|
|
602
782
|
// No mailer is wired in framework yet — log the accept URL so
|
|
603
783
|
// operators / UI can fall back to copy-paste flows. Replace this
|
|
604
784
|
// with a real mail integration when available.
|
|
605
|
-
sendInvitationEmail: async ({ email, invitation, organization: org, inviter }) => {
|
|
785
|
+
sendInvitationEmail: async ({ email: recipientEmail, invitation, organization: org, inviter }) => {
|
|
606
786
|
const baseUrl = (this.config.baseUrl ?? "").replace(/\/$/, "");
|
|
607
787
|
const acceptUrl = `${baseUrl}/accept-invitation/${invitation.id}`;
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
788
|
+
const emailService = this.getEmailService();
|
|
789
|
+
if (!emailService) {
|
|
790
|
+
console.warn(
|
|
791
|
+
`[AuthManager] Invitation email not configured. To: ${recipientEmail} (org: ${org?.name ?? invitation.organizationId}, role: ${invitation.role}, inviter: ${inviter?.user?.email ?? "unknown"}) URL: ${acceptUrl}`
|
|
792
|
+
);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
try {
|
|
796
|
+
await emailService.sendTemplate({
|
|
797
|
+
template: "auth.invitation",
|
|
798
|
+
to: recipientEmail,
|
|
799
|
+
data: {
|
|
800
|
+
inviter: {
|
|
801
|
+
name: inviter?.user?.name ?? inviter?.user?.email ?? "A teammate",
|
|
802
|
+
email: inviter?.user?.email ?? ""
|
|
803
|
+
},
|
|
804
|
+
organization: { name: org?.name ?? invitation.organizationId },
|
|
805
|
+
role: invitation.role || "",
|
|
806
|
+
acceptUrl,
|
|
807
|
+
appName: this.getAppName()
|
|
808
|
+
},
|
|
809
|
+
relatedObject: "sys_invitation",
|
|
810
|
+
relatedId: invitation.id
|
|
811
|
+
});
|
|
812
|
+
} catch (err) {
|
|
813
|
+
console.error(`[AuthManager] sendInvitationEmail failed: ${err?.message ?? err}`);
|
|
814
|
+
throw err;
|
|
815
|
+
}
|
|
611
816
|
}
|
|
612
817
|
}));
|
|
613
818
|
}
|
|
@@ -617,13 +822,38 @@ var AuthManager = class {
|
|
|
617
822
|
schema: buildTwoFactorPluginSchema()
|
|
618
823
|
}));
|
|
619
824
|
}
|
|
825
|
+
if (enabled.admin) {
|
|
826
|
+
const { admin } = await import("better-auth/plugins/admin");
|
|
827
|
+
plugins.push(admin({
|
|
828
|
+
schema: buildAdminPluginSchema()
|
|
829
|
+
}));
|
|
830
|
+
}
|
|
620
831
|
if (enabled.magicLink) {
|
|
621
832
|
const { magicLink } = await import("better-auth/plugins/magic-link");
|
|
622
833
|
plugins.push(magicLink({
|
|
623
|
-
sendMagicLink: async ({ email, url }) => {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
834
|
+
sendMagicLink: async ({ email: recipientEmail, url, token }) => {
|
|
835
|
+
const emailService = this.getEmailService();
|
|
836
|
+
if (!emailService) {
|
|
837
|
+
console.warn(
|
|
838
|
+
`[AuthManager] Magic-link requested for ${recipientEmail} but no email service is wired. URL: ${url}`
|
|
839
|
+
);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
try {
|
|
843
|
+
await emailService.sendTemplate({
|
|
844
|
+
template: "auth.magic_link",
|
|
845
|
+
to: recipientEmail,
|
|
846
|
+
data: {
|
|
847
|
+
magicLinkUrl: url,
|
|
848
|
+
token,
|
|
849
|
+
expiresInMinutes: 10,
|
|
850
|
+
appName: this.getAppName()
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
} catch (err) {
|
|
854
|
+
console.error(`[AuthManager] sendMagicLink failed: ${err?.message ?? err}`);
|
|
855
|
+
throw err;
|
|
856
|
+
}
|
|
627
857
|
}
|
|
628
858
|
}));
|
|
629
859
|
}
|
|
@@ -664,6 +894,55 @@ var AuthManager = class {
|
|
|
664
894
|
schema: buildDeviceAuthorizationPluginSchema()
|
|
665
895
|
}));
|
|
666
896
|
}
|
|
897
|
+
const dataEngine = this.config.dataEngine;
|
|
898
|
+
if (dataEngine) {
|
|
899
|
+
const { customSession } = await import("better-auth/plugins/custom-session");
|
|
900
|
+
plugins.push(customSession(async ({ user, session }) => {
|
|
901
|
+
if (!user?.id) return { user, session };
|
|
902
|
+
const isPlatformAdmin = async () => {
|
|
903
|
+
try {
|
|
904
|
+
const links = await dataEngine.find("sys_user_permission_set", {
|
|
905
|
+
where: { user_id: user.id },
|
|
906
|
+
limit: 50
|
|
907
|
+
});
|
|
908
|
+
const platformLinks = (Array.isArray(links) ? links : []).filter(
|
|
909
|
+
(l) => !l.organization_id
|
|
910
|
+
);
|
|
911
|
+
if (platformLinks.length === 0) return false;
|
|
912
|
+
const sets = await dataEngine.find("sys_permission_set", { limit: 50 });
|
|
913
|
+
const adminSet = (Array.isArray(sets) ? sets : []).find(
|
|
914
|
+
(r) => r.name === "admin_full_access"
|
|
915
|
+
);
|
|
916
|
+
if (!adminSet) return false;
|
|
917
|
+
return platformLinks.some(
|
|
918
|
+
(l) => l.permission_set_id === adminSet.id
|
|
919
|
+
);
|
|
920
|
+
} catch {
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
const isActiveOrgAdmin = async () => {
|
|
925
|
+
try {
|
|
926
|
+
const orgId = session?.activeOrganizationId;
|
|
927
|
+
if (!orgId) return false;
|
|
928
|
+
const members = await dataEngine.find("sys_member", {
|
|
929
|
+
where: { user_id: user.id, organization_id: orgId },
|
|
930
|
+
limit: 5
|
|
931
|
+
});
|
|
932
|
+
return (Array.isArray(members) ? members : []).some((m) => {
|
|
933
|
+
const raw = typeof m?.role === "string" ? m.role : "";
|
|
934
|
+
const roles = raw.split(",").map((s) => s.trim().toLowerCase());
|
|
935
|
+
return roles.includes("owner") || roles.includes("admin");
|
|
936
|
+
});
|
|
937
|
+
} catch {
|
|
938
|
+
return false;
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
const promote = await isPlatformAdmin() || await isActiveOrgAdmin();
|
|
942
|
+
if (!promote) return { user, session };
|
|
943
|
+
return { user: { ...user, role: "admin" }, session };
|
|
944
|
+
}));
|
|
945
|
+
}
|
|
667
946
|
return plugins;
|
|
668
947
|
}
|
|
669
948
|
/**
|
|
@@ -720,6 +999,26 @@ var AuthManager = class {
|
|
|
720
999
|
}
|
|
721
1000
|
this.config = { ...this.config, baseUrl: url };
|
|
722
1001
|
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Inject (or replace) the outbound email service used by better-auth
|
|
1004
|
+
* callbacks. Safe to call after construction but BEFORE the first
|
|
1005
|
+
* request hits the auth handler — callbacks read this via
|
|
1006
|
+
* {@link getEmailService} when invoked.
|
|
1007
|
+
*
|
|
1008
|
+
* AuthPlugin calls this on `kernel:ready` once `ctx.getService('email')`
|
|
1009
|
+
* resolves. For tests / serverless, callers may invoke directly.
|
|
1010
|
+
*/
|
|
1011
|
+
setEmailService(email) {
|
|
1012
|
+
this.config.emailService = email;
|
|
1013
|
+
}
|
|
1014
|
+
/** @internal Used by callback closures. */
|
|
1015
|
+
getEmailService() {
|
|
1016
|
+
return this.config.emailService;
|
|
1017
|
+
}
|
|
1018
|
+
/** @internal `{{appName}}` placeholder value for built-in templates. */
|
|
1019
|
+
getAppName() {
|
|
1020
|
+
return this.config.appName ?? "ObjectStack";
|
|
1021
|
+
}
|
|
723
1022
|
/**
|
|
724
1023
|
* Get the underlying better-auth instance
|
|
725
1024
|
* Useful for advanced use cases
|
|
@@ -900,24 +1199,22 @@ var AuthPlugin = class {
|
|
|
900
1199
|
ctx.registerService("auth", this.authManager);
|
|
901
1200
|
ctx.getService("manifest").register({
|
|
902
1201
|
...authPluginManifestHeader,
|
|
1202
|
+
...this.options.manifestDatasource ? { defaultDatasource: this.options.manifestDatasource } : {},
|
|
903
1203
|
objects: authIdentityObjects,
|
|
904
1204
|
// The platform Setup App is a static metadata artifact (lives in
|
|
905
1205
|
// @objectstack/platform-objects/apps). plugin-auth is the natural
|
|
906
1206
|
// owner of its registration since it loads first among the trio
|
|
907
1207
|
// (auth + security + audit) that supplies the underlying objects.
|
|
908
1208
|
apps: [import_apps.SETUP_APP],
|
|
909
|
-
//
|
|
910
|
-
//
|
|
911
|
-
//
|
|
912
|
-
//
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
import_apps.AuditLogsView,
|
|
919
|
-
import_apps.PackageInstallationsView
|
|
920
|
-
],
|
|
1209
|
+
// List views for each Setup-nav object are defined on the schema
|
|
1210
|
+
// itself via the canonical `listViews` map (e.g.
|
|
1211
|
+
// sys_user.listViews.{all_users,unverified,two_factor}). Registering
|
|
1212
|
+
// top-level views here is the legacy pre-M10.30c pattern — it caused
|
|
1213
|
+
// duplicate "Users"/"Roles"/"Sessions" tabs to appear alongside the
|
|
1214
|
+
// schema-derived ones, sometimes referencing nonexistent fields
|
|
1215
|
+
// (e.g. legacy `users.view` had phone/status/active columns that do
|
|
1216
|
+
// not exist on sys_user). Schema-embedded listViews is the single
|
|
1217
|
+
// source of truth.
|
|
921
1218
|
dashboards: [import_apps.SystemOverviewDashboard, import_apps.SecurityOverviewDashboard]
|
|
922
1219
|
});
|
|
923
1220
|
ctx.logger.info("Auth Plugin initialized successfully");
|
|
@@ -929,6 +1226,17 @@ var AuthPlugin = class {
|
|
|
929
1226
|
}
|
|
930
1227
|
if (this.options.registerRoutes) {
|
|
931
1228
|
ctx.hook("kernel:ready", async () => {
|
|
1229
|
+
if (this.authManager) {
|
|
1230
|
+
try {
|
|
1231
|
+
const emailSvc = ctx.getService("email");
|
|
1232
|
+
if (emailSvc) {
|
|
1233
|
+
this.authManager.setEmailService(emailSvc);
|
|
1234
|
+
ctx.logger.info("Auth: email service wired (transactional mail enabled)");
|
|
1235
|
+
}
|
|
1236
|
+
} catch {
|
|
1237
|
+
ctx.logger.info("Auth: no email service registered \u2014 auth callbacks will log instead of sending");
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
932
1240
|
let httpServer = null;
|
|
933
1241
|
try {
|
|
934
1242
|
httpServer = ctx.getService("http-server");
|
|
@@ -1034,6 +1342,19 @@ var AuthPlugin = class {
|
|
|
1034
1342
|
ctx.logger.error("[AuthPlugin] better-auth returned server error", new Error(`HTTP ${response.status}: (unable to read body)`));
|
|
1035
1343
|
}
|
|
1036
1344
|
}
|
|
1345
|
+
try {
|
|
1346
|
+
const url = c.req.url;
|
|
1347
|
+
if (response.ok && /\/jwks(\?|$)/.test(url)) {
|
|
1348
|
+
const existing = response.headers.get("cache-control");
|
|
1349
|
+
if (!existing) {
|
|
1350
|
+
response.headers.set(
|
|
1351
|
+
"cache-control",
|
|
1352
|
+
"public, max-age=300, stale-while-revalidate=86400"
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
} catch {
|
|
1357
|
+
}
|
|
1037
1358
|
return response;
|
|
1038
1359
|
} catch (error) {
|
|
1039
1360
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -1068,8 +1389,19 @@ var AuthPlugin = class {
|
|
|
1068
1389
|
const { oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata } = await import("@better-auth/oauth-provider");
|
|
1069
1390
|
const authServerHandler = oauthProviderAuthServerMetadata(auth);
|
|
1070
1391
|
const openidConfigHandler = oauthProviderOpenIdConfigMetadata(auth);
|
|
1071
|
-
|
|
1072
|
-
|
|
1392
|
+
const DISCOVERY_CACHE = "public, max-age=300, stale-while-revalidate=86400";
|
|
1393
|
+
const withDiscoveryCache = async (handler, req) => {
|
|
1394
|
+
const resp = await handler(req);
|
|
1395
|
+
try {
|
|
1396
|
+
if (resp.ok && !resp.headers.get("cache-control")) {
|
|
1397
|
+
resp.headers.set("cache-control", DISCOVERY_CACHE);
|
|
1398
|
+
}
|
|
1399
|
+
} catch {
|
|
1400
|
+
}
|
|
1401
|
+
return resp;
|
|
1402
|
+
};
|
|
1403
|
+
rawApp.get("/.well-known/oauth-authorization-server", (c) => withDiscoveryCache(authServerHandler, c.req.raw));
|
|
1404
|
+
rawApp.get("/.well-known/openid-configuration", (c) => withDiscoveryCache(openidConfigHandler, c.req.raw));
|
|
1073
1405
|
ctx.logger.info(
|
|
1074
1406
|
"OIDC discovery endpoints mounted at /.well-known/{oauth-authorization-server,openid-configuration}"
|
|
1075
1407
|
);
|
|
@@ -1078,6 +1410,8 @@ var AuthPlugin = class {
|
|
|
1078
1410
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1079
1411
|
0 && (module.exports = {
|
|
1080
1412
|
AUTH_ACCOUNT_CONFIG,
|
|
1413
|
+
AUTH_ADMIN_SESSION_FIELDS,
|
|
1414
|
+
AUTH_ADMIN_USER_FIELDS,
|
|
1081
1415
|
AUTH_DEVICE_CODE_SCHEMA,
|
|
1082
1416
|
AUTH_INVITATION_SCHEMA,
|
|
1083
1417
|
AUTH_JWKS_SCHEMA,
|
|
@@ -1099,6 +1433,7 @@ var AuthPlugin = class {
|
|
|
1099
1433
|
AUTH_VERIFICATION_CONFIG,
|
|
1100
1434
|
AuthManager,
|
|
1101
1435
|
AuthPlugin,
|
|
1436
|
+
buildAdminPluginSchema,
|
|
1102
1437
|
buildDeviceAuthorizationPluginSchema,
|
|
1103
1438
|
buildJwtPluginSchema,
|
|
1104
1439
|
buildOauthProviderPluginSchema,
|