@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.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
- // ObjectQL natively supports these types no extra conversion needed
55
- supportsBooleans: true,
56
- supportsDates: true,
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.sendOnSignUp != null ? { sendOnSignUp: this.config.emailVerification.sendOnSignUp } : {},
472
- ...this.config.emailVerification.sendOnSignIn != null ? { sendOnSignIn: this.config.emailVerification.sendOnSignIn } : {},
473
- ...this.config.emailVerification.autoSignInAfterVerification != null ? { autoSignInAfterVerification: this.config.emailVerification.autoSignInAfterVerification } : {},
474
- ...this.config.emailVerification.expiresIn != null ? { expiresIn: this.config.emailVerification.expiresIn } : {}
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
- console.warn(
553
- `[AuthManager] Invitation email not configured. To: ${email} (org: ${org?.name ?? invitation.organizationId}, role: ${invitation.role}, inviter: ${inviter?.user?.email ?? "unknown"}) URL: ${acceptUrl}`
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
- console.warn(
569
- `[AuthManager] Magic-link requested for ${email} but no sendMagicLink handler configured. URL: ${url}`
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
- // Curated list views and dashboards consumed by the Setup App's
873
- // navigation entries. The manifest service does NOT auto-discover
874
- // these from the app definition — they must be registered as
875
- // explicit top-level arrays per ObjectStackDefinitionSchema.
876
- views: [
877
- UsersView,
878
- OrganizationsView,
879
- RolesView,
880
- SessionsView,
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
- rawApp.get("/.well-known/oauth-authorization-server", (c) => authServerHandler(c.req.raw));
1035
- rawApp.get("/.well-known/openid-configuration", (c) => openidConfigHandler(c.req.raw));
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,