@mostajs/auth 2.2.0 → 2.3.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.
- package/dist/lib/check-request.d.ts +54 -0
- package/dist/lib/check-request.js +112 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +2 -0
- package/package.json +3 -2
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { IDialect } from '@mostajs/orm';
|
|
2
|
+
export type AuthErrorCode = 'UNAUTHORIZED' | 'FORBIDDEN' | 'UNAVAILABLE' | 'AUTH_ERROR';
|
|
3
|
+
export interface CheckRequestParams {
|
|
4
|
+
/** Dialect of the metadata DB (where api_keys / users / roles tables live). */
|
|
5
|
+
dialect: IDialect | null | undefined;
|
|
6
|
+
/** Normalized HTTP-style headers (lowercased keys preferred). */
|
|
7
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
8
|
+
/** Query string params. */
|
|
9
|
+
query?: Record<string, string | string[] | undefined>;
|
|
10
|
+
/** Pre-resolved client IP (else derived from X-Forwarded-For). */
|
|
11
|
+
ip?: string;
|
|
12
|
+
/** Transport identifier — 'rest' | 'graphql' | 'mcp' | 'sse' | etc. */
|
|
13
|
+
transport: string;
|
|
14
|
+
/**
|
|
15
|
+
* (scope, value) checks the apikey must satisfy. The module is fully
|
|
16
|
+
* generic — it doesn't know what 'projects' or 'transports' mean.
|
|
17
|
+
* Common pattern :
|
|
18
|
+
* [{scope:'projects',value:slug}, {scope:'transports',value:'rest'},
|
|
19
|
+
* {scope:'operations',value:'read'|'write'|'admin'}]
|
|
20
|
+
*/
|
|
21
|
+
checks?: Array<{
|
|
22
|
+
scope: string;
|
|
23
|
+
value: string;
|
|
24
|
+
}>;
|
|
25
|
+
/** Allow request through without apikey (legacy / public-mode). */
|
|
26
|
+
openMode?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface CheckRequestResult {
|
|
29
|
+
ok: boolean;
|
|
30
|
+
/** HTTP status code suggested for the failure response (200 if ok). */
|
|
31
|
+
status: number;
|
|
32
|
+
/** Suggested JSON body for the failure response. */
|
|
33
|
+
body?: any;
|
|
34
|
+
/** Authorization context built from the resolved apikey (if ok). */
|
|
35
|
+
ctx?: {
|
|
36
|
+
transport: string;
|
|
37
|
+
apiKey?: string;
|
|
38
|
+
projectName?: string;
|
|
39
|
+
accountId?: string;
|
|
40
|
+
apikeyId?: string;
|
|
41
|
+
subscription?: string;
|
|
42
|
+
permissions?: Record<string, any>;
|
|
43
|
+
meta?: Record<string, any>;
|
|
44
|
+
};
|
|
45
|
+
/** Resolved apikey row (read-only — sensitive fields like hash are present). */
|
|
46
|
+
apikey?: any;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Single-call request authorization.
|
|
50
|
+
*
|
|
51
|
+
* Returns a verdict that the consumer maps onto its framework's response
|
|
52
|
+
* (e.g. `if (!result.ok) reply.code(result.status).send(result.body)`).
|
|
53
|
+
*/
|
|
54
|
+
export declare function checkRequest(params: CheckRequestParams): Promise<CheckRequestResult>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// @mostajs/auth — Framework-agnostic request authorization check.
|
|
2
|
+
//
|
|
3
|
+
// Orchestrateur unique pour valider une requête HTTP entrante (toutes
|
|
4
|
+
// frameworks : Fastify, Express, Hono, Next.js route handlers, Cloudflare
|
|
5
|
+
// Workers, Bun.serve, etc.).
|
|
6
|
+
//
|
|
7
|
+
// Combine :
|
|
8
|
+
// - apikey check (délégué à @mostajs/api-keys → checkApiKey)
|
|
9
|
+
// - (futur) session check NextAuth
|
|
10
|
+
// - (futur) RBAC permission check via @mostajs/rbac
|
|
11
|
+
//
|
|
12
|
+
// Le consommateur passe un objet `request` normalisé (headers/query/ip) +
|
|
13
|
+
// les `checks` de scopes — le module retourne un verdict unifié.
|
|
14
|
+
//
|
|
15
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
16
|
+
function pickHeader(h, name) {
|
|
17
|
+
if (!h)
|
|
18
|
+
return undefined;
|
|
19
|
+
const v = h[name] ?? h[name.toLowerCase()];
|
|
20
|
+
if (v == null)
|
|
21
|
+
return undefined;
|
|
22
|
+
return Array.isArray(v) ? v[0] : String(v);
|
|
23
|
+
}
|
|
24
|
+
function pickQuery(q, name) {
|
|
25
|
+
if (!q)
|
|
26
|
+
return undefined;
|
|
27
|
+
const v = q[name];
|
|
28
|
+
if (v == null)
|
|
29
|
+
return undefined;
|
|
30
|
+
return Array.isArray(v) ? v[0] : String(v);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Single-call request authorization.
|
|
34
|
+
*
|
|
35
|
+
* Returns a verdict that the consumer maps onto its framework's response
|
|
36
|
+
* (e.g. `if (!result.ok) reply.code(result.status).send(result.body)`).
|
|
37
|
+
*/
|
|
38
|
+
export async function checkRequest(params) {
|
|
39
|
+
const { dialect, headers, query, ip, transport, checks = [], openMode = false } = params;
|
|
40
|
+
// Extract auth identifiers from the request
|
|
41
|
+
const apiKey = pickHeader(headers, 'x-api-key') ??
|
|
42
|
+
(pickHeader(headers, 'authorization')?.replace(/^Bearer\s+/i, '').trim() || undefined) ??
|
|
43
|
+
pickQuery(query, 'apikey') ??
|
|
44
|
+
pickQuery(query, 'api_key');
|
|
45
|
+
const projectName = pickHeader(headers, 'x-project') ??
|
|
46
|
+
pickQuery(query, 'project');
|
|
47
|
+
const resolvedIp = ip
|
|
48
|
+
?? pickHeader(headers, 'x-forwarded-for')?.split(',')[0]?.trim()
|
|
49
|
+
?? pickHeader(headers, 'x-real-ip');
|
|
50
|
+
const baseCtx = {
|
|
51
|
+
transport,
|
|
52
|
+
apiKey,
|
|
53
|
+
projectName,
|
|
54
|
+
meta: { ip: resolvedIp },
|
|
55
|
+
};
|
|
56
|
+
// No apikey → either openMode passes through, or 401
|
|
57
|
+
if (!apiKey) {
|
|
58
|
+
if (openMode)
|
|
59
|
+
return { ok: true, status: 200, ctx: baseCtx };
|
|
60
|
+
return {
|
|
61
|
+
ok: false, status: 401,
|
|
62
|
+
body: { status: 'error', error: { code: 'UNAUTHORIZED',
|
|
63
|
+
message: 'API key required (X-API-Key header, Authorization: Bearer, or ?apikey=)' } },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// No metadata DB → can't verify
|
|
67
|
+
if (!dialect) {
|
|
68
|
+
if (openMode)
|
|
69
|
+
return { ok: true, status: 200, ctx: baseCtx };
|
|
70
|
+
return {
|
|
71
|
+
ok: false, status: 503,
|
|
72
|
+
body: { status: 'error', error: { code: 'UNAVAILABLE',
|
|
73
|
+
message: 'API key validation unavailable (no metadata DB)' } },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// Delegate to @mostajs/api-keys for the actual cryptographic + scope check
|
|
77
|
+
try {
|
|
78
|
+
const { checkApiKey } = await import('@mostajs/api-keys/server');
|
|
79
|
+
const result = await checkApiKey(dialect, {
|
|
80
|
+
rawKey: apiKey,
|
|
81
|
+
checks,
|
|
82
|
+
ip: resolvedIp,
|
|
83
|
+
});
|
|
84
|
+
if (!result.ok) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
status: result.code === 'UNAUTHORIZED' ? 401 : 403,
|
|
88
|
+
body: { status: 'error', error: { code: result.code, message: result.message } },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const apikey = result.apikey;
|
|
92
|
+
return {
|
|
93
|
+
ok: true,
|
|
94
|
+
status: 200,
|
|
95
|
+
apikey,
|
|
96
|
+
ctx: {
|
|
97
|
+
...baseCtx,
|
|
98
|
+
subscription: apikey?.label || apikey?.id,
|
|
99
|
+
permissions: apikey?.permissions?.scopes ?? {},
|
|
100
|
+
accountId: apikey?.account || apikey?.accountId,
|
|
101
|
+
apikeyId: apikey?.id,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false, status: 500,
|
|
108
|
+
body: { status: 'error', error: { code: 'AUTH_ERROR',
|
|
109
|
+
message: e?.message || 'auth check failed' } },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export { createAuthChecks } from './lib/auth-check';
|
|
|
3
3
|
export { createAuthMiddleware } from './middleware/auth-middleware';
|
|
4
4
|
export type { AuthMiddlewareOptions } from './middleware/auth-middleware';
|
|
5
5
|
export { hashPassword, comparePassword } from './lib/password';
|
|
6
|
+
export { checkRequest } from './lib/check-request';
|
|
7
|
+
export type { CheckRequestParams, CheckRequestResult, AuthErrorCode } from './lib/check-request';
|
|
6
8
|
export { getPermissionsForRoleFromDB } from '@mostajs/rbac/lib/permissions-server';
|
|
7
9
|
export { seedRBAC } from '@mostajs/rbac/lib/rbac-seed';
|
|
8
10
|
export type { SeedRBACOptions } from '@mostajs/rbac/lib/rbac-seed';
|
package/dist/server.js
CHANGED
|
@@ -7,6 +7,8 @@ export { createAuthChecks } from './lib/auth-check';
|
|
|
7
7
|
export { createAuthMiddleware } from './middleware/auth-middleware';
|
|
8
8
|
// Password utils
|
|
9
9
|
export { hashPassword, comparePassword } from './lib/password';
|
|
10
|
+
// Framework-agnostic request authorization (apikey + scope checks)
|
|
11
|
+
export { checkRequest } from './lib/check-request';
|
|
10
12
|
// Server-side permission DB lookup (re-export from rbac/server)
|
|
11
13
|
export { getPermissionsForRoleFromDB } from '@mostajs/rbac/lib/permissions-server';
|
|
12
14
|
// RBAC seed (re-export from rbac/server)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/auth",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.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",
|
|
@@ -120,7 +120,6 @@
|
|
|
120
120
|
"dependencies": {
|
|
121
121
|
"@mostajs/config": "^1.0.0",
|
|
122
122
|
"@mostajs/net": "^2.0.0",
|
|
123
|
-
"@mostajs/orm": "^1.7.0",
|
|
124
123
|
"bcryptjs": "^2.4.3"
|
|
125
124
|
},
|
|
126
125
|
"peerDependencies": {
|
|
@@ -130,6 +129,8 @@
|
|
|
130
129
|
"react": ">=18"
|
|
131
130
|
},
|
|
132
131
|
"devDependencies": {
|
|
132
|
+
"@mostajs/api-keys": "^0.2.0",
|
|
133
|
+
"@mostajs/orm": "^1.13.1",
|
|
133
134
|
"@mostajs/rbac": "^2.0.3",
|
|
134
135
|
"@types/bcryptjs": "^2.4.0",
|
|
135
136
|
"@types/node": "^25.3.3",
|