@invect/user-auth 0.0.1 → 0.0.3

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 (38) hide show
  1. package/README.md +81 -72
  2. package/dist/backend/index.cjs +410 -54
  3. package/dist/backend/index.cjs.map +1 -1
  4. package/dist/backend/index.d.cts +456 -0
  5. package/dist/backend/index.d.cts.map +1 -0
  6. package/dist/backend/index.d.mts +456 -0
  7. package/dist/backend/index.d.mts.map +1 -0
  8. package/dist/backend/index.d.ts +28 -18
  9. package/dist/backend/index.d.ts.map +1 -1
  10. package/dist/backend/index.mjs +408 -53
  11. package/dist/backend/index.mjs.map +1 -1
  12. package/dist/backend/plugin.d.ts +15 -15
  13. package/dist/backend/plugin.d.ts.map +1 -1
  14. package/dist/backend/types.d.ts +85 -9
  15. package/dist/backend/types.d.ts.map +1 -1
  16. package/dist/frontend/components/ApiKeysDialog.d.ts +17 -0
  17. package/dist/frontend/components/ApiKeysDialog.d.ts.map +1 -0
  18. package/dist/frontend/components/AuthenticatedInvect.d.ts +10 -10
  19. package/dist/frontend/components/SignInForm.d.ts.map +1 -1
  20. package/dist/frontend/components/SignInPage.d.ts.map +1 -1
  21. package/dist/frontend/components/UserManagement.d.ts.map +1 -1
  22. package/dist/frontend/index.cjs +434 -58
  23. package/dist/frontend/index.cjs.map +1 -1
  24. package/dist/frontend/index.d.cts +317 -0
  25. package/dist/frontend/index.d.cts.map +1 -0
  26. package/dist/frontend/index.d.mts +317 -0
  27. package/dist/frontend/index.d.mts.map +1 -0
  28. package/dist/frontend/index.d.ts +3 -1
  29. package/dist/frontend/index.d.ts.map +1 -1
  30. package/dist/frontend/index.mjs +418 -43
  31. package/dist/frontend/index.mjs.map +1 -1
  32. package/dist/frontend/plugins/authFrontendPlugin.d.ts +2 -2
  33. package/dist/frontend/plugins/authFrontendPlugin.d.ts.map +1 -1
  34. package/dist/shared/types.d.cts +49 -0
  35. package/dist/shared/types.d.cts.map +1 -0
  36. package/dist/shared/types.d.mts +49 -0
  37. package/dist/shared/types.d.mts.map +1 -0
  38. package/package.json +68 -66
@@ -213,16 +213,16 @@ function getErrorLogDetails(error) {
213
213
  return { value: error };
214
214
  }
215
215
  /**
216
- * Abstract schema for better-auth's database tables.
216
+ * Abstract schema for the user-auth plugin's database tables.
217
217
  *
218
- * These definitions allow the Invect CLI (`npx invect generate`) to include
219
- * the better-auth tables when generating Drizzle/Prisma schema files.
218
+ * These definitions allow the Invect CLI (`npx invect-cli generate`) to include
219
+ * the auth tables when generating Drizzle/Prisma schema files.
220
220
  *
221
- * The shapes match better-auth's default table structure. If your better-auth
221
+ * The shapes match Better Auth's default table structure. If your Better Auth
222
222
  * config adds extra fields (e.g., via plugins like `twoFactor`, `organization`),
223
223
  * you can extend these in your own config.
224
224
  */
