@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.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
- // ObjectQL natively supports these types no extra conversion needed
111
- supportsBooleans: true,
112
- supportsDates: true,
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.sendOnSignUp != null ? { sendOnSignUp: this.config.emailVerification.sendOnSignUp } : {},
528
- ...this.config.emailVerification.sendOnSignIn != null ? { sendOnSignIn: this.config.emailVerification.sendOnSignIn } : {},
529
- ...this.config.emailVerification.autoSignInAfterVerification != null ? { autoSignInAfterVerification: this.config.emailVerification.autoSignInAfterVerification } : {},
530
- ...this.config.emailVerification.expiresIn != null ? { expiresIn: this.config.emailVerification.expiresIn } : {}
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
- console.warn(
609
- `[AuthManager] Invitation email not configured. To: ${email} (org: ${org?.name ?? invitation.organizationId}, role: ${invitation.role}, inviter: ${inviter?.user?.email ?? "unknown"}) URL: ${acceptUrl}`
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
- console.warn(
625
- `[AuthManager] Magic-link requested for ${email} but no sendMagicLink handler configured. URL: ${url}`
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
- // Curated list views and dashboards consumed by the Setup App's
910
- // navigation entries. The manifest service does NOT auto-discover
911
- // these from the app definition — they must be registered as
912
- // explicit top-level arrays per ObjectStackDefinitionSchema.
913
- views: [
914
- import_apps.UsersView,
915
- import_apps.OrganizationsView,
916
- import_apps.RolesView,
917
- import_apps.SessionsView,
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
- rawApp.get("/.well-known/oauth-authorization-server", (c) => authServerHandler(c.req.raw));
1072
- rawApp.get("/.well-known/openid-configuration", (c) => openidConfigHandler(c.req.raw));
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,