@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.mjs
CHANGED
|
@@ -2,13 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
SETUP_APP,
|
|
4
4
|
SystemOverviewDashboard,
|
|
5
|
-
SecurityOverviewDashboard
|
|
6
|
-
UsersView,
|
|
7
|
-
OrganizationsView,
|
|
8
|
-
RolesView,
|
|
9
|
-
SessionsView,
|
|
10
|
-
AuditLogsView,
|
|
11
|
-
PackageInstallationsView
|
|
5
|
+
SecurityOverviewDashboard
|
|
12
6
|
} from "@objectstack/platform-objects/apps";
|
|
13
7
|
|
|
14
8
|
// src/objectql-adapter.ts
|
|
@@ -23,6 +17,41 @@ var AUTH_MODEL_TO_PROTOCOL = {
|
|
|
23
17
|
function resolveProtocolName(model) {
|
|
24
18
|
return AUTH_MODEL_TO_PROTOCOL[model] ?? model;
|
|
25
19
|
}
|
|
20
|
+
var LEGACY_DATETIME_FIELDS_BY_MODEL = {
|
|
21
|
+
user: ["created_at", "updated_at"],
|
|
22
|
+
session: ["expires_at", "created_at", "updated_at"],
|
|
23
|
+
account: [
|
|
24
|
+
"access_token_expires_at",
|
|
25
|
+
"refresh_token_expires_at",
|
|
26
|
+
"created_at",
|
|
27
|
+
"updated_at"
|
|
28
|
+
],
|
|
29
|
+
verification: ["expires_at", "created_at", "updated_at"]
|
|
30
|
+
};
|
|
31
|
+
var NUMERIC_STRING_RE = /^-?\d+(\.\d+)?$/;
|
|
32
|
+
function normaliseLegacyDate(value) {
|
|
33
|
+
if (typeof value !== "string") return value;
|
|
34
|
+
if (!NUMERIC_STRING_RE.test(value)) return value;
|
|
35
|
+
const n = parseFloat(value);
|
|
36
|
+
if (!Number.isFinite(n)) return value;
|
|
37
|
+
if (Math.abs(n) < 1e10) return value;
|
|
38
|
+
const d = new Date(n);
|
|
39
|
+
if (Number.isNaN(d.getTime())) return value;
|
|
40
|
+
return d.toISOString();
|
|
41
|
+
}
|
|
42
|
+
function normaliseLegacyDates(model, record) {
|
|
43
|
+
if (!record) return record;
|
|
44
|
+
const cols = LEGACY_DATETIME_FIELDS_BY_MODEL[model];
|
|
45
|
+
if (!cols) return record;
|
|
46
|
+
for (const col of cols) {
|
|
47
|
+
if (col in record) {
|
|
48
|
+
record[col] = normaliseLegacyDate(
|
|
49
|
+
record[col]
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return record;
|
|
54
|
+
}
|
|
26
55
|
function convertWhere(where) {
|
|
27
56
|
const filter = {};
|
|
28
57
|
for (const condition of where) {
|
|
@@ -51,20 +80,24 @@ function createObjectQLAdapterFactory(dataEngine) {
|
|
|
51
80
|
return createAdapterFactory({
|
|
52
81
|
config: {
|
|
53
82
|
adapterId: "objectql",
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
83
|
+
// We let better-auth handle Date↔string and boolean↔0/1 conversion so
|
|
84
|
+
// that values land in the underlying SQL driver as primitive strings
|
|
85
|
+
// and integers. Some drivers (e.g. libsql over the HTTP transport)
|
|
86
|
+
// otherwise mangle `Date` objects into `"<epoch>.0"` strings that
|
|
87
|
+
// break the client-side session parser.
|
|
88
|
+
supportsBooleans: false,
|
|
89
|
+
supportsDates: false,
|
|
57
90
|
supportsJSON: true
|
|
58
91
|
},
|
|
59
92
|
adapter: () => ({
|
|
60
93
|
create: async ({ model, data, select: _select }) => {
|
|
61
94
|
const result = await dataEngine.insert(model, data);
|
|
62
|
-
return result;
|
|
95
|
+
return normaliseLegacyDates(model, result);
|
|
63
96
|
},
|
|
64
97
|
findOne: async ({ model, where, select, join: _join }) => {
|
|
65
98
|
const filter = convertWhere(where);
|
|
66
99
|
const result = await dataEngine.findOne(model, { where: filter, fields: select });
|
|
67
|
-
return result ? result : null;
|
|
100
|
+
return result ? normaliseLegacyDates(model, result) : null;
|
|
68
101
|
},
|
|
69
102
|
findMany: async ({ model, where, limit, offset, sortBy, join: _join }) => {
|
|
70
103
|
const filter = where ? convertWhere(where) : {};
|
|
@@ -75,7 +108,7 @@ function createObjectQLAdapterFactory(dataEngine) {
|
|
|
75
108
|
offset,
|
|
76
109
|
orderBy
|
|
77
110
|
});
|
|
78
|
-
return results;
|
|
111
|
+
return results.map((r) => normaliseLegacyDates(model, r));
|
|
79
112
|
},
|
|
80
113
|
count: async ({ model, where }) => {
|
|
81
114
|
const filter = where ? convertWhere(where) : {};
|
|
@@ -86,7 +119,7 @@ function createObjectQLAdapterFactory(dataEngine) {
|
|
|
86
119
|
const record = await dataEngine.findOne(model, { where: filter });
|
|
87
120
|
if (!record) return null;
|
|
88
121
|
const result = await dataEngine.update(model, { ...update, id: record.id });
|
|
89
|
-
return result ? result : null;
|
|
122
|
+
return result ? normaliseLegacyDates(model, result) : null;
|
|
90
123
|
},
|
|
91
124
|
updateMany: async ({ model, where, update }) => {
|
|
92
125
|
const filter = convertWhere(where);
|
|
@@ -283,6 +316,13 @@ var AUTH_TWO_FACTOR_SCHEMA = {
|
|
|
283
316
|
var AUTH_TWO_FACTOR_USER_FIELDS = {
|
|
284
317
|
twoFactorEnabled: "two_factor_enabled"
|
|
285
318
|
};
|
|
319
|
+
var AUTH_ADMIN_USER_FIELDS = {
|
|
320
|
+
banReason: "ban_reason",
|
|
321
|
+
banExpires: "ban_expires"
|
|
322
|
+
};
|
|
323
|
+
var AUTH_ADMIN_SESSION_FIELDS = {
|
|
324
|
+
impersonatedBy: "impersonated_by"
|
|
325
|
+
};
|
|
286
326
|
var AUTH_OAUTH_CLIENT_SCHEMA = {
|
|
287
327
|
modelName: SystemObjectName2.OAUTH_APPLICATION,
|
|
288
328
|
// 'sys_oauth_application'
|
|
@@ -366,6 +406,16 @@ function buildTwoFactorPluginSchema() {
|
|
|
366
406
|
}
|
|
367
407
|
};
|
|
368
408
|
}
|
|
409
|
+
function buildAdminPluginSchema() {
|
|
410
|
+
return {
|
|
411
|
+
user: {
|
|
412
|
+
fields: AUTH_ADMIN_USER_FIELDS
|
|
413
|
+
},
|
|
414
|
+
session: {
|
|
415
|
+
fields: AUTH_ADMIN_SESSION_FIELDS
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
}
|
|
369
419
|
function buildOrganizationPluginSchema() {
|
|
370
420
|
return {
|
|
371
421
|
organization: AUTH_ORGANIZATION_SCHEMA,
|
|
@@ -447,7 +497,41 @@ var AuthManager = class {
|
|
|
447
497
|
...AUTH_USER_CONFIG
|
|
448
498
|
},
|
|
449
499
|
account: {
|
|
450
|
-
...AUTH_ACCOUNT_CONFIG
|
|
500
|
+
...AUTH_ACCOUNT_CONFIG,
|
|
501
|
+
// Allow OIDC/OAuth callbacks to implicitly link the incoming
|
|
502
|
+
// identity to a pre-existing local user when the emails match.
|
|
503
|
+
//
|
|
504
|
+
// ObjectStack's platform SSO ("objectstack-cloud" provider) is the
|
|
505
|
+
// canonical case: cloud is the IdP for every project, so a user
|
|
506
|
+
// arriving via SSO is — by construction — the same person who was
|
|
507
|
+
// auto-seeded as the project owner when the project was created.
|
|
508
|
+
// Without trusting the provider, better-auth's safety check rejects
|
|
509
|
+
// the link with `error=account_not_linked` because the seeded user
|
|
510
|
+
// row has `emailVerified=false` (no actual verification ever runs
|
|
511
|
+
// in the IdP-mediated flow). See packages/plugins/plugin-auth/
|
|
512
|
+
// node_modules/better-auth/dist/oauth2/link-account.mjs:22.
|
|
513
|
+
//
|
|
514
|
+
// Custom-deployment consumers can extend the trusted set via
|
|
515
|
+
// `config.account.accountLinking.trustedProviders`; we always
|
|
516
|
+
// include `objectstack-cloud` because it is the platform IdP.
|
|
517
|
+
accountLinking: {
|
|
518
|
+
enabled: true,
|
|
519
|
+
// better-auth's account-linking gate has TWO independent clauses
|
|
520
|
+
// (see link-account.mjs:22). Trusting the provider only satisfies
|
|
521
|
+
// the first clause; the second — `requireLocalEmailVerified &&
|
|
522
|
+
// !dbUser.user.emailVerified` — still blocks linking when the
|
|
523
|
+
// pre-existing local user row has `emailVerified=false` (the
|
|
524
|
+
// default for owner-seeded rows). Disabling the local-email gate
|
|
525
|
+
// is safe here because the OAuth side is what we actually trust:
|
|
526
|
+
// the incoming identity was verified by the IdP. Consumers who
|
|
527
|
+
// need the stricter behavior can override via config.
|
|
528
|
+
requireLocalEmailVerified: false,
|
|
529
|
+
...this.config?.account?.accountLinking ?? {},
|
|
530
|
+
trustedProviders: Array.from(/* @__PURE__ */ new Set([
|
|
531
|
+
"objectstack-cloud",
|
|
532
|
+
...this.config?.account?.accountLinking?.trustedProviders ?? []
|
|
533
|
+
]))
|
|
534
|
+
}
|
|
451
535
|
},
|
|
452
536
|
verification: {
|
|
453
537
|
...AUTH_VERIFICATION_CONFIG
|
|
@@ -463,15 +547,71 @@ var AuthManager = class {
|
|
|
463
547
|
...this.config.emailAndPassword?.maxPasswordLength != null ? { maxPasswordLength: this.config.emailAndPassword.maxPasswordLength } : {},
|
|
464
548
|
...this.config.emailAndPassword?.resetPasswordTokenExpiresIn != null ? { resetPasswordTokenExpiresIn: this.config.emailAndPassword.resetPasswordTokenExpiresIn } : {},
|
|
465
549
|
...this.config.emailAndPassword?.autoSignIn != null ? { autoSignIn: this.config.emailAndPassword.autoSignIn } : {},
|
|
466
|
-
...this.config.emailAndPassword?.revokeSessionsOnPasswordReset != null ? { revokeSessionsOnPasswordReset: this.config.emailAndPassword.revokeSessionsOnPasswordReset } : {}
|
|
550
|
+
...this.config.emailAndPassword?.revokeSessionsOnPasswordReset != null ? { revokeSessionsOnPasswordReset: this.config.emailAndPassword.revokeSessionsOnPasswordReset } : {},
|
|
551
|
+
sendResetPassword: async ({ user, url, token }) => {
|
|
552
|
+
const email = this.getEmailService();
|
|
553
|
+
if (!email) {
|
|
554
|
+
console.warn(
|
|
555
|
+
`[AuthManager] Password-reset requested for ${user.email} but no email service is wired. URL: ${url}`
|
|
556
|
+
);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const ttlSec = this.config.emailAndPassword?.resetPasswordTokenExpiresIn ?? 60 * 60;
|
|
560
|
+
try {
|
|
561
|
+
await email.sendTemplate({
|
|
562
|
+
template: "auth.password_reset",
|
|
563
|
+
to: { address: user.email, ...user.name ? { name: user.name } : {} },
|
|
564
|
+
data: {
|
|
565
|
+
user: { name: user.name || user.email, email: user.email, id: user.id },
|
|
566
|
+
resetUrl: url,
|
|
567
|
+
token,
|
|
568
|
+
expiresInMinutes: Math.round(ttlSec / 60),
|
|
569
|
+
appName: this.getAppName()
|
|
570
|
+
},
|
|
571
|
+
relatedObject: "sys_user",
|
|
572
|
+
relatedId: user.id
|
|
573
|
+
});
|
|
574
|
+
} catch (err) {
|
|
575
|
+
console.error(`[AuthManager] sendResetPassword failed: ${err?.message ?? err}`);
|
|
576
|
+
throw err;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
467
579
|
},
|
|
468
580
|
// Email verification
|
|
469
|
-
...this.config.emailVerification ? {
|
|
581
|
+
...this.config.emailVerification || this.config.emailService ? {
|
|
470
582
|
emailVerification: {
|
|
471
|
-
...this.config.emailVerification
|
|
472
|
-
...this.config.emailVerification
|
|
473
|
-
...this.config.emailVerification
|
|
474
|
-
...this.config.emailVerification
|
|
583
|
+
...this.config.emailVerification?.sendOnSignUp != null ? { sendOnSignUp: this.config.emailVerification.sendOnSignUp } : {},
|
|
584
|
+
...this.config.emailVerification?.sendOnSignIn != null ? { sendOnSignIn: this.config.emailVerification.sendOnSignIn } : {},
|
|
585
|
+
...this.config.emailVerification?.autoSignInAfterVerification != null ? { autoSignInAfterVerification: this.config.emailVerification.autoSignInAfterVerification } : {},
|
|
586
|
+
...this.config.emailVerification?.expiresIn != null ? { expiresIn: this.config.emailVerification.expiresIn } : {},
|
|
587
|
+
sendVerificationEmail: async ({ user, url, token }) => {
|
|
588
|
+
const email = this.getEmailService();
|
|
589
|
+
if (!email) {
|
|
590
|
+
console.warn(
|
|
591
|
+
`[AuthManager] Verification email requested for ${user.email} but no email service is wired. URL: ${url}`
|
|
592
|
+
);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const ttlSec = this.config.emailVerification?.expiresIn ?? 60 * 60;
|
|
596
|
+
try {
|
|
597
|
+
await email.sendTemplate({
|
|
598
|
+
template: "auth.verify_email",
|
|
599
|
+
to: { address: user.email, ...user.name ? { name: user.name } : {} },
|
|
600
|
+
data: {
|
|
601
|
+
user: { name: user.name || user.email, email: user.email, id: user.id },
|
|
602
|
+
verificationUrl: url,
|
|
603
|
+
token,
|
|
604
|
+
expiresInMinutes: Math.round(ttlSec / 60),
|
|
605
|
+
appName: this.getAppName()
|
|
606
|
+
},
|
|
607
|
+
relatedObject: "sys_user",
|
|
608
|
+
relatedId: user.id
|
|
609
|
+
});
|
|
610
|
+
} catch (err) {
|
|
611
|
+
console.error(`[AuthManager] sendVerificationEmail failed: ${err?.message ?? err}`);
|
|
612
|
+
throw err;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
475
615
|
}
|
|
476
616
|
} : {},
|
|
477
617
|
// Session configuration
|
|
@@ -496,6 +636,8 @@ var AuthManager = class {
|
|
|
496
636
|
}
|
|
497
637
|
if (!origins.length && (!corsOrigin || corsOrigin === "*")) {
|
|
498
638
|
origins.push("http://localhost:*");
|
|
639
|
+
origins.push("http://*.localhost:*");
|
|
640
|
+
origins.push("https://*.localhost:*");
|
|
499
641
|
}
|
|
500
642
|
return origins.length ? { trustedOrigins: origins } : {};
|
|
501
643
|
})(),
|
|
@@ -527,12 +669,34 @@ var AuthManager = class {
|
|
|
527
669
|
passkeys: pluginConfig.passkeys ?? false,
|
|
528
670
|
magicLink: pluginConfig.magicLink ?? false,
|
|
529
671
|
oidcProvider: pluginConfig.oidcProvider ?? false,
|
|
530
|
-
deviceAuthorization: pluginConfig.deviceAuthorization ?? false
|
|
672
|
+
deviceAuthorization: pluginConfig.deviceAuthorization ?? false,
|
|
673
|
+
admin: pluginConfig.admin ?? false
|
|
531
674
|
};
|
|
532
675
|
const { bearer } = await import("better-auth/plugins/bearer");
|
|
533
676
|
plugins.push(bearer());
|
|
534
677
|
if (enabled.organization) {
|
|
535
678
|
const { organization } = await import("better-auth/plugins/organization");
|
|
679
|
+
let customOrgRoles;
|
|
680
|
+
const extra = this.config.additionalOrgRoles;
|
|
681
|
+
if (extra && extra.length > 0) {
|
|
682
|
+
try {
|
|
683
|
+
const accessMod = await import("better-auth/plugins/organization/access");
|
|
684
|
+
const { defaultAc, memberAc, defaultRoles: importedDefaultRoles } = accessMod;
|
|
685
|
+
const defaultRoles = importedDefaultRoles || null;
|
|
686
|
+
if (defaultAc && memberAc && typeof memberAc.statements === "object") {
|
|
687
|
+
const built = defaultRoles ? { ...defaultRoles } : {};
|
|
688
|
+
const stmts = memberAc.statements;
|
|
689
|
+
for (const name of extra) {
|
|
690
|
+
if (!name) continue;
|
|
691
|
+
if (built[name]) continue;
|
|
692
|
+
built[name] = defaultAc.newRole(stmts);
|
|
693
|
+
}
|
|
694
|
+
customOrgRoles = built;
|
|
695
|
+
}
|
|
696
|
+
} catch {
|
|
697
|
+
customOrgRoles = void 0;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
536
700
|
plugins.push(organization({
|
|
537
701
|
schema: buildOrganizationPluginSchema(),
|
|
538
702
|
// Enable the team sub-feature so the framework's `sys_team` /
|
|
@@ -543,15 +707,47 @@ var AuthManager = class {
|
|
|
543
707
|
// portal exposes a Teams page; without this flag those endpoints
|
|
544
708
|
// 404 and the section silently breaks.
|
|
545
709
|
teams: { enabled: true },
|
|
710
|
+
// Without a mailer wired in framework, requiring email verification
|
|
711
|
+
// before accepting invitations dead-ends every invite flow with
|
|
712
|
+
// FORBIDDEN EMAIL_VERIFICATION_REQUIRED…. Default-off here keeps
|
|
713
|
+
// the built-in /accept-invitation route usable for pilots; operators
|
|
714
|
+
// who wire a real mailer can re-enable downstream.
|
|
715
|
+
requireEmailVerificationOnInvitation: false,
|
|
716
|
+
...customOrgRoles ? { roles: customOrgRoles } : {},
|
|
546
717
|
// No mailer is wired in framework yet — log the accept URL so
|
|
547
718
|
// operators / UI can fall back to copy-paste flows. Replace this
|
|
548
719
|
// with a real mail integration when available.
|
|
549
|
-
sendInvitationEmail: async ({ email, invitation, organization: org, inviter }) => {
|
|
720
|
+
sendInvitationEmail: async ({ email: recipientEmail, invitation, organization: org, inviter }) => {
|
|
550
721
|
const baseUrl = (this.config.baseUrl ?? "").replace(/\/$/, "");
|
|
551
722
|
const acceptUrl = `${baseUrl}/accept-invitation/${invitation.id}`;
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
723
|
+
const emailService = this.getEmailService();
|
|
724
|
+
if (!emailService) {
|
|
725
|
+
console.warn(
|
|
726
|
+
`[AuthManager] Invitation email not configured. To: ${recipientEmail} (org: ${org?.name ?? invitation.organizationId}, role: ${invitation.role}, inviter: ${inviter?.user?.email ?? "unknown"}) URL: ${acceptUrl}`
|
|
727
|
+
);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
await emailService.sendTemplate({
|
|
732
|
+
template: "auth.invitation",
|
|
733
|
+
to: recipientEmail,
|
|
734
|
+
data: {
|
|
735
|
+
inviter: {
|
|
736
|
+
name: inviter?.user?.name ?? inviter?.user?.email ?? "A teammate",
|
|
737
|
+
email: inviter?.user?.email ?? ""
|
|
738
|
+
},
|
|
739
|
+
organization: { name: org?.name ?? invitation.organizationId },
|
|
740
|
+
role: invitation.role || "",
|
|
741
|
+
acceptUrl,
|
|
742
|
+
appName: this.getAppName()
|
|
743
|
+
},
|
|
744
|
+
relatedObject: "sys_invitation",
|
|
745
|
+
relatedId: invitation.id
|
|
746
|
+
});
|
|
747
|
+
} catch (err) {
|
|
748
|
+
console.error(`[AuthManager] sendInvitationEmail failed: ${err?.message ?? err}`);
|
|
749
|
+
throw err;
|
|
750
|
+
}
|
|
555
751
|
}
|
|
556
752
|
}));
|
|
557
753
|
}
|
|
@@ -561,13 +757,38 @@ var AuthManager = class {
|
|
|
561
757
|
schema: buildTwoFactorPluginSchema()
|
|
562
758
|
}));
|
|
563
759
|
}
|
|
760
|
+
if (enabled.admin) {
|
|
761
|
+
const { admin } = await import("better-auth/plugins/admin");
|
|
762
|
+
plugins.push(admin({
|
|
763
|
+
schema: buildAdminPluginSchema()
|
|
764
|
+
}));
|
|
765
|
+
}
|
|
564
766
|
if (enabled.magicLink) {
|
|
565
767
|
const { magicLink } = await import("better-auth/plugins/magic-link");
|
|
566
768
|
plugins.push(magicLink({
|
|
567
|
-
sendMagicLink: async ({ email, url }) => {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
769
|
+
sendMagicLink: async ({ email: recipientEmail, url, token }) => {
|
|
770
|
+
const emailService = this.getEmailService();
|
|
771
|
+
if (!emailService) {
|
|
772
|
+
console.warn(
|
|
773
|
+
`[AuthManager] Magic-link requested for ${recipientEmail} but no email service is wired. URL: ${url}`
|
|
774
|
+
);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
try {
|
|
778
|
+
await emailService.sendTemplate({
|
|
779
|
+
template: "auth.magic_link",
|
|
780
|
+
to: recipientEmail,
|
|
781
|
+
data: {
|
|
782
|
+
magicLinkUrl: url,
|
|
783
|
+
token,
|
|
784
|
+
expiresInMinutes: 10,
|
|
785
|
+
appName: this.getAppName()
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
} catch (err) {
|
|
789
|
+
console.error(`[AuthManager] sendMagicLink failed: ${err?.message ?? err}`);
|
|
790
|
+
throw err;
|
|
791
|
+
}
|
|
571
792
|
}
|
|
572
793
|
}));
|
|
573
794
|
}
|
|
@@ -608,6 +829,55 @@ var AuthManager = class {
|
|
|
608
829
|
schema: buildDeviceAuthorizationPluginSchema()
|
|
609
830
|
}));
|
|
610
831
|
}
|
|
832
|
+
const dataEngine = this.config.dataEngine;
|
|
833
|
+
if (dataEngine) {
|
|
834
|
+
const { customSession } = await import("better-auth/plugins/custom-session");
|
|
835
|
+
plugins.push(customSession(async ({ user, session }) => {
|
|
836
|
+
if (!user?.id) return { user, session };
|
|
837
|
+
const isPlatformAdmin = async () => {
|
|
838
|
+
try {
|
|
839
|
+
const links = await dataEngine.find("sys_user_permission_set", {
|
|
840
|
+
where: { user_id: user.id },
|
|
841
|
+
limit: 50
|
|
842
|
+
});
|
|
843
|
+
const platformLinks = (Array.isArray(links) ? links : []).filter(
|
|
844
|
+
(l) => !l.organization_id
|
|
845
|
+
);
|
|
846
|
+
if (platformLinks.length === 0) return false;
|
|
847
|
+
const sets = await dataEngine.find("sys_permission_set", { limit: 50 });
|
|
848
|
+
const adminSet = (Array.isArray(sets) ? sets : []).find(
|
|
849
|
+
(r) => r.name === "admin_full_access"
|
|
850
|
+
);
|
|
851
|
+
if (!adminSet) return false;
|
|
852
|
+
return platformLinks.some(
|
|
853
|
+
(l) => l.permission_set_id === adminSet.id
|
|
854
|
+
);
|
|
855
|
+
} catch {
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
const isActiveOrgAdmin = async () => {
|
|
860
|
+
try {
|
|
861
|
+
const orgId = session?.activeOrganizationId;
|
|
862
|
+
if (!orgId) return false;
|
|
863
|
+
const members = await dataEngine.find("sys_member", {
|
|
864
|
+
where: { user_id: user.id, organization_id: orgId },
|
|
865
|
+
limit: 5
|
|
866
|
+
});
|
|
867
|
+
return (Array.isArray(members) ? members : []).some((m) => {
|
|
868
|
+
const raw = typeof m?.role === "string" ? m.role : "";
|
|
869
|
+
const roles = raw.split(",").map((s) => s.trim().toLowerCase());
|
|
870
|
+
return roles.includes("owner") || roles.includes("admin");
|
|
871
|
+
});
|
|
872
|
+
} catch {
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
const promote = await isPlatformAdmin() || await isActiveOrgAdmin();
|
|
877
|
+
if (!promote) return { user, session };
|
|
878
|
+
return { user: { ...user, role: "admin" }, session };
|
|
879
|
+
}));
|
|
880
|
+
}
|
|
611
881
|
return plugins;
|
|
612
882
|
}
|
|
613
883
|
/**
|
|
@@ -664,6 +934,26 @@ var AuthManager = class {
|
|
|
664
934
|
}
|
|
665
935
|
this.config = { ...this.config, baseUrl: url };
|
|
666
936
|
}
|
|
937
|
+
/**
|
|
938
|
+
* Inject (or replace) the outbound email service used by better-auth
|
|
939
|
+
* callbacks. Safe to call after construction but BEFORE the first
|
|
940
|
+
* request hits the auth handler — callbacks read this via
|
|
941
|
+
* {@link getEmailService} when invoked.
|
|
942
|
+
*
|
|
943
|
+
* AuthPlugin calls this on `kernel:ready` once `ctx.getService('email')`
|
|
944
|
+
* resolves. For tests / serverless, callers may invoke directly.
|
|
945
|
+
*/
|
|
946
|
+
setEmailService(email) {
|
|
947
|
+
this.config.emailService = email;
|
|
948
|
+
}
|
|
949
|
+
/** @internal Used by callback closures. */
|
|
950
|
+
getEmailService() {
|
|
951
|
+
return this.config.emailService;
|
|
952
|
+
}
|
|
953
|
+
/** @internal `{{appName}}` placeholder value for built-in templates. */
|
|
954
|
+
getAppName() {
|
|
955
|
+
return this.config.appName ?? "ObjectStack";
|
|
956
|
+
}
|
|
667
957
|
/**
|
|
668
958
|
* Get the underlying better-auth instance
|
|
669
959
|
* Useful for advanced use cases
|
|
@@ -863,24 +1153,22 @@ var AuthPlugin = class {
|
|
|
863
1153
|
ctx.registerService("auth", this.authManager);
|
|
864
1154
|
ctx.getService("manifest").register({
|
|
865
1155
|
...authPluginManifestHeader,
|
|
1156
|
+
...this.options.manifestDatasource ? { defaultDatasource: this.options.manifestDatasource } : {},
|
|
866
1157
|
objects: authIdentityObjects,
|
|
867
1158
|
// The platform Setup App is a static metadata artifact (lives in
|
|
868
1159
|
// @objectstack/platform-objects/apps). plugin-auth is the natural
|
|
869
1160
|
// owner of its registration since it loads first among the trio
|
|
870
1161
|
// (auth + security + audit) that supplies the underlying objects.
|
|
871
1162
|
apps: [SETUP_APP],
|
|
872
|
-
//
|
|
873
|
-
//
|
|
874
|
-
//
|
|
875
|
-
//
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
AuditLogsView,
|
|
882
|
-
PackageInstallationsView
|
|
883
|
-
],
|
|
1163
|
+
// List views for each Setup-nav object are defined on the schema
|
|
1164
|
+
// itself via the canonical `listViews` map (e.g.
|
|
1165
|
+
// sys_user.listViews.{all_users,unverified,two_factor}). Registering
|
|
1166
|
+
// top-level views here is the legacy pre-M10.30c pattern — it caused
|
|
1167
|
+
// duplicate "Users"/"Roles"/"Sessions" tabs to appear alongside the
|
|
1168
|
+
// schema-derived ones, sometimes referencing nonexistent fields
|
|
1169
|
+
// (e.g. legacy `users.view` had phone/status/active columns that do
|
|
1170
|
+
// not exist on sys_user). Schema-embedded listViews is the single
|
|
1171
|
+
// source of truth.
|
|
884
1172
|
dashboards: [SystemOverviewDashboard, SecurityOverviewDashboard]
|
|
885
1173
|
});
|
|
886
1174
|
ctx.logger.info("Auth Plugin initialized successfully");
|
|
@@ -892,6 +1180,17 @@ var AuthPlugin = class {
|
|
|
892
1180
|
}
|
|
893
1181
|
if (this.options.registerRoutes) {
|
|
894
1182
|
ctx.hook("kernel:ready", async () => {
|
|
1183
|
+
if (this.authManager) {
|
|
1184
|
+
try {
|
|
1185
|
+
const emailSvc = ctx.getService("email");
|
|
1186
|
+
if (emailSvc) {
|
|
1187
|
+
this.authManager.setEmailService(emailSvc);
|
|
1188
|
+
ctx.logger.info("Auth: email service wired (transactional mail enabled)");
|
|
1189
|
+
}
|
|
1190
|
+
} catch {
|
|
1191
|
+
ctx.logger.info("Auth: no email service registered \u2014 auth callbacks will log instead of sending");
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
895
1194
|
let httpServer = null;
|
|
896
1195
|
try {
|
|
897
1196
|
httpServer = ctx.getService("http-server");
|
|
@@ -997,6 +1296,19 @@ var AuthPlugin = class {
|
|
|
997
1296
|
ctx.logger.error("[AuthPlugin] better-auth returned server error", new Error(`HTTP ${response.status}: (unable to read body)`));
|
|
998
1297
|
}
|
|
999
1298
|
}
|
|
1299
|
+
try {
|
|
1300
|
+
const url = c.req.url;
|
|
1301
|
+
if (response.ok && /\/jwks(\?|$)/.test(url)) {
|
|
1302
|
+
const existing = response.headers.get("cache-control");
|
|
1303
|
+
if (!existing) {
|
|
1304
|
+
response.headers.set(
|
|
1305
|
+
"cache-control",
|
|
1306
|
+
"public, max-age=300, stale-while-revalidate=86400"
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
} catch {
|
|
1311
|
+
}
|
|
1000
1312
|
return response;
|
|
1001
1313
|
} catch (error) {
|
|
1002
1314
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -1031,8 +1343,19 @@ var AuthPlugin = class {
|
|
|
1031
1343
|
const { oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata } = await import("@better-auth/oauth-provider");
|
|
1032
1344
|
const authServerHandler = oauthProviderAuthServerMetadata(auth);
|
|
1033
1345
|
const openidConfigHandler = oauthProviderOpenIdConfigMetadata(auth);
|
|
1034
|
-
|
|
1035
|
-
|
|
1346
|
+
const DISCOVERY_CACHE = "public, max-age=300, stale-while-revalidate=86400";
|
|
1347
|
+
const withDiscoveryCache = async (handler, req) => {
|
|
1348
|
+
const resp = await handler(req);
|
|
1349
|
+
try {
|
|
1350
|
+
if (resp.ok && !resp.headers.get("cache-control")) {
|
|
1351
|
+
resp.headers.set("cache-control", DISCOVERY_CACHE);
|
|
1352
|
+
}
|
|
1353
|
+
} catch {
|
|
1354
|
+
}
|
|
1355
|
+
return resp;
|
|
1356
|
+
};
|
|
1357
|
+
rawApp.get("/.well-known/oauth-authorization-server", (c) => withDiscoveryCache(authServerHandler, c.req.raw));
|
|
1358
|
+
rawApp.get("/.well-known/openid-configuration", (c) => withDiscoveryCache(openidConfigHandler, c.req.raw));
|
|
1036
1359
|
ctx.logger.info(
|
|
1037
1360
|
"OIDC discovery endpoints mounted at /.well-known/{oauth-authorization-server,openid-configuration}"
|
|
1038
1361
|
);
|
|
@@ -1040,6 +1363,8 @@ var AuthPlugin = class {
|
|
|
1040
1363
|
};
|
|
1041
1364
|
export {
|
|
1042
1365
|
AUTH_ACCOUNT_CONFIG,
|
|
1366
|
+
AUTH_ADMIN_SESSION_FIELDS,
|
|
1367
|
+
AUTH_ADMIN_USER_FIELDS,
|
|
1043
1368
|
AUTH_DEVICE_CODE_SCHEMA,
|
|
1044
1369
|
AUTH_INVITATION_SCHEMA,
|
|
1045
1370
|
AUTH_JWKS_SCHEMA,
|
|
@@ -1061,6 +1386,7 @@ export {
|
|
|
1061
1386
|
AUTH_VERIFICATION_CONFIG,
|
|
1062
1387
|
AuthManager,
|
|
1063
1388
|
AuthPlugin,
|
|
1389
|
+
buildAdminPluginSchema,
|
|
1064
1390
|
buildDeviceAuthorizationPluginSchema,
|
|
1065
1391
|
buildJwtPluginSchema,
|
|
1066
1392
|
buildOauthProviderPluginSchema,
|