@objectstack/plugin-auth 4.0.4 → 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.
Files changed (40) hide show
  1. package/README.md +4 -1
  2. package/dist/index.d.mts +441 -19940
  3. package/dist/index.d.ts +441 -19940
  4. package/dist/index.js +704 -900
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +699 -880
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +35 -12
  9. package/.turbo/turbo-build.log +0 -78
  10. package/ARCHITECTURE.md +0 -176
  11. package/CHANGELOG.md +0 -333
  12. package/IMPLEMENTATION_SUMMARY.md +0 -192
  13. package/examples/basic-usage.ts +0 -107
  14. package/objectstack.config.ts +0 -24
  15. package/src/auth-manager.test.ts +0 -883
  16. package/src/auth-manager.ts +0 -419
  17. package/src/auth-plugin.test.ts +0 -446
  18. package/src/auth-plugin.ts +0 -314
  19. package/src/auth-schema-config.ts +0 -339
  20. package/src/index.ts +0 -16
  21. package/src/objectql-adapter.test.ts +0 -281
  22. package/src/objectql-adapter.ts +0 -279
  23. package/src/objects/auth-account.object.ts +0 -7
  24. package/src/objects/auth-session.object.ts +0 -7
  25. package/src/objects/auth-user.object.ts +0 -7
  26. package/src/objects/auth-verification.object.ts +0 -7
  27. package/src/objects/index.ts +0 -40
  28. package/src/objects/sys-account.object.ts +0 -111
  29. package/src/objects/sys-api-key.object.ts +0 -104
  30. package/src/objects/sys-invitation.object.ts +0 -93
  31. package/src/objects/sys-member.object.ts +0 -68
  32. package/src/objects/sys-organization.object.ts +0 -82
  33. package/src/objects/sys-session.object.ts +0 -84
  34. package/src/objects/sys-team-member.object.ts +0 -61
  35. package/src/objects/sys-team.object.ts +0 -69
  36. package/src/objects/sys-two-factor.object.ts +0 -73
  37. package/src/objects/sys-user-preference.object.ts +0 -82
  38. package/src/objects/sys-user.object.ts +0 -91
  39. package/src/objects/sys-verification.object.ts +0 -75
  40. package/tsconfig.json +0 -18
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,15 +17,32 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
21
31
  var index_exports = {};