225
- const BETTER_AUTH_SCHEMA = {
225
+ const USER_AUTH_SCHEMA = {
226
226
  user: {
227
227
  tableName: "user",
228
228
  order: 1,
@@ -423,10 +423,161 @@ const BETTER_AUTH_SCHEMA = {
423
423
  required: false
424
424
  }
425
425
  }
426
+ },
427
+ flowAccess: {
428
+ tableName: "flow_access",
429
+ order: 3,
430
+ fields: {
431
+ id: {
432
+ type: "uuid",
433
+ primaryKey: true,
434
+ defaultValue: "uuid()"
435
+ },
436
+ flowId: {
437
+ type: "string",
438
+ required: true,
439
+ references: {
440
+ table: "flows",
441
+ field: "id",
442
+ onDelete: "cascade"
443
+ }
444
+ },
445
+ userId: {
446
+ type: "string",
447
+ required: false
448
+ },
449
+ teamId: {
450
+ type: "string",
451
+ required: false
452
+ },
453
+ permission: {
454
+ type: "string",
455
+ required: true,
456
+ defaultValue: "viewer"
457
+ },
458
+ grantedBy: {
459
+ type: "string",
460
+ required: false
461
+ },
462
+ grantedAt: {
463
+ type: "date",
464
+ required: true,
465
+ defaultValue: "now()"
466
+ },
467
+ expiresAt: {
468
+ type: "date",
469
+ required: false
470
+ }
471
+ }
426
472
  }
427
473
  };
428
474
  /**
429
- * Create a better-auth instance internally using Invect's database config.
475
+ * Abstract schema for the Better Auth API Key plugin's `apikey` table.
476
+ *
477
+ * Only merged into the plugin schema when `apiKey` is enabled.
478
+ *
479
+ * @see https://better-auth.com/docs/plugins/api-key/reference#schema
480
+ */
481
+ const API_KEY_SCHEMA = { apikey: {
482
+ tableName: "apikey",
483
+ order: 3,
484
+ fields: {
485
+ id: {
486
+ type: "string",
487
+ primaryKey: true
488
+ },
489
+ configId: {
490
+ type: "string",
491
+ required: true,
492
+ defaultValue: "default"
493
+ },
494
+ name: {
495
+ type: "string",
496
+ required: false
497
+ },
498
+ start: {
499
+ type: "string",
500
+ required: false
501
+ },
502
+ prefix: {
503
+ type: "string",
504
+ required: false
505
+ },
506
+ key: {
507
+ type: "string",
508
+ required: true
509
+ },
510
+ referenceId: {
511
+ type: "string",
512
+ required: true
513
+ },
514
+ refillInterval: {
515
+ type: "number",
516
+ required: false
517
+ },
518
+ refillAmount: {
519
+ type: "number",
520
+ required: false
521
+ },
522
+ lastRefillAt: {
523
+ type: "date",
524
+ required: false
525
+ },
526
+ enabled: {
527
+ type: "boolean",
528
+ required: false,
529
+ defaultValue: true
530
+ },
531
+ rateLimitEnabled: {
532
+ type: "boolean",
533
+ required: false
534
+ },
535
+ rateLimitTimeWindow: {
536
+ type: "number",
537
+ required: false
538
+ },
539
+ rateLimitMax: {
540
+ type: "number",
541
+ required: false
542
+ },
543
+ requestCount: {
544
+ type: "number",
545
+ required: false
546
+ },
547
+ remaining: {
548
+ type: "number",
549
+ required: false
550
+ },
551
+ lastRequest: {
552
+ type: "date",
553
+ required: false
554
+ },
555
+ expiresAt: {
556
+ type: "date",
557
+ required: false
558
+ },
559
+ createdAt: {
560
+ type: "date",
561
+ required: true,
562
+ defaultValue: "now()"
563
+ },
564
+ updatedAt: {
565
+ type: "date",
566
+ required: true,
567
+ defaultValue: "now()"
568
+ },
569
+ permissions: {
570
+ type: "string",
571
+ required: false
572
+ },
573
+ metadata: {
574
+ type: "string",
575
+ required: false
576
+ }
577
+ }
578
+ } };
579
+ /**
580
+ * Create a Better Auth instance internally using Invect's database config.
430
581
  *
431
582
  * Dynamically imports `better-auth` (a required peer dependency) and creates
432
583
  * a fully-configured instance with email/password auth, the admin plugin,
@@ -434,7 +585,7 @@ const BETTER_AUTH_SCHEMA = {
434
585
  *
435
586
  * Database resolution order:
436
587
  * 1. Explicit `options.database` (any value `betterAuth({ database })` accepts)
437
- * 2. Auto-created client from Invect's `baseDatabaseConfig.connectionString`
588
+ * 2. Auto-created client from Invect's `database.connectionString`
438
589
  */
