@mostajs/auth 2.3.2 → 2.4.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.
@@ -24,6 +24,16 @@ export interface CheckRequestParams {
24
24
  }>;
25
25
  /** Allow request through without apikey (legacy / public-mode). */
26
26
  openMode?: boolean;
27
+ /**
28
+ * When no apikey is provided, fall back to a labeled apikey stored in
29
+ * the api_keys table (typically `public-default`). The fallback's
30
+ * permissions still apply — so a public-default key with read-only
31
+ * scope keeps writes blocked even when no key is sent.
32
+ *
33
+ * Use case : MCP endpoint that wants to preserve compat with existing
34
+ * mcp.so / Claude Desktop users who haven't published the apikey yet.
35
+ */
36
+ fallbackPublicLabel?: string;
27
37
  }
28
38
  export interface CheckRequestResult {
29
39
  ok: boolean;
@@ -36,7 +36,7 @@ function pickQuery(q, name) {
36
36
  * (e.g. `if (!result.ok) reply.code(result.status).send(result.body)`).
37
37
  */
38
38
  export async function checkRequest(params) {
39
- const { dialect, headers, query, ip, transport, checks = [], openMode = false } = params;
39
+ const { dialect, headers, query, ip, transport, checks = [], openMode = false, fallbackPublicLabel } = params;
40
40
  // Extract auth identifiers from the request
41
41
  const apiKey = pickHeader(headers, 'x-api-key') ??
42
42
  (pickHeader(headers, 'authorization')?.replace(/^Bearer\s+/i, '').trim() || undefined) ??
@@ -53,8 +53,53 @@ export async function checkRequest(params) {
53
53
  projectName,
54
54
  meta: { ip: resolvedIp },
55
55
  };
56
- // No apikey → either openMode passes through, or 401
56
+ // No apikey → either fallback to a public labeled key, or openMode pass-through, or 401
57
57
  if (!apiKey) {
58
+ // Fallback to a labeled public apikey (e.g. 'public-default') if configured.
59
+ // Looks up the row by label, applies its scope checks. Useful for MCP /
60
+ // demo endpoints that want to preserve compat with users who haven't yet
61
+ // published the public apikey in their config.
62
+ if (fallbackPublicLabel && dialect) {
63
+ try {
64
+ const { getApiKeyRepo, isScopeAuthorized } = await import('@mostajs/api-keys/server');
65
+ const repo = getApiKeyRepo(dialect);
66
+ const fallbackKey = await repo.findOne({ label: fallbackPublicLabel, enabled: true });
67
+ if (fallbackKey) {
68
+ // Normalize legacy permission shape (move projects/operations/transports into scopes)
69
+ const perms = fallbackKey.permissions || {};
70
+ const scopes = { ...(perms.scopes ?? {}) };
71
+ for (const k of ['projects', 'operations', 'transports']) {
72
+ if (perms[k] !== undefined && scopes[k] === undefined)
73
+ scopes[k] = perms[k];
74
+ }
75
+ const normPerms = { ...perms, scopes };
76
+ // Run the same scope checks against the public key's perms
77
+ for (const c of checks) {
78
+ if (!isScopeAuthorized(normPerms, c.scope, c.value)) {
79
+ return {
80
+ ok: false, status: 403,
81
+ body: { status: 'error', error: { code: 'FORBIDDEN',
82
+ message: `Public access not authorized for ${c.scope}="${c.value}"`, } },
83
+ };
84
+ }
85
+ }
86
+ return {
87
+ ok: true, status: 200,
88
+ apikey: fallbackKey,
89
+ ctx: {
90
+ ...baseCtx,
91
+ subscription: fallbackKey.label || fallbackKey.id,
92
+ permissions: normPerms.scopes ?? {},
93
+ accountId: fallbackKey.account || fallbackKey.accountId,
94
+ apikeyId: fallbackKey.id,
95
+ },
96
+ };
97
+ }
98
+ }
99
+ catch {
100
+ // Fallback failed — fall through to standard 401 / openMode
101
+ }
102
+ }
58
103
  if (openMode)
59
104
  return { ok: true, status: 200, ctx: baseCtx };
60
105
  return {
@@ -0,0 +1,57 @@
1
+ export interface CredentialsProviderConfig {
2
+ /** Lookup the user row by email (case-insensitive). Return null if not found.
3
+ * Conseillé : passer une fonction qui utilise UserRepository de @mostajs/rbac
4
+ * pour bénéficier de findByEmail (qui lowercase l'email côté repo). */
5
+ findUserByEmail: (email: string) => Promise<any | null>;
6
+ /** Default role assigned when the user has no role relations (default: 'user'). */
7
+ defaultRole?: string;
8
+ /** Custom check before allowing login (e.g. email verified, 2FA).
9
+ * Return false to block. Default: status !== 'disabled' && status !== 'locked'. */
10
+ checkUserAllowed?: (user: any) => boolean | Promise<boolean>;
11
+ /** Custom role resolution. Default: first user.roles[].name or fallback. */
12
+ resolveRole?: (user: any) => string | Promise<string>;
13
+ /** Custom display name. Default: "<firstName> <lastName>" trimmed → email. */
14
+ resolveName?: (user: any) => string;
15
+ /** Provider id (default: 'credentials'). Useful if multiple providers coexist. */
16
+ id?: string;
17
+ /** Provider display name (default: 'Email + Password'). */
18
+ name?: string;
19
+ }
20
+ /**
21
+ * Build a NextAuth Credentials provider config (returned as plain object,
22
+ * directly passable to `NextAuth({ providers: [provider], ... })` without
23
+ * any next-auth imports côté consumer).
24
+ *
25
+ * Usage :
26
+ * import { createCredentialsProvider } from '@mostajs/auth/server'
27
+ *
28
+ * NextAuth({
29
+ * providers: [
30
+ * createCredentialsProvider({
31
+ * findUserByEmail: async (email) => userRepo.findByEmail(email),
32
+ * defaultRole: 'user',
33
+ * }),
34
+ * ],
35
+ * })
36
+ */
37
+ export declare function createCredentialsProvider(config: CredentialsProviderConfig): {
38
+ id: string;
39
+ name: string;
40
+ type: "credentials";
41
+ credentials: {
42
+ email: {
43
+ label: string;
44
+ type: string;
45
+ };
46
+ password: {
47
+ label: string;
48
+ type: string;
49
+ };
50
+ };
51
+ authorize(credentials: any): Promise<{
52
+ id: any;
53
+ email: any;
54
+ name: string;
55
+ role: string;
56
+ } | null>;
57
+ };
@@ -0,0 +1,84 @@
1
+ // @mostajs/auth — Email + password credentials provider for NextAuth
2
+ //
3
+ // Factorise le boilerplate NextAuth Credentials que chaque app refait. Le
4
+ // consumer obtient un provider PRÊT À L'EMPLOI à passer à NextAuth({providers}) :
5
+ // il n'importe ni `next-auth/providers/credentials`, ni bcrypt, ni le
6
+ // UserRepository — tout est encapsulé.
7
+ //
8
+ // Pendant naturel de createApiKeyProvider (même style de retour : objet
9
+ // provider config plat). Différence : authentication via email/password
10
+ // (UI register/signin) vs API key (programmatic).
11
+ //
12
+ // Le caller fournit `findUserByEmail` (ou laisse passer pour un default
13
+ // fail-closed). Pour leverager directement @mostajs/rbac, il suffit de
14
+ // passer `(email) => new UserRepository(dialect).findByEmail(email)`.
15
+ //
16
+ // Author: Dr Hamid MADANI <drmdh@msn.com>
17
+ import { comparePassword } from './password.js';
18
+ function defaultStatusCheck(user) {
19
+ return user.status !== 'disabled' && user.status !== 'locked';
20
+ }
21
+ function defaultRoleResolver(user, fallback) {
22
+ if (user.roles?.length > 0) {
23
+ const r = user.roles[0];
24
+ return typeof r === 'string' ? r : (r?.name ?? fallback);
25
+ }
26
+ return user.role ?? fallback;
27
+ }
28
+ function defaultNameResolver(user) {
29
+ return `${user.firstName ?? ''} ${user.lastName ?? ''}`.trim() || (user.email ?? '');
30
+ }
31
+ /**
32
+ * Build a NextAuth Credentials provider config (returned as plain object,
33
+ * directly passable to `NextAuth({ providers: [provider], ... })` without
34
+ * any next-auth imports côté consumer).
35
+ *
36
+ * Usage :
37
+ * import { createCredentialsProvider } from '@mostajs/auth/server'
38
+ *
39
+ * NextAuth({
40
+ * providers: [
41
+ * createCredentialsProvider({
42
+ * findUserByEmail: async (email) => userRepo.findByEmail(email),
43
+ * defaultRole: 'user',
44
+ * }),
45
+ * ],
46
+ * })
47
+ */
48
+ export function createCredentialsProvider(config) {
49
+ const defaultRole = config.defaultRole ?? 'user';
50
+ const checkUserAllowed = config.checkUserAllowed ?? defaultStatusCheck;
51
+ const resolveRole = config.resolveRole ?? ((u) => defaultRoleResolver(u, defaultRole));
52
+ const resolveName = config.resolveName ?? defaultNameResolver;
53
+ return {
54
+ id: config.id ?? 'credentials',
55
+ name: config.name ?? 'Email + Password',
56
+ type: 'credentials',
57
+ credentials: {
58
+ email: { label: 'Email', type: 'email' },
59
+ password: { label: 'Password', type: 'password' },
60
+ },
61
+ async authorize(credentials) {
62
+ if (!credentials?.email || !credentials?.password)
63
+ return null;
64
+ const email = String(credentials.email).toLowerCase().trim();
65
+ const user = await config.findUserByEmail(email);
66
+ if (!user)
67
+ return null;
68
+ const allowed = await checkUserAllowed(user);
69
+ if (!allowed)
70
+ return null;
71
+ const valid = await comparePassword(String(credentials.password), user.password);
72
+ if (!valid)
73
+ return null;
74
+ const role = await resolveRole(user);
75
+ const name = resolveName(user);
76
+ return {
77
+ id: user.id,
78
+ email: user.email,
79
+ name,
80
+ role,
81
+ };
82
+ },
83
+ };
84
+ }
package/dist/server.d.ts CHANGED
@@ -20,5 +20,7 @@ export { createPasswordResetHandlers, generateResetToken } from './lib/password-
20
20
  export type { PasswordResetConfig } from './lib/password-reset';
21
21
  export { createApiKeyProvider } from './lib/apikey-provider';
22
22
  export type { ApiKeyProviderConfig } from './lib/apikey-provider';
23
+ export { createCredentialsProvider } from './lib/credentials-provider';
24
+ export type { CredentialsProviderConfig } from './lib/credentials-provider';
23
25
  export { enrichTokenWithPlan, enrichSessionWithPlan } from './lib/session-enrichment';
24
26
  export type { SessionEnrichmentConfig } from './lib/session-enrichment';
package/dist/server.js CHANGED
@@ -27,5 +27,6 @@ export { createVerificationHandlers, generateVerifyToken } from './lib/email-ver
27
27
  export { createPasswordResetHandlers, generateResetToken } from './lib/password-reset.js';
28
28
  // API key provider
29
29
  export { createApiKeyProvider } from './lib/apikey-provider.js';
30
+ export { createCredentialsProvider } from './lib/credentials-provider.js';
30
31
  // Session enrichment
31
32
  export { enrichTokenWithPlan, enrichSessionWithPlan } from './lib/session-enrichment.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/auth",
3
- "version": "2.3.2",
3
+ "version": "2.4.0",
4
4
  "description": "Authentication — NextAuth, password hashing, session management",
5
5
  "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
6
  "license": "AGPL-3.0-or-later",
@@ -83,6 +83,11 @@
83
83
  "import": "./dist/lib/apikey-provider.js",
84
84
  "default": "./dist/lib/apikey-provider.js"
85
85
  },
86
+ "./lib/credentials-provider": {
87
+ "types": "./dist/lib/credentials-provider.d.ts",
88
+ "import": "./dist/lib/credentials-provider.js",
89
+ "default": "./dist/lib/credentials-provider.js"
90
+ },
86
91
  "./lib/check-request": {
87
92
  "types": "./dist/lib/check-request.d.ts",
88
93
  "import": "./dist/lib/check-request.js",
@@ -120,7 +125,7 @@
120
125
  },
121
126
  "scripts": {
122
127
  "build": "tsc && npm run fix-esm",
123
- "fix-esm": "find dist -name '*.js' -exec sed -i -E \"s|from '(\\\\.{1,2}/[^']+)'(;?)|from '\\\\1.js'\\\\2|g; s|from \\\"(\\\\.{1,2}/[^\\\"]+)\\\"(;?)|from \\\"\\\\1.js\\\"\\\\2|g\" {} \\; && find dist -name '*.js' -exec sed -i -E \"s|\\\\.js\\\\.js|.js|g\" {} \\;",
128
+ "fix-esm": "find dist -name '*.js' -exec sed -i -E \"s|from '(\\\\.{1,2}/[^']+)'(;?)|from '\\\\1.js'\\\\2|g; s|from \\\"(\\\\.{1,2}/[^\\\"]+)\\\"(;?)|from \\\"\\\\1.js\\\"\\\\2|g\" {} \\; && find dist -name '*.js' -exec sed -i -E \"s|\\\\.js\\\\.js|.js|g; s|\\\\.json\\\\.js|.json|g; s|\\\\.css\\\\.js|.css|g\" {} \\;",
124
129
  "prepublishOnly": "npm run build"
125
130
  },
126
131
  "dependencies": {