22
32
  __export(index_exports, {
23
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,
36
+ AUTH_DEVICE_CODE_SCHEMA: () => AUTH_DEVICE_CODE_SCHEMA,
24
37
  AUTH_INVITATION_SCHEMA: () => AUTH_INVITATION_SCHEMA,
38
+ AUTH_JWKS_SCHEMA: () => AUTH_JWKS_SCHEMA,
25
39
  AUTH_MEMBER_SCHEMA: () => AUTH_MEMBER_SCHEMA,
26
40
  AUTH_MODEL_TO_PROTOCOL: () => AUTH_MODEL_TO_PROTOCOL,
41
+ AUTH_OAUTH_ACCESS_TOKEN_SCHEMA: () => AUTH_OAUTH_ACCESS_TOKEN_SCHEMA,
42
+ AUTH_OAUTH_APPLICATION_SCHEMA: () => AUTH_OAUTH_APPLICATION_SCHEMA,
43
+ AUTH_OAUTH_CLIENT_SCHEMA: () => AUTH_OAUTH_CLIENT_SCHEMA,
44
+ AUTH_OAUTH_CONSENT_SCHEMA: () => AUTH_OAUTH_CONSENT_SCHEMA,
45
+ AUTH_OAUTH_REFRESH_TOKEN_SCHEMA: () => AUTH_OAUTH_REFRESH_TOKEN_SCHEMA,
27
46
  AUTH_ORGANIZATION_SCHEMA: () => AUTH_ORGANIZATION_SCHEMA,
28
47
  AUTH_ORG_SESSION_FIELDS: () => AUTH_ORG_SESSION_FIELDS,
29
48
  AUTH_SESSION_CONFIG: () => AUTH_SESSION_CONFIG,
@@ -33,24 +52,13 @@ __export(index_exports, {
33
52
  AUTH_TWO_FACTOR_USER_FIELDS: () => AUTH_TWO_FACTOR_USER_FIELDS,
34
53
  AUTH_USER_CONFIG: () => AUTH_USER_CONFIG,
35
54
  AUTH_VERIFICATION_CONFIG: () => AUTH_VERIFICATION_CONFIG,
36
- AuthAccount: () => SysAccount,
37
55
  AuthManager: () => AuthManager,
38
56
  AuthPlugin: () => AuthPlugin,
39
- AuthSession: () => SysSession,
40
- AuthUser: () => SysUser,
41
- AuthVerification: () => SysVerification,
42
- SysAccount: () => SysAccount,
43
- SysApiKey: () => SysApiKey,
44
- SysInvitation: () => SysInvitation,
45
- SysMember: () => SysMember,
46
- SysOrganization: () => SysOrganization,
47
- SysSession: () => SysSession,
48
- SysTeam: () => SysTeam,
49
- SysTeamMember: () => SysTeamMember,
50
- SysTwoFactor: () => SysTwoFactor,
51
- SysUser: () => SysUser,
52
- SysUserPreference: () => SysUserPreference,
53
- SysVerification: () => SysVerification,
57
+ buildAdminPluginSchema: () => buildAdminPluginSchema,
58
+ buildDeviceAuthorizationPluginSchema: () => buildDeviceAuthorizationPluginSchema,
59
+ buildJwtPluginSchema: () => buildJwtPluginSchema,
60
+ buildOauthProviderPluginSchema: () => buildOauthProviderPluginSchema,
61
+ buildOidcProviderPluginSchema: () => buildOidcProviderPluginSchema,
54
62
  buildOrganizationPluginSchema: () => buildOrganizationPluginSchema,
55
63
  buildTwoFactorPluginSchema: () => buildTwoFactorPluginSchema,
56
64
  createObjectQLAdapter: () => createObjectQLAdapter,
@@ -59,11 +67,8 @@ __export(index_exports, {
59
67
  });
60
68
  module.exports = __toCommonJS(index_exports);
61
69
 
62
- // src/auth-manager.ts
63
- var import_better_auth = require("better-auth");
64
- var import_organization = require("better-auth/plugins/organization");
65
- var import_two_factor = require("better-auth/plugins/two-factor");
66
- var import_magic_link = require("better-auth/plugins/magic-link");
70
+ // src/auth-plugin.ts
71
+ var import_apps = require("@objectstack/platform-objects/apps");
67
72
 
68
73
  // src/objectql-adapter.ts
69
74
  var import_adapters = require("better-auth/adapters");
@@ -77,6 +82,41 @@ var AUTH_MODEL_TO_PROTOCOL = {
77
82
  function resolveProtocolName(model) {
78
83
  return AUTH_MODEL_TO_PROTOCOL[model] ?? model;
79
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
+ }
80
120
  function convertWhere(where) {
81
121
  const filter = {};
82
122
  for (const condition of where) {
@@ -105,20 +145,24 @@ function createObjectQLAdapterFactory(dataEngine) {
105
145
  return (0, import_adapters.createAdapterFactory)({
106
146
  config: {
107
147
  adapterId: "objectql",
108
- // ObjectQL natively supports these types no extra conversion needed
109
- supportsBooleans: true,
110
- 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,
111
155
  supportsJSON: true
112
156
  },
113
157
  adapter: () => ({
114
158
  create: async ({ model, data, select: _select }) => {
115
159
  const result = await dataEngine.insert(model, data);
116
- return result;
160
+ return normaliseLegacyDates(model, result);
117
161
  },
118
162
  findOne: async ({ model, where, select, join: _join }) => {
119
163
  const filter = convertWhere(where);
120
164
  const result = await dataEngine.findOne(model, { where: filter, fields: select });
121
- return result ? result : null;
165
+ return result ? normaliseLegacyDates(model, result) : null;
122
166
  },
123
167
  findMany: async ({ model, where, limit, offset, sortBy, join: _join }) => {
124
168
  const filter = where ? convertWhere(where) : {};
@@ -129,7 +173,7 @@ function createObjectQLAdapterFactory(dataEngine) {
129
173
  offset,
130
174
  orderBy
131
175
  });
132
- return results;
176
+ return results.map((r) => normaliseLegacyDates(model, r));
133
177
  },
134
178
  count: async ({ model, where }) => {
135
179
  const filter = where ? convertWhere(where) : {};
@@ -140,7 +184,7 @@ function createObjectQLAdapterFactory(dataEngine) {
140
184
  const record = await dataEngine.findOne(model, { where: filter });
141
185
  if (!record) return null;
142
186
  const result = await dataEngine.update(model, { ...update, id: record.id });
143
- return result ? result : null;
187
+ return result ? normaliseLegacyDates(model, result) : null;
144
188
  },
145
189
  updateMany: async ({ model, where, update }) => {
146
190
  const filter = convertWhere(where);
@@ -337,6 +381,88 @@ var AUTH_TWO_FACTOR_SCHEMA = {
337
381
  var AUTH_TWO_FACTOR_USER_FIELDS = {
338
382
  twoFactorEnabled: "two_factor_enabled"
339
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
+ };
391
+ var AUTH_OAUTH_CLIENT_SCHEMA = {
392
+ modelName: import_system2.SystemObjectName.OAUTH_APPLICATION,
393
+ // 'sys_oauth_application'
394
+ fields: {
395
+ clientId: "client_id",
396
+ clientSecret: "client_secret",
397
+ skipConsent: "skip_consent",
398
+ enableEndSession: "enable_end_session",
399
+ subjectType: "subject_type",
400
+ userId: "user_id",
401
+ createdAt: "created_at",
402
+ updatedAt: "updated_at",
403
+ redirectUris: "redirect_uris",
404
+ postLogoutRedirectUris: "post_logout_redirect_uris",
405
+ tokenEndpointAuthMethod: "token_endpoint_auth_method",
406
+ grantTypes: "grant_types",
407
+ responseTypes: "response_types",
408
+ requirePKCE: "require_pkce",
409
+ softwareId: "software_id",
410
+ softwareVersion: "software_version",
411
+ softwareStatement: "software_statement",
412
+ referenceId: "reference_id"
413
+ }
414
+ };
415
+ var AUTH_OAUTH_APPLICATION_SCHEMA = AUTH_OAUTH_CLIENT_SCHEMA;
416
+ var AUTH_OAUTH_ACCESS_TOKEN_SCHEMA = {
417
+ modelName: import_system2.SystemObjectName.OAUTH_ACCESS_TOKEN,
418
+ // 'sys_oauth_access_token'
419
+ fields: {
420
+ clientId: "client_id",
421
+ sessionId: "session_id",
422
+ userId: "user_id",
423
+ referenceId: "reference_id",
424
+ refreshId: "refresh_id",
425
+ expiresAt: "expires_at",
426
+ createdAt: "created_at"
427
+ }
428
+ };
429
+ var AUTH_OAUTH_REFRESH_TOKEN_SCHEMA = {
430
+ modelName: import_system2.SystemObjectName.OAUTH_REFRESH_TOKEN,
431
+ // 'sys_oauth_refresh_token'
432
+ fields: {
433
+ clientId: "client_id",
434
+ sessionId: "session_id",
435
+ userId: "user_id",
436
+ referenceId: "reference_id",
437
+ expiresAt: "expires_at",
438
+ createdAt: "created_at",
439
+ authTime: "auth_time"
440
+ }
441
+ };
442
+ var AUTH_OAUTH_CONSENT_SCHEMA = {
443
+ modelName: import_system2.SystemObjectName.OAUTH_CONSENT,
444
+ // 'sys_oauth_consent'
445
+ fields: {
446
+ clientId: "client_id",
447
+ userId: "user_id",
448
+ referenceId: "reference_id",
449
+ createdAt: "created_at",
450
+ updatedAt: "updated_at"
451
+ }
452
+ };
453
+ var AUTH_DEVICE_CODE_SCHEMA = {
454
+ modelName: import_system2.SystemObjectName.DEVICE_CODE,
455
+ // 'sys_device_code'
456
+ fields: {
457
+ deviceCode: "device_code",
458
+ userCode: "user_code",
459
+ userId: "user_id",
460
+ expiresAt: "expires_at",
461
+ lastPolledAt: "last_polled_at",
462
+ pollingInterval: "polling_interval",
463
+ clientId: "client_id"
464
+ }
465
+ };
340
466
  function buildTwoFactorPluginSchema() {
341
467
  return {
342
468
  twoFactor: AUTH_TWO_FACTOR_SCHEMA,
@@ -345,6 +471,16 @@ function buildTwoFactorPluginSchema() {
345
471
  }
346
472
  };
347
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
+ }
348
484
  function buildOrganizationPluginSchema() {
349
485
  return {
350
486
  organization: AUTH_ORGANIZATION_SCHEMA,
@@ -357,6 +493,35 @@ function buildOrganizationPluginSchema() {
357
493
  }
358
494
  };
359
495
  }
496
+ var AUTH_JWKS_SCHEMA = {
497
+ modelName: import_system2.SystemObjectName.JWKS,
498
+ // 'sys_jwks'
499
+ fields: {
500
+ publicKey: "public_key",
501
+ privateKey: "private_key",
502
+ createdAt: "created_at",
503
+ expiresAt: "expires_at"
504
+ }
505
+ };
506
+ function buildJwtPluginSchema() {
507
+ return {
508
+ jwks: AUTH_JWKS_SCHEMA
509
+ };
510
+ }
511
+ function buildOauthProviderPluginSchema() {
512
+ return {
513
+ oauthClient: AUTH_OAUTH_CLIENT_SCHEMA,
514
+ oauthAccessToken: AUTH_OAUTH_ACCESS_TOKEN_SCHEMA,
515
+ oauthRefreshToken: AUTH_OAUTH_REFRESH_TOKEN_SCHEMA,
516
+ oauthConsent: AUTH_OAUTH_CONSENT_SCHEMA
517
+ };
518
+ }
519
+ var buildOidcProviderPluginSchema = buildOauthProviderPluginSchema;
520
+ function buildDeviceAuthorizationPluginSchema() {
521
+ return {
522
+ deviceCode: AUTH_DEVICE_CODE_SCHEMA
523
+ };
524
+ }
360
525
 
361
526
  // src/auth-manager.ts
362
527
  var AuthManager = class {
@@ -370,16 +535,18 @@ var AuthManager = class {
370
535
  /**
371
536
  * Get or create the better-auth instance (lazy initialization)
372
537
  */
373
- getOrCreateAuth() {
538
+ async getOrCreateAuth() {
374
539
  if (!this.auth) {
375
- this.auth = this.createAuthInstance();
540
+ this.auth = await this.createAuthInstance();
376
541
  }
377
542
  return this.auth;
378
543
  }
379
544
  /**
380
545
  * Create a better-auth instance from configuration
381
546
  */
382
- createAuthInstance() {
547
+ async createAuthInstance() {
548
+ const { betterAuth } = await import("better-auth");
549
+ const plugins = await this.buildPluginList();
383
550
  const betterAuthConfig = {
384
551
  // Base configuration
385
552
  secret: this.config.secret || this.generateSecret(),
@@ -395,7 +562,41 @@ var AuthManager = class {
395
562
  ...AUTH_USER_CONFIG
396
563
  },
397
564
  account: {
398
- ...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
+ }
399
600
  },
400
601
  verification: {
401
602
  ...AUTH_VERIFICATION_CONFIG
@@ -411,15 +612,71 @@ var AuthManager = class {
411
612
  ...this.config.emailAndPassword?.maxPasswordLength != null ? { maxPasswordLength: this.config.emailAndPassword.maxPasswordLength } : {},
412
613
  ...this.config.emailAndPassword?.resetPasswordTokenExpiresIn != null ? { resetPasswordTokenExpiresIn: this.config.emailAndPassword.resetPasswordTokenExpiresIn } : {},
413
614
  ...this.config.emailAndPassword?.autoSignIn != null ? { autoSignIn: this.config.emailAndPassword.autoSignIn } : {},
414
- ...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
+ }
415
644
  },
416
645
  // Email verification
417
- ...this.config.emailVerification ? {
646
+ ...this.config.emailVerification || this.config.emailService ? {
418
647
  emailVerification: {
419
- ...this.config.emailVerification.sendOnSignUp != null ? { sendOnSignUp: this.config.emailVerification.sendOnSignUp } : {},
420
- ...this.config.emailVerification.sendOnSignIn != null ? { sendOnSignIn: this.config.emailVerification.sendOnSignIn } : {},
421
- ...this.config.emailVerification.autoSignInAfterVerification != null ? { autoSignInAfterVerification: this.config.emailVerification.autoSignInAfterVerification } : {},
422
- ...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
+ }
423
680
  }
424
681
  } : {},
425
682
  // Session configuration
@@ -431,7 +688,7 @@ var AuthManager = class {
431
688
  // 1 day default
432
689
  },
433
690
  // better-auth plugins — registered based on AuthPluginConfig flags
434
- plugins: this.buildPluginList(),
691
+ plugins,
435
692
  // Trusted origins for CSRF protection (supports wildcards like "https://*.example.com")
436
693
  // Auto-includes origins from CORS_ORIGIN env var so CORS and CSRF stay in sync.
437
694
  ...(() => {
@@ -444,6 +701,8 @@ var AuthManager = class {
444
701
  }
445
702
  if (!origins.length && (!corsOrigin || corsOrigin === "*")) {
446
703
  origins.push("http://localhost:*");
704
+ origins.push("http://*.localhost:*");
705
+ origins.push("https://*.localhost:*");
447
706
  }
448
707
  return origins.length ? { trustedOrigins: origins } : {};
449
708
  })(),
@@ -457,7 +716,7 @@ var AuthManager = class {
457
716
  }
458
717
  } : {}
459
718
  };
460
- return (0, import_better_auth.betterAuth)(betterAuthConfig);
719
+ return betterAuth(betterAuthConfig);
461
720
  }
462
721
  /**
463
722
  * Build the list of better-auth plugins based on AuthPluginConfig flags.
@@ -466,28 +725,224 @@ var AuthManager = class {
466
725
  * a `schema` option containing the appropriate snake_case field mappings,
467
726
  * so that `createAdapterFactory` transforms them automatically.
468
727
  */
469
- buildPluginList() {
470
- const pluginConfig = this.config.plugins;
728
+ async buildPluginList() {
729
+ const pluginConfig = this.config.plugins ?? {};
471
730
  const plugins = [];
472
- if (pluginConfig?.organization) {
473
- plugins.push((0, import_organization.organization)({
474
- schema: buildOrganizationPluginSchema()
731
+ const enabled = {
732
+ organization: pluginConfig.organization ?? true,
733
+ twoFactor: pluginConfig.twoFactor ?? false,
734
+ passkeys: pluginConfig.passkeys ?? false,
735
+ magicLink: pluginConfig.magicLink ?? false,
736
+ oidcProvider: pluginConfig.oidcProvider ?? false,
737
+ deviceAuthorization: pluginConfig.deviceAuthorization ?? false,
738
+ admin: pluginConfig.admin ?? false
739
+ };
740
+ const { bearer } = await import("better-auth/plugins/bearer");
741
+ plugins.push(bearer());
742
+ if (enabled.organization) {
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
+ }
765
+ plugins.push(organization({
766
+ schema: buildOrganizationPluginSchema(),
767
+ // Enable the team sub-feature so the framework's `sys_team` /
768
+ // `sys_team_member` tables (already declared in platform-objects)
769
+ // are actually wired up to better-auth's CRUD endpoints
770
+ // (`/organization/{create,update,remove,list}-team[s]` and
771
+ // `/organization/{add,remove,list}-team-member[s]`). The Account
772
+ // portal exposes a Teams page; without this flag those endpoints
773
+ // 404 and the section silently breaks.
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 } : {},
782
+ // No mailer is wired in framework yet — log the accept URL so
783
+ // operators / UI can fall back to copy-paste flows. Replace this
784
+ // with a real mail integration when available.
785
+ sendInvitationEmail: async ({ email: recipientEmail, invitation, organization: org, inviter }) => {
786
+ const baseUrl = (this.config.baseUrl ?? "").replace(/\/$/, "");
787
+ const acceptUrl = `${baseUrl}/accept-invitation/${invitation.id}`;
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
+ }
816
+ }
475
817
  }));
476
818
  }
477
- if (pluginConfig?.twoFactor) {
478
- plugins.push((0, import_two_factor.twoFactor)({
819
+ if (enabled.twoFactor) {
820
+ const { twoFactor } = await import("better-auth/plugins/two-factor");
821
+ plugins.push(twoFactor({
479
822
  schema: buildTwoFactorPluginSchema()
480
823
  }));
481
824
  }
482
- if (pluginConfig?.magicLink) {
483
- plugins.push((0, import_magic_link.magicLink)({
484
- sendMagicLink: async ({ email, url }) => {
485
- console.warn(
486
- `[AuthManager] Magic-link requested for ${email} but no sendMagicLink handler configured. URL: ${url}`
487
- );
825
+ if (enabled.admin) {
826
+ const { admin } = await import("better-auth/plugins/admin");
827
+ plugins.push(admin({
828
+ schema: buildAdminPluginSchema()
829
+ }));
830
+ }
831
+ if (enabled.magicLink) {
832
+ const { magicLink } = await import("better-auth/plugins/magic-link");
833
+ plugins.push(magicLink({
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
+ }
488
857
  }
489
858
  }));
490
859
  }
860
+ if (this.config.oidcProviders?.length) {
861
+ const { genericOAuth } = await import("better-auth/plugins/generic-oauth");
862
+ plugins.push(genericOAuth({
863
+ config: this.config.oidcProviders.map((p) => ({
864
+ providerId: p.providerId,
865
+ ...p.discoveryUrl ? { discoveryUrl: p.discoveryUrl } : {},
866
+ ...p.issuer ? { issuer: p.issuer } : {},
867
+ ...p.authorizationUrl ? { authorizationUrl: p.authorizationUrl } : {},
868
+ ...p.tokenUrl ? { tokenUrl: p.tokenUrl } : {},
869
+ ...p.userInfoUrl ? { userInfoUrl: p.userInfoUrl } : {},
870
+ clientId: p.clientId,
871
+ clientSecret: p.clientSecret,
872
+ ...p.scopes ? { scopes: p.scopes } : {},
873
+ ...p.pkce != null ? { pkce: p.pkce } : {}
874
+ }))
875
+ }));
876
+ }
877
+ if (enabled.oidcProvider) {
878
+ const { jwt } = await import("better-auth/plugins");
879
+ plugins.push(jwt({ schema: buildJwtPluginSchema() }));
880
+ const { oauthProvider } = await import("@better-auth/oauth-provider");
881
+ const baseUrl = (this.config.baseUrl ?? "").replace(/\/$/, "");
882
+ plugins.push(oauthProvider({
883
+ // Account SPA renders both pages — see apps/account.
884
+ loginPage: `${baseUrl}/_account/login`,
885
+ consentPage: `${baseUrl}/_account/oauth/consent`,
886
+ schema: buildOauthProviderPluginSchema()
887
+ }));
888
+ }
889
+ if (enabled.deviceAuthorization) {
890
+ const { deviceAuthorization } = await import("better-auth/plugins/device-authorization");
891
+ const baseUrl = (this.config.baseUrl ?? "").replace(/\/$/, "");
892
+ plugins.push(deviceAuthorization({
893
+ verificationUri: `${baseUrl}/_account/auth/device`,
894
+ schema: buildDeviceAuthorizationPluginSchema()
895
+ }));
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
+ }
491
946
  return plugins;
492
947
  }
493
948
  /**
@@ -544,6 +999,26 @@ var AuthManager = class {
544
999
  }
545
1000
  this.config = { ...this.config, baseUrl: url };
546
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
+ }
547
1022
  /**
548
1023
  * Get the underlying better-auth instance
549
1024
  * Useful for advanced use cases
@@ -563,7 +1038,7 @@ var AuthManager = class {
563
1038
  * @returns Web standard Response object
564
1039
  */
565
1040
  async handleRequest(request) {
566
- const auth = this.getOrCreateAuth();
1041
+ const auth = await this.getOrCreateAuth();
567
1042
  const response = await auth.handler(request);
568
1043
  if (response.status >= 500) {
569
1044
  try {
@@ -579,18 +1054,19 @@ var AuthManager = class {
579
1054
  * Get the better-auth API for programmatic access
580
1055
  * Use this for server-side operations (e.g., creating users, checking sessions)
581
1056
  */
582
- get api() {
583
- return this.getOrCreateAuth().api;
1057
+ async getApi() {
1058
+ const auth = await this.getOrCreateAuth();
1059
+ return auth.api;
584
1060
  }
585
- /**
586
- * Get public authentication configuration
587
- * Returns safe, non-sensitive configuration that can be exposed to the frontend
588
- *
589
- * This allows the frontend to discover:
590
- * - Which social/OAuth providers are available
591
- * - Whether email/password login is enabled
592
- * - Which advanced features are enabled (2FA, magic links, etc.)
593
- */
1061
+ // ---------------------------------------------------------------------------
1062
+ // Device Flow (CLI browser-based login)
1063
+ //
1064
+ // The device authorization flow (RFC 8628) is now handled entirely by
1065
+ // better-auth's `device-authorization` plugin. Endpoints are exposed at
1066
+ // `${basePath}/device/{code,token,approve,deny}` and persisted in
1067
+ // `sys_device_code`. Enable via `plugins.deviceAuthorization: true` in
1068
+ // AuthPluginConfig.
1069
+ // ---------------------------------------------------------------------------
594
1070
  getPublicConfig() {
595
1071
  const socialProviders = [];
596
1072
  if (this.config.socialProviders) {
@@ -610,11 +1086,22 @@ var AuthManager = class {
610
1086
  socialProviders.push({
611
1087
  id,
612
1088
  name: nameMap[id] || id.charAt(0).toUpperCase() + id.slice(1),
613
- enabled: true
1089
+ enabled: true,
1090
+ type: "social"
614
1091
  });
615
1092
  }
616
1093
  }
617
1094
  }
1095
+ if (this.config.oidcProviders?.length) {
1096
+ for (const p of this.config.oidcProviders) {
1097
+ socialProviders.push({
1098
+ id: p.providerId,
1099
+ name: p.name ?? p.providerId.charAt(0).toUpperCase() + p.providerId.slice(1),
1100
+ enabled: true,
1101
+ type: "oidc"
1102
+ });
1103
+ }
1104
+ }
618
1105
  const emailPasswordConfig = this.config.emailAndPassword ?? {};
619
1106
  const emailPassword = {
620
1107
  enabled: emailPasswordConfig.enabled !== false,
@@ -627,7 +1114,9 @@ var AuthManager = class {
627
1114
  twoFactor: pluginConfig.twoFactor ?? false,
628
1115
  passkeys: pluginConfig.passkeys ?? false,
629
1116
  magicLink: pluginConfig.magicLink ?? false,
630
- organization: pluginConfig.organization ?? false
1117
+ organization: pluginConfig.organization ?? true,
1118
+ oidcProvider: pluginConfig.oidcProvider ?? false,
1119
+ deviceAuthorization: pluginConfig.deviceAuthorization ?? false
631
1120
  };
632
1121
  return {
633
1122
  emailPassword,
@@ -635,776 +1124,50 @@ var AuthManager = class {
635
1124
  features
636
1125
  };
637
1126
  }
638
- };
639
-
640
- // src/objects/sys-user.object.ts
641
- var import_data = require("@objectstack/spec/data");
642
- var SysUser = import_data.ObjectSchema.create({
643
- namespace: "sys",
644
- name: "user",
645
- label: "User",
646
- pluralLabel: "Users",
647
- icon: "user",
648
- isSystem: true,
649
- description: "User accounts for authentication",
650
- titleFormat: "{name} ({email})",
651
- compactLayout: ["name", "email", "email_verified"],
652
- fields: {
653
- id: import_data.Field.text({
654
- label: "User ID",
655
- required: true,
656
- readonly: true
657
- }),
658
- created_at: import_data.Field.datetime({
659
- label: "Created At",
660
- defaultValue: "NOW()",
661
- readonly: true
662
- }),
663
- updated_at: import_data.Field.datetime({
664
- label: "Updated At",
665
- defaultValue: "NOW()",
666
- readonly: true
667
- }),
668
- email: import_data.Field.email({
669
- label: "Email",
670
- required: true,
671
- searchable: true
672
- }),
673
- email_verified: import_data.Field.boolean({
674
- label: "Email Verified",
675
- defaultValue: false
676
- }),
677
- name: import_data.Field.text({
678
- label: "Name",
679
- required: true,
680
- searchable: true,
681
- maxLength: 255
682
- }),
683
- image: import_data.Field.url({
684
- label: "Profile Image",
685
- required: false
686
- })
687
- },
688
- indexes: [
689
- { fields: ["email"], unique: true },
690
- { fields: ["created_at"], unique: false }
691
- ],
692
- enable: {
693
- trackHistory: true,
694
- searchable: true,
695
- apiEnabled: true,
696
- apiMethods: ["get", "list", "create", "update", "delete"],
697
- trash: true,
698
- mru: true
699
- },
700
- validations: [
701
- {
702
- name: "email_unique",
703
- type: "unique",
704
- severity: "error",
705
- message: "Email must be unique",
706
- fields: ["email"],
707
- caseSensitive: false
708
- }
709
- ]
710
- });
711
-
712
- // src/objects/sys-session.object.ts
713
- var import_data2 = require("@objectstack/spec/data");
714
- var SysSession = import_data2.ObjectSchema.create({
715
- namespace: "sys",
716
- name: "session",
717
- label: "Session",
718
- pluralLabel: "Sessions",
719
- icon: "key",
720
- isSystem: true,
721
- description: "Active user sessions",
722
- titleFormat: "Session {token}",
723
- compactLayout: ["user_id", "expires_at", "ip_address"],
724
- fields: {
725
- id: import_data2.Field.text({
726
- label: "Session ID",
727
- required: true,
728
- readonly: true
729
- }),
730
- created_at: import_data2.Field.datetime({
731
- label: "Created At",
732
- defaultValue: "NOW()",
733
- readonly: true
734
- }),
735
- updated_at: import_data2.Field.datetime({
736
- label: "Updated At",
737
- defaultValue: "NOW()",
738
- readonly: true
739
- }),
740
- user_id: import_data2.Field.text({
741
- label: "User ID",
742
- required: true
743
- }),
744
- expires_at: import_data2.Field.datetime({
745
- label: "Expires At",
746
- required: true
747
- }),
748
- token: import_data2.Field.text({
749
- label: "Session Token",
750
- required: true
751
- }),
752
- ip_address: import_data2.Field.text({
753
- label: "IP Address",
754
- required: false,
755
- maxLength: 45
756
- // Support IPv6
757
- }),
758
- user_agent: import_data2.Field.textarea({
759
- label: "User Agent",
760
- required: false
761
- })
762
- },
763
- indexes: [
764
- { fields: ["token"], unique: true },
765
- { fields: ["user_id"], unique: false },
766
- { fields: ["expires_at"], unique: false }
767
- ],
768
- enable: {
769
- trackHistory: false,
770
- searchable: false,
771
- apiEnabled: true,
772
- apiMethods: ["get", "list", "create", "delete"],
773
- trash: false,
774
- mru: false
775
- }
776
- });
777
-
778
- // src/objects/sys-account.object.ts
779
- var import_data3 = require("@objectstack/spec/data");
780
- var SysAccount = import_data3.ObjectSchema.create({
781
- namespace: "sys",
782
- name: "account",
783
- label: "Account",
784
- pluralLabel: "Accounts",
785
- icon: "link",
786
- isSystem: true,
787
- description: "OAuth and authentication provider accounts",
788
- titleFormat: "{provider_id} - {account_id}",
789
- compactLayout: ["provider_id", "user_id", "account_id"],
790
- fields: {
791
- id: import_data3.Field.text({
792
- label: "Account ID",
793
- required: true,
794
- readonly: true
795
- }),
796
- created_at: import_data3.Field.datetime({
797
- label: "Created At",
798
- defaultValue: "NOW()",
799
- readonly: true
800
- }),
801
- updated_at: import_data3.Field.datetime({
802
- label: "Updated At",
803
- defaultValue: "NOW()",
804
- readonly: true
805
- }),
806
- provider_id: import_data3.Field.text({
807
- label: "Provider ID",
808
- required: true,
809
- description: "OAuth provider identifier (google, github, etc.)"
810
- }),
811
- account_id: import_data3.Field.text({
812
- label: "Provider Account ID",
813
- required: true,
814
- description: "User's ID in the provider's system"
815
- }),
816
- user_id: import_data3.Field.text({
817
- label: "User ID",
818
- required: true,
819
- description: "Link to user table"
820
- }),
821
- access_token: import_data3.Field.textarea({
822
- label: "Access Token",
823
- required: false
824
- }),
825
- refresh_token: import_data3.Field.textarea({
826
- label: "Refresh Token",
827
- required: false
828
- }),
829
- id_token: import_data3.Field.textarea({
830
- label: "ID Token",
831
- required: false
832
- }),
833
- access_token_expires_at: import_data3.Field.datetime({
834
- label: "Access Token Expires At",
835
- required: false
836
- }),
837
- refresh_token_expires_at: import_data3.Field.datetime({
838
- label: "Refresh Token Expires At",
839
- required: false
840
- }),
841
- scope: import_data3.Field.text({
842
- label: "OAuth Scope",
843
- required: false
844
- }),
845
- password: import_data3.Field.text({
846
- label: "Password Hash",
847
- required: false,
848
- description: "Hashed password for email/password provider"
849
- })
850
- },
851
- indexes: [
852
- { fields: ["user_id"], unique: false },
853
- { fields: ["provider_id", "account_id"], unique: true }
854
- ],
855
- enable: {
856
- trackHistory: false,
857
- searchable: false,
858
- apiEnabled: true,
859
- apiMethods: ["get", "list", "create", "update", "delete"],
860
- trash: true,
861
- mru: false
862
- }
863
- });
864
-
865
- // src/objects/sys-verification.object.ts
866
- var import_data4 = require("@objectstack/spec/data");
867
- var SysVerification = import_data4.ObjectSchema.create({
868
- namespace: "sys",
869
- name: "verification",
870
- label: "Verification",
871
- pluralLabel: "Verifications",
872
- icon: "shield-check",
873
- isSystem: true,
874
- description: "Email and phone verification tokens",
875
- titleFormat: "Verification for {identifier}",
876
- compactLayout: ["identifier", "expires_at", "created_at"],
877
- fields: {
878
- id: import_data4.Field.text({
879
- label: "Verification ID",
880
- required: true,
881
- readonly: true
882
- }),
883
- created_at: import_data4.Field.datetime({
884
- label: "Created At",
885
- defaultValue: "NOW()",
886
- readonly: true
887
- }),
888
- updated_at: import_data4.Field.datetime({
889
- label: "Updated At",
890
- defaultValue: "NOW()",
891
- readonly: true
892
- }),
893
- value: import_data4.Field.text({
894
- label: "Verification Token",
895
- required: true,
896
- description: "Token or code for verification"
897
- }),
898
- expires_at: import_data4.Field.datetime({
899
- label: "Expires At",
900
- required: true
901
- }),
902
- identifier: import_data4.Field.text({
903
- label: "Identifier",
904
- required: true,
905
- description: "Email address or phone number"
906
- })
907
- },
908
- indexes: [
909
- { fields: ["value"], unique: true },
910
- { fields: ["identifier"], unique: false },
911
- { fields: ["expires_at"], unique: false }
912
- ],
913
- enable: {
914
- trackHistory: false,
915
- searchable: false,
916
- apiEnabled: true,
917
- apiMethods: ["get", "create", "delete"],
918
- trash: false,
919
- mru: false
920
- }
921
- });
922
-
923
- // src/objects/sys-organization.object.ts
924
- var import_data5 = require("@objectstack/spec/data");
925
- var SysOrganization = import_data5.ObjectSchema.create({
926
- namespace: "sys",
927
- name: "organization",
928
- label: "Organization",
929
- pluralLabel: "Organizations",
930
- icon: "building-2",
931
- isSystem: true,
932
- description: "Organizations for multi-tenant grouping",
933
- titleFormat: "{name}",
934
- compactLayout: ["name", "slug", "created_at"],
935
- fields: {
936
- id: import_data5.Field.text({
937
- label: "Organization ID",
938
- required: true,
939
- readonly: true
940
- }),
941
- created_at: import_data5.Field.datetime({
942
- label: "Created At",
943
- defaultValue: "NOW()",
944
- readonly: true
945
- }),
946
- updated_at: import_data5.Field.datetime({
947
- label: "Updated At",
948
- defaultValue: "NOW()",
949
- readonly: true
950
- }),
951
- name: import_data5.Field.text({
952
- label: "Name",
953
- required: true,
954
- searchable: true,
955
- maxLength: 255
956
- }),
957
- slug: import_data5.Field.text({
958
- label: "Slug",
959
- required: false,
960
- maxLength: 255,
961
- description: "URL-friendly identifier"
962
- }),
963
- logo: import_data5.Field.url({
964
- label: "Logo",
965
- required: false
966
- }),
967
- metadata: import_data5.Field.textarea({
968
- label: "Metadata",
969
- required: false,
970
- description: "JSON-serialized organization metadata"
971
- })
972
- },
973
- indexes: [
974
- { fields: ["slug"], unique: true },
975
- { fields: ["name"] }
976
- ],
977
- enable: {
978
- trackHistory: true,
979
- searchable: true,
980
- apiEnabled: true,
981
- apiMethods: ["get", "list", "create", "update", "delete"],
982
- trash: true,
983
- mru: true
984
- }
985
- });
986
-
987
- // src/objects/sys-member.object.ts
988
- var import_data6 = require("@objectstack/spec/data");
989
- var SysMember = import_data6.ObjectSchema.create({
990
- namespace: "sys",
991
- name: "member",
992
- label: "Member",
993
- pluralLabel: "Members",
994
- icon: "user-check",
995
- isSystem: true,
996
- description: "Organization membership records",
997
- titleFormat: "{user_id} in {organization_id}",
998
- compactLayout: ["user_id", "organization_id", "role"],
999
- fields: {
1000
- id: import_data6.Field.text({
1001
- label: "Member ID",
1002
- required: true,
1003
- readonly: true
1004
- }),
1005
- created_at: import_data6.Field.datetime({
1006
- label: "Created At",
1007
- defaultValue: "NOW()",
1008
- readonly: true
1009
- }),
1010
- organization_id: import_data6.Field.text({
1011
- label: "Organization ID",
1012
- required: true
1013
- }),
1014
- user_id: import_data6.Field.text({
1015
- label: "User ID",
1016
- required: true
1017
- }),
1018
- role: import_data6.Field.text({
1019
- label: "Role",
1020
- required: false,
1021
- description: "Member role within the organization (e.g. admin, member)",
1022
- maxLength: 100
1023
- })
1024
- },
1025
- indexes: [
1026
- { fields: ["organization_id", "user_id"], unique: true },
1027
- { fields: ["user_id"] }
1028
- ],
1029
- enable: {
1030
- trackHistory: true,
1031
- searchable: false,
1032
- apiEnabled: true,
1033
- apiMethods: ["get", "list", "create", "update", "delete"],
1034
- trash: false,
1035
- mru: false
1036
- }
1037
- });
1038
-
1039
- // src/objects/sys-invitation.object.ts
1040
- var import_data7 = require("@objectstack/spec/data");
1041
- var SysInvitation = import_data7.ObjectSchema.create({
1042
- namespace: "sys",
1043
- name: "invitation",
1044
- label: "Invitation",
1045
- pluralLabel: "Invitations",
1046
- icon: "mail",
1047
- isSystem: true,
1048
- description: "Organization invitations for user onboarding",
1049
- titleFormat: "Invitation to {organization_id}",
1050
- compactLayout: ["email", "organization_id", "status"],
1051
- fields: {
1052
- id: import_data7.Field.text({
1053
- label: "Invitation ID",
1054
- required: true,
1055
- readonly: true
1056
- }),
1057
- created_at: import_data7.Field.datetime({
1058
- label: "Created At",
1059
- defaultValue: "NOW()",
1060
- readonly: true
1061
- }),
1062
- organization_id: import_data7.Field.text({
1063
- label: "Organization ID",
1064
- required: true
1065
- }),
1066
- email: import_data7.Field.email({
1067
- label: "Email",
1068
- required: true,
1069
- description: "Email address of the invited user"
1070
- }),
1071
- role: import_data7.Field.text({
1072
- label: "Role",
1073
- required: false,
1074
- maxLength: 100,
1075
- description: "Role to assign upon acceptance"
1076
- }),
1077
- status: import_data7.Field.select(["pending", "accepted", "rejected", "expired", "canceled"], {
1078
- label: "Status",
1079
- required: true,
1080
- defaultValue: "pending"
1081
- }),
1082
- inviter_id: import_data7.Field.text({
1083
- label: "Inviter ID",
1084
- required: true,
1085
- description: "User ID of the person who sent the invitation"
1086
- }),
1087
- expires_at: import_data7.Field.datetime({
1088
- label: "Expires At",
1089
- required: true
1090
- }),
1091
- team_id: import_data7.Field.text({
1092
- label: "Team ID",
1093
- required: false,
1094
- description: "Optional team to assign upon acceptance"
1095
- })
1096
- },
1097
- indexes: [
1098
- { fields: ["organization_id"] },
1099
- { fields: ["email"] },
1100
- { fields: ["expires_at"] }
1101
- ],
1102
- enable: {
1103
- trackHistory: true,
1104
- searchable: false,
1105
- apiEnabled: true,
1106
- apiMethods: ["get", "list", "create", "update", "delete"],
1107
- trash: false,
1108
- mru: false
1109
- }
1110
- });
1111
-
1112
- // src/objects/sys-team.object.ts
1113
- var import_data8 = require("@objectstack/spec/data");
1114
- var SysTeam = import_data8.ObjectSchema.create({
1115
- namespace: "sys",
1116
- name: "team",
1117
- label: "Team",
1118
- pluralLabel: "Teams",
1119
- icon: "users",
1120
- isSystem: true,
1121
- description: "Teams within organizations for fine-grained grouping",
1122
- titleFormat: "{name}",
1123
- compactLayout: ["name", "organization_id", "created_at"],
1124
- fields: {
1125
- id: import_data8.Field.text({
1126
- label: "Team ID",
1127
- required: true,
1128
- readonly: true
1129
- }),
1130
- created_at: import_data8.Field.datetime({
1131
- label: "Created At",
1132
- defaultValue: "NOW()",
1133
- readonly: true
1134
- }),
1135
- updated_at: import_data8.Field.datetime({
1136
- label: "Updated At",
1137
- defaultValue: "NOW()",
1138
- readonly: true
1139
- }),
1140
- name: import_data8.Field.text({
1141
- label: "Name",
1142
- required: true,
1143
- searchable: true,
1144
- maxLength: 255
1145
- }),
1146
- organization_id: import_data8.Field.text({
1147
- label: "Organization ID",
1148
- required: true
1149
- })
1150
- },
1151
- indexes: [
1152
- { fields: ["organization_id"] },
1153
- { fields: ["name", "organization_id"], unique: true }
1154
- ],
1155
- enable: {
1156
- trackHistory: true,
1157
- searchable: true,
1158
- apiEnabled: true,
1159
- apiMethods: ["get", "list", "create", "update", "delete"],
1160
- trash: true,
1161
- mru: false
1162
- }
1163
- });
1164
-
1165
- // src/objects/sys-team-member.object.ts
1166
- var import_data9 = require("@objectstack/spec/data");
1167
- var SysTeamMember = import_data9.ObjectSchema.create({
1168
- namespace: "sys",
1169
- name: "team_member",
1170
- label: "Team Member",
1171
- pluralLabel: "Team Members",
1172
- icon: "user-plus",
1173
- isSystem: true,
1174
- description: "Team membership records linking users to teams",
1175
- titleFormat: "{user_id} in {team_id}",
1176
- compactLayout: ["user_id", "team_id", "created_at"],
1177
- fields: {
1178
- id: import_data9.Field.text({
1179
- label: "Team Member ID",
1180
- required: true,
1181
- readonly: true
1182
- }),
1183
- created_at: import_data9.Field.datetime({
1184
- label: "Created At",
1185
- defaultValue: "NOW()",
1186
- readonly: true
1187
- }),
1188
- team_id: import_data9.Field.text({
1189
- label: "Team ID",
1190
- required: true
1191
- }),
1192
- user_id: import_data9.Field.text({
1193
- label: "User ID",
1194
- required: true
1195
- })
1196
- },
1197
- indexes: [
1198
- { fields: ["team_id", "user_id"], unique: true },
1199
- { fields: ["user_id"] }
1200
- ],
1201
- enable: {
1202
- trackHistory: true,
1203
- searchable: false,
1204
- apiEnabled: true,
1205
- apiMethods: ["get", "list", "create", "delete"],
1206
- trash: false,
1207
- mru: false
1208
- }
1209
- });
1210
-
1211
- // src/objects/sys-api-key.object.ts
1212
- var import_data10 = require("@objectstack/spec/data");
1213
- var SysApiKey = import_data10.ObjectSchema.create({
1214
- namespace: "sys",
1215
- name: "api_key",
1216
- label: "API Key",
1217
- pluralLabel: "API Keys",
1218
- icon: "key-round",
1219
- isSystem: true,
1220
- description: "API keys for programmatic access",
1221
- titleFormat: "{name}",
1222
- compactLayout: ["name", "user_id", "expires_at"],
1223
- fields: {
1224
- id: import_data10.Field.text({
1225
- label: "API Key ID",
1226
- required: true,
1227
- readonly: true
1228
- }),
1229
- created_at: import_data10.Field.datetime({
1230
- label: "Created At",
1231
- defaultValue: "NOW()",
1232
- readonly: true
1233
- }),
1234
- updated_at: import_data10.Field.datetime({
1235
- label: "Updated At",
1236
- defaultValue: "NOW()",
1237
- readonly: true
1238
- }),
1239
- name: import_data10.Field.text({
1240
- label: "Name",
1241
- required: true,
1242
- maxLength: 255,
1243
- description: "Human-readable label for the API key"
1244
- }),
1245
- key: import_data10.Field.text({
1246
- label: "Key",
1247
- required: true,
1248
- description: "Hashed API key value"
1249
- }),
1250
- prefix: import_data10.Field.text({
1251
- label: "Prefix",
1252
- required: false,
1253
- maxLength: 16,
1254
- description: 'Visible prefix for identifying the key (e.g., "osk_")'
1255
- }),
1256
- user_id: import_data10.Field.text({
1257
- label: "User ID",
1258
- required: true,
1259
- description: "Owner user of this API key"
1260
- }),
1261
- scopes: import_data10.Field.textarea({
1262
- label: "Scopes",
1263
- required: false,
1264
- description: "JSON array of permission scopes"
1265
- }),
1266
- expires_at: import_data10.Field.datetime({
1267
- label: "Expires At",
1268
- required: false
1269
- }),
1270
- last_used_at: import_data10.Field.datetime({
1271
- label: "Last Used At",
1272
- required: false
1273
- }),
1274
- revoked: import_data10.Field.boolean({
1275
- label: "Revoked",
1276
- defaultValue: false
1277
- })
1278
- },
1279
- indexes: [
1280
- { fields: ["key"], unique: true },
1281
- { fields: ["user_id"] },
1282
- { fields: ["prefix"] }
1283
- ],
1284
- enable: {
1285
- trackHistory: true,
1286
- searchable: false,
1287
- apiEnabled: true,
1288
- apiMethods: ["get", "list", "create", "update", "delete"],
1289
- trash: false,
1290
- mru: false
1291
- }
1292
- });
1293
-
1294
- // src/objects/sys-two-factor.object.ts
1295
- var import_data11 = require("@objectstack/spec/data");
1296
- var SysTwoFactor = import_data11.ObjectSchema.create({
1297
- namespace: "sys",
1298
- name: "two_factor",
1299
- label: "Two Factor",
1300
- pluralLabel: "Two Factor Credentials",
1301
- icon: "smartphone",
1302
- isSystem: true,
1303
- description: "Two-factor authentication credentials",
1304
- titleFormat: "Two-factor for {user_id}",
1305
- compactLayout: ["user_id", "created_at"],
1306
- fields: {
1307
- id: import_data11.Field.text({
1308
- label: "Two Factor ID",
1309
- required: true,
1310
- readonly: true
1311
- }),
1312
- created_at: import_data11.Field.datetime({
1313
- label: "Created At",
1314
- defaultValue: "NOW()",
1315
- readonly: true
1316
- }),
1317
- updated_at: import_data11.Field.datetime({
1318
- label: "Updated At",
1319
- defaultValue: "NOW()",
1320
- readonly: true
1321
- }),
1322
- user_id: import_data11.Field.text({
1323
- label: "User ID",
1324
- required: true
1325
- }),
1326
- secret: import_data11.Field.text({
1327
- label: "Secret",
1328
- required: true,
1329
- description: "TOTP secret key"
1330
- }),
1331
- backup_codes: import_data11.Field.textarea({
1332
- label: "Backup Codes",
1333
- required: false,
1334
- description: "JSON-serialized backup recovery codes"
1335
- })
1336
- },
1337
- indexes: [
1338
- { fields: ["user_id"], unique: true }
1339
- ],
1340
- enable: {
1341
- trackHistory: false,
1342
- searchable: false,
1343
- apiEnabled: true,
1344
- apiMethods: ["get", "create", "update", "delete"],
1345
- trash: false,
1346
- mru: false
1127
+ /**
1128
+ * Returns the data engine wired into this auth manager. Used by route
1129
+ * handlers (e.g. bootstrap-status) that need to query identity tables
1130
+ * directly without going through better-auth.
1131
+ */
1132
+ getDataEngine() {
1133
+ return this.config.dataEngine;
1347
1134
  }
1348
- });
1135
+ };
1349
1136
 
1350
- // src/objects/sys-user-preference.object.ts
1351
- var import_data12 = require("@objectstack/spec/data");
1352
- var SysUserPreference = import_data12.ObjectSchema.create({
1137
+ // src/manifest.ts
1138
+ var import_identity = require("@objectstack/platform-objects/identity");
1139
+ var AUTH_PLUGIN_ID = "com.objectstack.plugin-auth";
1140
+ var AUTH_PLUGIN_VERSION = "3.0.1";
1141
+ var authIdentityObjects = [
1142
+ import_identity.SysUser,
1143
+ import_identity.SysSession,
1144
+ import_identity.SysAccount,
1145
+ import_identity.SysVerification,
1146
+ import_identity.SysOrganization,
1147
+ import_identity.SysMember,
1148
+ import_identity.SysInvitation,
1149
+ import_identity.SysTeam,
1150
+ import_identity.SysTeamMember,
1151
+ import_identity.SysApiKey,
1152
+ import_identity.SysTwoFactor,
1153
+ import_identity.SysUserPreference,
1154
+ import_identity.SysOauthApplication,
1155
+ import_identity.SysOauthAccessToken,
1156
+ import_identity.SysOauthRefreshToken,
1157
+ import_identity.SysOauthConsent,
1158
+ import_identity.SysJwks,
1159
+ import_identity.SysDeviceCode
1160
+ ];
1161
+ var authPluginManifestHeader = {
1162
+ id: AUTH_PLUGIN_ID,
1353
1163
  namespace: "sys",
1354
- name: "user_preference",
1355
- label: "User Preference",
1356
- pluralLabel: "User Preferences",
1357
- icon: "settings",
1358
- isSystem: true,
1359
- description: "Per-user key-value preferences (theme, locale, etc.)",
1360
- titleFormat: "{key}",
1361
- compactLayout: ["user_id", "key"],
1362
- fields: {
1363
- id: import_data12.Field.text({
1364
- label: "Preference ID",
1365
- required: true,
1366
- readonly: true
1367
- }),
1368
- created_at: import_data12.Field.datetime({
1369
- label: "Created At",
1370
- defaultValue: "NOW()",
1371
- readonly: true
1372
- }),
1373
- updated_at: import_data12.Field.datetime({
1374
- label: "Updated At",
1375
- defaultValue: "NOW()",
1376
- readonly: true
1377
- }),
1378
- user_id: import_data12.Field.text({
1379
- label: "User ID",
1380
- required: true,
1381
- maxLength: 255,
1382
- description: "Owner user of this preference"
1383
- }),
1384
- key: import_data12.Field.text({
1385
- label: "Key",
1386
- required: true,
1387
- maxLength: 255,
1388
- description: "Preference key (e.g., theme, locale, plugin.ai.auto_save)"
1389
- }),
1390
- value: import_data12.Field.json({
1391
- label: "Value",
1392
- description: "Preference value (any JSON-serializable type)"
1393
- })
1394
- },
1395
- indexes: [
1396
- { fields: ["user_id", "key"], unique: true },
1397
- { fields: ["user_id"], unique: false }
1398
- ],
1399
- enable: {
1400
- trackHistory: false,
1401
- searchable: false,
1402
- apiEnabled: true,
1403
- apiMethods: ["get", "list", "create", "update", "delete"],
1404
- trash: false,
1405
- mru: false
1406
- }
1407
- });
1164
+ version: AUTH_PLUGIN_VERSION,
1165
+ type: "plugin",
1166
+ scope: "system",
1167
+ defaultDatasource: "cloud",
1168
+ name: "Authentication & Identity Plugin",
1169
+ description: "Core authentication objects for ObjectStack (User, Session, Account, Verification)"
1170
+ };
1408
1171
 
1409
1172
  // src/auth-plugin.ts
1410
1173
  var AuthPlugin = class {
@@ -1435,43 +1198,25 @@ var AuthPlugin = class {
1435
1198
  });
1436
1199
  ctx.registerService("auth", this.authManager);
1437
1200
  ctx.getService("manifest").register({
1438
- id: "com.objectstack.system",
1439
- name: "System",
1440
- version: "1.0.0",
1441
- type: "plugin",
1442
- namespace: "sys",
1443
- objects: [
1444
- SysUser,
1445
- SysSession,
1446
- SysAccount,
1447
- SysVerification,
1448
- SysOrganization,
1449
- SysMember,
1450
- SysInvitation,
1451
- SysTeam,
1452
- SysTeamMember,
1453
- SysApiKey,
1454
- SysTwoFactor,
1455
- SysUserPreference
1456
- ]
1201
+ ...authPluginManifestHeader,
1202
+ ...this.options.manifestDatasource ? { defaultDatasource: this.options.manifestDatasource } : {},
1203
+ objects: authIdentityObjects,
1204
+ // The platform Setup App is a static metadata artifact (lives in
1205
+ // @objectstack/platform-objects/apps). plugin-auth is the natural
1206
+ // owner of its registration since it loads first among the trio
1207
+ // (auth + security + audit) that supplies the underlying objects.
1208
+ apps: [import_apps.SETUP_APP],
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.
1218
+ dashboards: [import_apps.SystemOverviewDashboard, import_apps.SecurityOverviewDashboard]
1457
1219
  });
1458
- try {
1459
- const setupNav = ctx.getService("setupNav");
1460
- if (setupNav) {
1461
- setupNav.contribute({
1462
- areaId: "area_administration",
1463
- items: [
1464
- { id: "nav_users", type: "object", label: "Users", objectName: "user", icon: "users", order: 10 },
1465
- { id: "nav_organizations", type: "object", label: "Organizations", objectName: "organization", icon: "building-2", order: 20 },
1466
- { id: "nav_teams", type: "object", label: "Teams", objectName: "team", icon: "users-round", order: 30 },
1467
- { id: "nav_api_keys", type: "object", label: "API Keys", objectName: "api_key", icon: "key", order: 40 },
1468
- { id: "nav_sessions", type: "object", label: "Sessions", objectName: "session", icon: "monitor", order: 50 }
1469
- ]
1470
- });
1471
- ctx.logger.info("Auth navigation items contributed to Setup App");
1472
- }
1473
- } catch {
1474
- }
1475
1220
  ctx.logger.info("Auth Plugin initialized successfully");
1476
1221
  }
1477
1222
  async start(ctx) {
@@ -1481,6 +1226,17 @@ var AuthPlugin = class {
1481
1226
  }
1482
1227
  if (this.options.registerRoutes) {
1483
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
+ }
1484
1240
  let httpServer = null;
1485
1241
  try {
1486
1242
  httpServer = ctx.getService("http-server");
@@ -1494,7 +1250,8 @@ var AuthPlugin = class {
1494
1250
  const configuredUrl = this.options.baseUrl || "http://localhost:3000";
1495
1251
  const configuredOrigin = new URL(configuredUrl).origin;
1496
1252
  const actualUrl = `http://localhost:${actualPort}`;
1497
- if (configuredOrigin !== actualUrl) {
1253
+ const configuredIsLocalhost = configuredOrigin.startsWith("http://localhost");
1254
+ if (configuredIsLocalhost && configuredOrigin !== actualUrl) {
1498
1255
  this.authManager.setRuntimeBaseUrl(actualUrl);
1499
1256
  ctx.logger.info(
1500
1257
  `Auth baseUrl auto-updated to ${actualUrl} (configured: ${configuredUrl})`
@@ -1552,23 +1309,26 @@ var AuthPlugin = class {
1552
1309
  );
1553
1310
  }
1554
1311
  const rawApp = httpServer.getRawApp();
1555
- rawApp.get(`${basePath}/config`, async (c) => {
1312
+ rawApp.get(`${basePath}/config`, (c) => {
1556
1313
  try {
1557
1314
  const config = this.authManager.getPublicConfig();
1558
- return c.json({
1559
- success: true,
1560
- data: config
1561
- });
1315
+ return c.json({ success: true, data: config });
1562
1316
  } catch (error) {
1563
1317
  const err = error instanceof Error ? error : new Error(String(error));
1564
- ctx.logger.error("Auth config error:", err);
1565
- return c.json({
1566
- success: false,
1567
- error: {
1568
- code: "auth_config_error",
1569
- message: err.message
1570
- }
1571
- }, 500);
1318
+ return c.json({ success: false, error: { code: "auth_config_error", message: err.message } }, 500);
1319
+ }
1320
+ });
1321
+ rawApp.get(`${basePath}/bootstrap-status`, async (c) => {
1322
+ try {
1323
+ const dataEngine = this.authManager.getDataEngine();
1324
+ if (!dataEngine) {
1325
+ return c.json({ hasOwner: true });
1326
+ }
1327
+ const count = await dataEngine.count("sys_user", {});
1328
+ return c.json({ hasOwner: (count ?? 0) > 0 });
1329
+ } catch (error) {
1330
+ ctx.logger.warn("[AuthPlugin] bootstrap-status check failed; assuming bootstrapped", error);
1331
+ return c.json({ hasOwner: true });
1572
1332
  }
1573
1333
  });
1574
1334
  rawApp.all(`${basePath}/*`, async (c) => {
@@ -1582,6 +1342,19 @@ var AuthPlugin = class {
1582
1342
  ctx.logger.error("[AuthPlugin] better-auth returned server error", new Error(`HTTP ${response.status}: (unable to read body)`));
1583
1343
  }
1584
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
+ }
1585
1358
  return response;
1586
1359
  } catch (error) {
1587
1360
  const err = error instanceof Error ? error : new Error(String(error));
@@ -1598,15 +1371,57 @@ var AuthPlugin = class {
1598
1371
  );
1599
1372
  }
1600
1373
  });
1374
+ if (this.options.plugins?.oidcProvider) {
1375
+ void this.registerOidcDiscoveryRoutes(rawApp, ctx).catch((error) => {
1376
+ ctx.logger.error("Failed to register OIDC discovery routes", error);
1377
+ });
1378
+ }
1601
1379
  ctx.logger.info(`Auth routes registered: All requests under ${basePath}/* forwarded to better-auth`);
1602
1380
  }
1381
+ /**
1382
+ * Mount the OIDC / OAuth 2.0 well-known discovery documents at the root
1383
+ * URL. Required by RFC 8414 §3 and OpenID Connect Discovery 1.0 §4 — the
1384
+ * documents must live at `/.well-known/{oauth-authorization-server,openid-configuration}`
1385
+ * relative to the issuer, not under the auth basePath.
1386
+ */
1387
+ async registerOidcDiscoveryRoutes(rawApp, ctx) {
1388
+ const auth = await this.authManager.getAuthInstance();
1389
+ const { oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata } = await import("@better-auth/oauth-provider");
1390
+ const authServerHandler = oauthProviderAuthServerMetadata(auth);
1391
+ const openidConfigHandler = oauthProviderOpenIdConfigMetadata(auth);
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));
1405
+ ctx.logger.info(
1406
+ "OIDC discovery endpoints mounted at /.well-known/{oauth-authorization-server,openid-configuration}"
1407
+ );
1408
+ }
1603
1409
  };
1604
1410
  // Annotate the CommonJS export names for ESM import in node:
1605
1411
  0 && (module.exports = {
1606
1412
  AUTH_ACCOUNT_CONFIG,
1413
+ AUTH_ADMIN_SESSION_FIELDS,
1414
+ AUTH_ADMIN_USER_FIELDS,
1415
+ AUTH_DEVICE_CODE_SCHEMA,
1607
1416
  AUTH_INVITATION_SCHEMA,
1417
+ AUTH_JWKS_SCHEMA,
1608
1418
  AUTH_MEMBER_SCHEMA,
1609
1419
  AUTH_MODEL_TO_PROTOCOL,
1420
+ AUTH_OAUTH_ACCESS_TOKEN_SCHEMA,
1421
+ AUTH_OAUTH_APPLICATION_SCHEMA,
1422
+ AUTH_OAUTH_CLIENT_SCHEMA,
1423
+ AUTH_OAUTH_CONSENT_SCHEMA,
1424
+ AUTH_OAUTH_REFRESH_TOKEN_SCHEMA,
1610
1425
  AUTH_ORGANIZATION_SCHEMA,
1611
1426
  AUTH_ORG_SESSION_FIELDS,
1612
1427
  AUTH_SESSION_CONFIG,
@@ -1616,24 +1431,13 @@ var AuthPlugin = class {
1616
1431
  AUTH_TWO_FACTOR_USER_FIELDS,
1617
1432
  AUTH_USER_CONFIG,
1618
1433
  AUTH_VERIFICATION_CONFIG,
1619
- AuthAccount,
1620
1434
  AuthManager,
1621
1435
  AuthPlugin,
1622
- AuthSession,
1623
- AuthUser,
1624
- AuthVerification,
1625
- SysAccount,
1626
- SysApiKey,
1627
- SysInvitation,
1628
- SysMember,
1629
- SysOrganization,
1630
- SysSession,
1631
- SysTeam,
1632
- SysTeamMember,
1633
- SysTwoFactor,
1634
- SysUser,
1635
- SysUserPreference,
1636
- SysVerification,
1436
+ buildAdminPluginSchema,
1437
+ buildDeviceAuthorizationPluginSchema,
1438
+ buildJwtPluginSchema,
1439
+ buildOauthProviderPluginSchema,
1440
+ buildOidcProviderPluginSchema,
1637
1441
  buildOrganizationPluginSchema,
1638
1442
  buildTwoFactorPluginSchema,
1639
1443
  createObjectQLAdapter,