439
590
  async function createInternalBetterAuth(invectConfig, options, logger) {
440
591
  let betterAuthFn;
@@ -451,14 +602,14 @@ async function createInternalBetterAuth(invectConfig, options, logger) {
451
602
  }
452
603
  let database = options.database;
453
604
  if (!database) {
454
- const dbConfig = invectConfig.baseDatabaseConfig;
455
- if (!dbConfig?.connectionString) throw new Error("Cannot create internal better-auth instance: no database configuration found. Either provide `auth` (a better-auth instance), `database`, or ensure Invect baseDatabaseConfig has a connectionString.");
605
+ const dbConfig = invectConfig.database;
606
+ if (!dbConfig?.connectionString) throw new Error("Cannot create internal Better Auth instance: no database configuration found. Either provide `auth` (a Better Auth instance), `database`, or ensure Invect database has a connectionString.");
456
607
  const connStr = dbConfig.connectionString;
457
608
  const dbType = (dbConfig.type ?? "sqlite").toLowerCase();
458
609
  if (dbType === "sqlite") database = await createSQLiteClient(connStr, logger);
459
610
  else if (dbType === "pg" || dbType === "postgresql") database = await createPostgresPool(connStr);
460
611
  else if (dbType === "mysql") database = await createMySQLPool(connStr);
461
- else throw new Error(`Unsupported database type for internal better-auth: "${dbType}". Supported: sqlite, pg, mysql. Alternatively, provide your own better-auth instance via \`auth\`.`);
612
+ else throw new Error(`Unsupported database type for internal Better Auth: "${dbType}". Supported: sqlite, pg, mysql. Alternatively, provide your own Better Auth instance via \`auth\`.`);
462
613
  }
463
614
  const baseURL = options.baseURL ?? process.env.BETTER_AUTH_URL ?? `http://localhost:${process.env.PORT ?? "3000"}`;
464
615
  const trustedOrigins = options.trustedOrigins ?? ((request) => {
@@ -489,26 +640,53 @@ async function createInternalBetterAuth(invectConfig, options, logger) {
489
640
  ...passthrough.session.cookieCache
490
641
  } } : {}
491
642
  };
492
- logger.info?.("Creating internal better-auth instance");
643
+ let resolvedSecret = passthrough.secret;
644
+ const resolvedSecrets = passthrough.secrets;
645
+ if (!resolvedSecret && !resolvedSecrets) {
646
+ const envKey = process.env.INVECT_ENCRYPTION_KEY;
647
+ if (envKey) {
648
+ resolvedSecret = envKey;
649
+ logger.debug?.("Using INVECT_ENCRYPTION_KEY as Better Auth secret (no explicit secret/secrets provided)");
650
+ }
651
+ }
652
+ const apiKeyOpt = options.apiKey ?? passthrough.apiKey;
653
+ const betterAuthPlugins = [adminPlugin({
654
+ defaultRole: AUTH_DEFAULT_ROLE,
655
+ adminRoles: [AUTH_ADMIN_ROLE]
656
+ })];
657
+ if (apiKeyOpt) {
658
+ let apiKeyPluginFn;
659
+ try {
660
+ apiKeyPluginFn = (await import("@better-auth/api-key")).apiKey;
661
+ } catch {
662
+ throw new Error("Could not import \"@better-auth/api-key\". Install it with: npm install @better-auth/api-key");
663
+ }
664
+ const apiKeyConfig = typeof apiKeyOpt === "object" ? { ...apiKeyOpt } : {};
665
+ if (apiKeyConfig.apiKeyHeaders === void 0) apiKeyConfig.apiKeyHeaders = "x-invect-token";
666
+ if (apiKeyConfig.enableSessionForAPIKeys === void 0) apiKeyConfig.enableSessionForAPIKeys = true;
667
+ betterAuthPlugins.push(apiKeyPluginFn(apiKeyConfig));
668
+ logger.info?.("Better Auth API Key plugin enabled");
669
+ }
670
+ logger.info?.("Creating internal Better Auth instance");
493
671
  return betterAuthFn({
494
672
  baseURL,
495
673
  database,
496
674
  emailAndPassword,
497
- plugins: [adminPlugin({
498
- defaultRole: AUTH_DEFAULT_ROLE,
499
- adminRoles: [AUTH_ADMIN_ROLE]
500
- })],
675
+ plugins: betterAuthPlugins,
501
676
  session,
502
677
  trustedOrigins,
503
678
  ...passthrough.socialProviders ? { socialProviders: passthrough.socialProviders } : {},
504
679
  ...passthrough.account ? { account: passthrough.account } : {},
505
680
  ...passthrough.rateLimit ? { rateLimit: passthrough.rateLimit } : {},
506
- ...passthrough.advanced ? { advanced: passthrough.advanced } : {},
681
+ advanced: {
682
+ cookiePrefix: "invect",
683
+ ...passthrough.advanced
684
+ },
507
685
  ...passthrough.databaseHooks ? { databaseHooks: passthrough.databaseHooks } : {},
508
686
  ...passthrough.hooks ? { hooks: passthrough.hooks } : {},
509
687
  ...passthrough.disabledPaths ? { disabledPaths: passthrough.disabledPaths } : {},
510
- ...passthrough.secret ? { secret: passthrough.secret } : {},
511
- ...passthrough.secrets ? { secrets: passthrough.secrets } : {}
688
+ ...resolvedSecret ? { secret: resolvedSecret } : {},
689
+ ...resolvedSecrets ? { secrets: resolvedSecrets } : {}
512
690
  });
513
691
  }
