@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.
- package/README.md +81 -72
- package/dist/backend/index.cjs +410 -54
- package/dist/backend/index.cjs.map +1 -1
- package/dist/backend/index.d.cts +456 -0
- package/dist/backend/index.d.cts.map +1 -0
- package/dist/backend/index.d.mts +456 -0
- package/dist/backend/index.d.mts.map +1 -0
- package/dist/backend/index.d.ts +28 -18
- package/dist/backend/index.d.ts.map +1 -1
- package/dist/backend/index.mjs +408 -53
- package/dist/backend/index.mjs.map +1 -1
- package/dist/backend/plugin.d.ts +15 -15
- package/dist/backend/plugin.d.ts.map +1 -1
- package/dist/backend/types.d.ts +85 -9
- package/dist/backend/types.d.ts.map +1 -1
- package/dist/frontend/components/ApiKeysDialog.d.ts +17 -0
- package/dist/frontend/components/ApiKeysDialog.d.ts.map +1 -0
- package/dist/frontend/components/AuthenticatedInvect.d.ts +10 -10
- package/dist/frontend/components/SignInForm.d.ts.map +1 -1
- package/dist/frontend/components/SignInPage.d.ts.map +1 -1
- package/dist/frontend/components/UserManagement.d.ts.map +1 -1
- package/dist/frontend/index.cjs +434 -58
- package/dist/frontend/index.cjs.map +1 -1
- package/dist/frontend/index.d.cts +317 -0
- package/dist/frontend/index.d.cts.map +1 -0
- package/dist/frontend/index.d.mts +317 -0
- package/dist/frontend/index.d.mts.map +1 -0
- package/dist/frontend/index.d.ts +3 -1
- package/dist/frontend/index.d.ts.map +1 -1
- package/dist/frontend/index.mjs +418 -43
- package/dist/frontend/index.mjs.map +1 -1
- package/dist/frontend/plugins/authFrontendPlugin.d.ts +2 -2
- package/dist/frontend/plugins/authFrontendPlugin.d.ts.map +1 -1
- package/dist/shared/types.d.cts +49 -0
- package/dist/shared/types.d.cts.map +1 -0
- package/dist/shared/types.d.mts +49 -0
- package/dist/shared/types.d.mts.map +1 -0
- package/package.json +68 -66
package/dist/backend/index.mjs
CHANGED
|
@@ -213,16 +213,16 @@ function getErrorLogDetails(error) {
|
|
|
213
213
|
return { value: error };
|
|
214
214
|
}
|
|
215
215
|
/**
|
|
216
|
-
* Abstract schema for
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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 `
|
|
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.
|
|
455
|
-
if (!dbConfig?.connectionString) throw new Error("Cannot create internal
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
...
|
|
511
|
-
...
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
730
|
+
* Create the Invect user-auth plugin (a light wrapper around Better Auth).
|
|
553
731
|
*
|
|
554
732
|
* This plugin:
|
|
555
733
|
*
|
|
556
|
-
* 1. **Proxies
|
|
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
|
|
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
|
|
570
|
-
* import {
|
|
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: [
|
|
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 {
|
|
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: [
|
|
772
|
+
* plugins: [authentication({ auth })],
|
|
595
773
|
* }));
|
|
596
774
|
* ```
|
|
597
775
|
*/
|
|
598
|
-
function
|
|
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
|
|
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: "
|
|
676
|
-
name: "
|
|
677
|
-
schema
|
|
678
|
-
requiredTables
|
|
679
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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((
|
|
1114
|
-
pluginContext.logger.
|
|
1443
|
+
}).catch((_err) => {
|
|
1444
|
+
pluginContext.logger.debug?.(`createUser API requires auth, falling back to signUpEmail for ${adminEmail}`);
|
|
1115
1445
|
return null;
|
|
1116
1446
|
});
|
|
1117
|
-
}
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|