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