514
692
  /** Create a SQLite client using better-sqlite3. */
@@ -516,7 +694,7 @@ async function createSQLiteClient(connectionString, logger) {
516
694
  try {
517
695
  const { default: Database } = await import("better-sqlite3");
518
696
  const { Kysely, SqliteDialect, CamelCasePlugin } = await import("kysely");
519
- logger.debug?.(`Using better-sqlite3 for internal better-auth database`);
697
+ logger.debug?.(`Using better-sqlite3 for internal Better Auth database`);
520
698
  let dbPath = connectionString.replace(/^file:/, "");
521
699
  if (dbPath === "") dbPath = ":memory:";
522
700
  return {
@@ -527,7 +705,7 @@ async function createSQLiteClient(connectionString, logger) {
527
705
  type: "sqlite"
528
706
  };
529
707
  } catch (err) {
530
- if (err instanceof Error && err.message.includes("better-sqlite3")) throw new Error("Cannot create SQLite database for internal better-auth: install better-sqlite3 (npm install better-sqlite3). Alternatively, provide your own better-auth instance via the `auth` option.");
708
+ if (err instanceof Error && err.message.includes("better-sqlite3")) throw new Error("Cannot create SQLite database for internal Better Auth: install better-sqlite3 (npm install better-sqlite3). Alternatively, provide your own Better Auth instance via the `auth` option.");
531
709
  throw err;
532
710
  }
533
711
  }
@@ -537,7 +715,7 @@ async function createPostgresPool(connectionString) {
537
715
  const { Pool } = await import("pg");
538
716
  return new Pool({ connectionString });
539
717
  } catch {
540
- throw new Error("Cannot create PostgreSQL pool for internal better-auth: install the \"pg\" package. Alternatively, provide your own better-auth instance via the `auth` option.");
718
+ throw new Error("Cannot create PostgreSQL pool for internal Better Auth: install the \"pg\" package. Alternatively, provide your own Better Auth instance via the `auth` option.");
541
719
  }
542
720
  }
543
721
  /** Create a MySQL pool from a connection string. */
@@ -545,15 +723,15 @@ async function createMySQLPool(connectionString) {
545
723
  try {
546
724
  return (await import("mysql2/promise")).createPool(connectionString);
547
725
  } catch {
548
- throw new Error("Cannot create MySQL pool for internal better-auth: install the \"mysql2\" package. Alternatively, provide your own better-auth instance via the `auth` option.");
726
+ throw new Error("Cannot create MySQL pool for internal Better Auth: install the \"mysql2\" package. Alternatively, provide your own Better Auth instance via the `auth` option.");
549
727
  }
550
728
  }
551
729
  /**
552
- * Create an Invect plugin that wraps a better-auth instance.
730
+ * Create the Invect user-auth plugin (a light wrapper around Better Auth).
553
731
  *
554
732
  * This plugin:
555
733
  *
556
- * 1. **Proxies better-auth routes** — All of better-auth's HTTP endpoints
734
+ * 1. **Proxies Better Auth routes** — All of Better Auth's HTTP endpoints
557
735
  * (sign-in, sign-up, sign-out, OAuth callbacks, session, etc.) are mounted
558
736
  * under the plugin endpoint space at `/plugins/auth/api/auth/*` (configurable).
559
737
  *
@@ -561,17 +739,17 @@ async function createMySQLPool(connectionString) {
561
739
  * `onRequest` hook reads the session cookie / bearer token via
562
740
  * `auth.api.getSession()` and populates `InvectIdentity`.
563
741
  *
564
- * 3. **Handles authorization** — The `onAuthorize` hook lets better-auth's
742
+ * 3. **Handles authorization** — The `onAuthorize` hook lets Better Auth's
565
743
  * session decide whether a request is allowed.
566
744
  *
567
745
  * @example
568
746
  * ```ts
569
- * // Simple: let the plugin manage better-auth internally
570
- * import { betterAuthPlugin } from '@invect/user-auth';
747
+ * // Simple: let the plugin manage Better Auth internally
748
+ * import { authentication } from '@invect/user-auth';
571
749
  *
572
750
  * app.use('/invect', createInvectRouter({
573
751
  * databaseUrl: 'file:./dev.db',
574
- * plugins: [betterAuthPlugin({
752
+ * plugins: [authentication({
575
753
  * globalAdmins: [{ email: 'admin@co.com', pw: 'secret' }],
576
754
  * })],
577
755
  * }));
@@ -581,7 +759,7 @@ async function createMySQLPool(connectionString) {
581
759
  * ```ts
582
760
  * // Advanced: provide your own better-auth instance
583
761
  * import { betterAuth } from 'better-auth';
584
- * import { betterAuthPlugin } from '@invect/user-auth';
762
+ * import { authentication } from '@invect/user-auth';
585
763
  *
586
764
  * const auth = betterAuth({
587
765
  * database: { ... },
@@ -591,13 +769,18 @@ async function createMySQLPool(connectionString) {
591
769
  *
592
770
  * app.use('/invect', createInvectRouter({
593
771
  * databaseUrl: 'file:./dev.db',
594
- * plugins: [betterAuthPlugin({ auth })],
772
+ * plugins: [authentication({ auth })],
595
773
  * }));
596
774
  * ```
597
775
  */
598
- function betterAuthPlugin(options) {
776
+ function authentication(options) {
599
777
  const { prefix = DEFAULT_PREFIX, mapUser: customMapUser, mapRole = defaultMapRole, publicPaths = [], onSessionError = "throw", globalAdmins = [] } = options;
600
778
  let auth = options.auth ?? null;
779
+ /** Narrow `auth` for call-sites that run only after init. */
780
+ function requireAuth() {
781
+ if (!auth) throw new Error("Auth plugin not initialized");
782
+ return auth;
783
+ }
601
784
  let endpointLogger = console;
602
785
  let betterAuthBasePath = "/api/auth";
603
786
  /**
@@ -663,7 +846,7 @@ function betterAuthPlugin(options) {
663
846
  body: method !== "GET" && method !== "DELETE" ? ctx.request.body : void 0,
664
847
  duplex: method !== "GET" && method !== "DELETE" ? "half" : void 0
665
848
  });
666
- const response = await auth.handler(authRequest);
849
+ const response = await requireAuth().handler(authRequest);
667
850
  endpointLogger.debug?.(`[auth-proxy] Response: ${response.status} ${response.statusText}`, {
668
851
  setCookie: response.headers.get("set-cookie") ? "present" : "absent",
669
852
  contentType: response.headers.get("content-type")
@@ -671,17 +854,25 @@ function betterAuthPlugin(options) {
671
854
  return response;
672
855
  }
673
856
  }));
857
+ const apiKeyEnabled = !!(options.apiKey ?? options.betterAuthOptions?.apiKey);
858
+ const schema = apiKeyEnabled ? {
859
+ ...USER_AUTH_SCHEMA,
860
+ ...API_KEY_SCHEMA
861
+ } : USER_AUTH_SCHEMA;
862
+ const requiredTables = [
863
+ "user",
864
+ "session",
865
+ "account",
866
+ "verification",
867
+ "flow_access"
868
+ ];
869
+ if (apiKeyEnabled) requiredTables.push("apikey");
674
870
  return {
675
- id: "better-auth",
676
- name: "Better Auth",
677
- schema: BETTER_AUTH_SCHEMA,
678
- requiredTables: [
679
- "user",
680
- "session",
681
- "account",
682
- "verification"
683
- ],
684
- setupInstructions: "Run `npx invect generate` to add the better-auth tables to your schema, then `npx drizzle-kit push` (or `npx invect migrate`) to apply.",
871
+ id: "user-auth",
872
+ name: "User Auth",
873
+ schema,
874
+ requiredTables,
875
+ setupInstructions: "Run `npx invect-cli generate` to add the better-auth tables to your schema, then `npx drizzle-kit push` (or `npx invect-cli migrate`) to apply.",
685
876
  endpoints: [
686
877
  {
687
878
  method: "GET",
@@ -722,6 +913,145 @@ function betterAuthPlugin(options) {
722
913
  };
723
914
  }
724
915
  },
916
+ {
917
+ method: "GET",
918
+ path: `/${prefix}/info`,
919
+ isPublic: false,
920
+ handler: async (ctx) => {
921
+ if (!await resolveEndpointIdentity(ctx)) return {
922
+ status: 401,
923
+ body: { error: "Unauthorized" }
924
+ };
925
+ return {
926
+ status: 200,
927
+ body: { apiKeysEnabled: apiKeyEnabled }
928
+ };
929
+ }
930
+ },
931
+ {
932
+ method: "GET",
933
+ path: `/${prefix}/api-keys`,
934
+ isPublic: false,
935
+ handler: async (ctx) => {
936
+ const identity = await resolveEndpointIdentity(ctx);
937
+ if (!identity || identity.role !== "admin") return {
938
+ status: 403,
939
+ body: {
940
+ error: "Forbidden",
941
+ message: "Admin access required"
942
+ }
943
+ };
944
+ if (!apiKeyEnabled) return {
945
+ status: 400,
946
+ body: { error: "API keys are not enabled" }
947
+ };
948
+ try {
949
+ const result = await callBetterAuthHandler(auth, ctx.request, "/api-key/list", {
950
+ method: "GET",
951
+ query: ctx.query
952
+ });
953
+ if (result && result.status >= 200 && result.status < 300) return {
954
+ status: 200,
955
+ body: result.body
956
+ };
957
+ return {
958
+ status: result?.status ?? 500,
959
+ body: result?.body ?? { error: "Failed to list API keys" }
960
+ };
961
+ } catch (err) {
962
+ endpointLogger.error("Failed to list API keys", {
963
+ identity: sanitizeForLogging(identity),
964
+ error: getErrorLogDetails(err)
965
+ });
966
+ return toAuthApiErrorResponse("Failed to list API keys", err);
967
+ }
968
+ }
969
+ },
970
+ {
971
+ method: "POST",
972
+ path: `/${prefix}/api-keys`,
973
+ isPublic: false,
974
+ handler: async (ctx) => {
975
+ const identity = await resolveEndpointIdentity(ctx);
976
+ if (!identity || identity.role !== "admin") return {
977
+ status: 403,
978
+ body: {
979
+ error: "Forbidden",
980
+ message: "Admin access required"
981
+ }
982
+ };
983
+ if (!apiKeyEnabled) return {
984
+ status: 400,
985
+ body: { error: "API keys are not enabled" }
986
+ };
987
+ const { name, expiresIn, prefix: keyPrefix } = ctx.body;
988
+ try {
989
+ const result = await callBetterAuthHandler(auth, ctx.request, "/api-key/create", {
990
+ method: "POST",
991
+ body: {
992
+ name: name || void 0,
993
+ expiresIn: expiresIn || void 0,
994
+ prefix: keyPrefix || void 0
995
+ }
996
+ });
997
+ if (result && result.status >= 200 && result.status < 300) return {
998
+ status: 201,
999
+ body: result.body
1000
+ };
1001
+ return {
1002
+ status: result?.status ?? 500,
1003
+ body: result?.body ?? { error: "Failed to create API key" }
1004
+ };
1005
+ } catch (err) {
1006
+ endpointLogger.error("Failed to create API key", {
1007
+ identity: sanitizeForLogging(identity),
1008
+ error: getErrorLogDetails(err)
1009
+ });
1010
+ return toAuthApiErrorResponse("Failed to create API key", err);
1011
+ }
1012
+ }
1013
+ },
1014
+ {
1015
+ method: "DELETE",
1016
+ path: `/${prefix}/api-keys/:keyId`,
1017
+ isPublic: false,
1018
+ handler: async (ctx) => {
1019
+ const identity = await resolveEndpointIdentity(ctx);
1020
+ if (!identity || identity.role !== "admin") return {
1021
+ status: 403,
1022
+ body: {
1023
+ error: "Forbidden",
1024
+ message: "Admin access required"
1025
+ }
1026
+ };
1027
+ if (!apiKeyEnabled) return {
1028
+ status: 400,
1029
+ body: { error: "API keys are not enabled" }
1030
+ };
1031
+ const { keyId } = ctx.params;
1032
+ try {
1033
+ const result = await callBetterAuthHandler(auth, ctx.request, "/api-key/delete", {
1034
+ method: "POST",
1035
+ body: { keyId }
1036
+ });
1037
+ if (result && result.status >= 200 && result.status < 300) return {
1038
+ status: 200,
1039
+ body: { success: true }
1040
+ };
1041
+ return {
1042
+ status: result?.status ?? 500,
1043
+ body: result?.body ?? { error: "Failed to delete API key" }
1044
+ };
1045
+ } catch (err) {
1046
+ endpointLogger.error("Failed to delete API key", {
1047
+ identity: sanitizeForLogging(identity),
1048
+ params: sanitizeForLogging(ctx.params),
1049
+ error: getErrorLogDetails(err)
1050
+ });
1051
+ return toAuthApiErrorResponse("Failed to delete API key", err);
1052
+ }
1053
+ }
1054
+ },
725
1055
  {
726
1056
  method: "GET",
727
1057
  path: `/${prefix}/users`,
@@ -736,7 +1066,7 @@ function betterAuthPlugin(options) {
736
1066
  }
737
1067
  };
738
1068
  try {
739
- const api = auth.api;
1069
+ const api = requireAuth().api;
740
1070
  const headers = toHeaders(ctx.headers);
741
1071
  if (typeof api.listUsers === "function") {
742
1072
  const listUsers = api.listUsers;
@@ -803,7 +1133,7 @@ function betterAuthPlugin(options) {
803
1133
  body: { error: "role must be one of: " + AUTH_ASSIGNABLE_ROLES.join(", ") }
804
1134
  };
805
1135
  try {
806
- const api = auth.api;
1136
+ const api = requireAuth().api;
807
1137
  const headers = toHeaders(ctx.headers);
808
1138
  let result = null;
809
1139
  if (typeof api.createUser === "function") {
@@ -894,7 +1224,7 @@ function betterAuthPlugin(options) {
894
1224
  body: { error: "role must be one of: " + AUTH_ASSIGNABLE_ROLES.join(", ") }
895
1225
  };
896
1226
  try {
897
- const api = auth.api;
1227
+ const api = requireAuth().api;
898
1228
  const headers = toHeaders(ctx.headers);
899
1229
  if (typeof api.setRole === "function") {
900
1230
  const setRole = api.setRole;
@@ -979,7 +1309,7 @@ function betterAuthPlugin(options) {
979
1309
  body: { error: "Cannot delete your own account" }
980
1310
  };
981
1311
  try {
982
- const api = auth.api;
1312
+ const api = requireAuth().api;
983
1313
  const headers = toHeaders(ctx.headers);
984
1314
  if (typeof api.removeUser === "function") {
985
1315
  const removeUser = api.removeUser;
@@ -1077,7 +1407,7 @@ function betterAuthPlugin(options) {
1077
1407
  betterAuthBasePath = auth.options?.basePath ?? "/api/auth";
1078
1408
  pluginContext.logger.info(`Better Auth plugin initialized (prefix: ${prefix}, basePath: ${betterAuthBasePath})`);
1079
1409
  if (globalAdmins.length === 0) {
1080
- pluginContext.logger.debug("No global admins configured. Pass `globalAdmins` to betterAuthPlugin(...) to seed admin access.");
1410
+ pluginContext.logger.debug("No global admins configured. Pass `globalAdmins` to authentication(...) to seed admin access.");
1081
1411
  return;
1082
1412
  }
1083
1413
  for (const configuredAdmin of globalAdmins) {
@@ -1098,7 +1428,7 @@ function betterAuthPlugin(options) {
1098
1428
  } else pluginContext.logger.debug(`Admin user already configured: ${adminEmail}`);
1099
1429
  continue;
1100
1430
  }
1101
- const api = auth.api;
1431
+ const api = requireAuth().api;
1102
1432
  let result = null;
1103
1433
  if (typeof api.createUser === "function") {
1104
1434
  const createUser = api.createUser;
@@ -1110,11 +1440,12 @@ function betterAuthPlugin(options) {
1110
1440
  name: adminName,
1111
1441
  role: "admin"
1112
1442
  }
1113
- }).catch((err) => {
1114
- pluginContext.logger.error?.(`createUser failed for ${adminEmail}: ${err instanceof Error ? err.message : String(err)}`);
1443
+ }).catch((_err) => {
1444
+ pluginContext.logger.debug?.(`createUser API requires auth, falling back to signUpEmail for ${adminEmail}`);
1115
1445
  return null;
1116
1446
  });
1117
- } else if (typeof api.signUpEmail === "function") {
1447
+ }
1448
+ if (!result?.user && typeof api.signUpEmail === "function") {
1118
1449
  const signUpEmail = api.signUpEmail;
1119
1450
  result = await signUpEmail({ body: {
1120
1451
  email: adminEmail,
@@ -1124,7 +1455,8 @@ function betterAuthPlugin(options) {
1124
1455
  pluginContext.logger.error?.(`signUpEmail failed for ${adminEmail}: ${err instanceof Error ? err.message : String(err)}`);
1125
1456
  return null;
1126
1457
  });
1127
- } else {
1458
+ }
1459
+ if (!result?.user && typeof api.createUser !== "function" && typeof api.signUpEmail !== "function") {
1128
1460
  pluginContext.logger.debug(`Could not create global admin ${adminEmail}: auth.api.createUser/signUpEmail are unavailable.`);
1129
1461
  continue;
1130
1462
  }
@@ -1159,6 +1491,29 @@ function isBetterAuthRoute(path, prefix, basePath) {
1159
1491
  return path.startsWith(`/plugins/${prefix}${basePath}`);
1160
1492
  }
1161
1493
  //#endregion
1162
- export { BETTER_AUTH_SCHEMA, betterAuthPlugin };
1494
+ //#region src/backend/index.ts
1495
+ /**
1496
+ * Create the auth plugin definition for Invect config.
1497
+ *
1498
+ * @example
1499
+ * ```ts
1500
+ * // Express (backend only):
1501
+ * auth({ adminEmail: '...' })
1502
+ *
1503
+ * // Next.js (with frontend):
1504
+ * import { authFrontend } from '@invect/user-auth/ui';
1505
+ * auth({ adminEmail: '...', frontend: authFrontend })
1506
+ * ```
1507
+ */
1508
+ function auth(options) {
1509
+ return {
1510
+ id: "user-auth",
1511
+ name: "User Authentication",
1512
+ backend: authentication(options),
1513
+ frontend: options.frontend
1514
+ };
1515
+ }
1516
+ //#endregion
1517
+ export { USER_AUTH_SCHEMA, auth, authentication };
1163
1518
 
1164
1519
  //# sourceMappingURL=index.mjs.map