@airdraft/plugin-auth 0.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.
- package/CHANGELOG.md +5 -0
- package/dist/CredentialsProvider.d.ts +49 -0
- package/dist/CredentialsProvider.d.ts.map +1 -0
- package/dist/CredentialsProvider.js +441 -0
- package/dist/CredentialsProvider.js.map +1 -0
- package/dist/GitHubOAuthProvider.d.ts +49 -0
- package/dist/GitHubOAuthProvider.d.ts.map +1 -0
- package/dist/GitHubOAuthProvider.js +224 -0
- package/dist/GitHubOAuthProvider.js.map +1 -0
- package/dist/GoogleOAuthProvider.d.ts +49 -0
- package/dist/GoogleOAuthProvider.d.ts.map +1 -0
- package/dist/GoogleOAuthProvider.js +205 -0
- package/dist/GoogleOAuthProvider.js.map +1 -0
- package/dist/UserStore.d.ts +74 -0
- package/dist/UserStore.d.ts.map +1 -0
- package/dist/UserStore.js +220 -0
- package/dist/UserStore.js.map +1 -0
- package/dist/blocklist.d.ts +7 -0
- package/dist/blocklist.d.ts.map +1 -0
- package/dist/blocklist.js +23 -0
- package/dist/blocklist.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +48 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/withAuth.d.ts +32 -0
- package/dist/withAuth.d.ts.map +1 -0
- package/dist/withAuth.js +84 -0
- package/dist/withAuth.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { scrypt as scryptCallback, randomBytes, timingSafeEqual, } from 'node:crypto';
|
|
2
|
+
import { readFile, writeFile, mkdir, access } from 'node:fs/promises';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Scrypt helpers (C5)
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
const SCRYPT_PARAMS = { N: 16384, r: 8, p: 1 };
|
|
8
|
+
const KEY_LEN = 64;
|
|
9
|
+
function scrypt(password, salt) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
scryptCallback(password, salt, KEY_LEN, SCRYPT_PARAMS, (err, key) => (err ? reject(err) : resolve(key)));
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
async function hashPassword(password) {
|
|
15
|
+
const salt = randomBytes(16);
|
|
16
|
+
const key = await scrypt(password, salt);
|
|
17
|
+
return `${salt.toString('hex')}:${key.toString('hex')}`;
|
|
18
|
+
}
|
|
19
|
+
async function checkPassword(password, stored) {
|
|
20
|
+
const [saltHex, keyHex] = stored.split(':');
|
|
21
|
+
if (!saltHex || !keyHex)
|
|
22
|
+
return false;
|
|
23
|
+
const salt = Buffer.from(saltHex, 'hex');
|
|
24
|
+
const expected = Buffer.from(keyHex, 'hex');
|
|
25
|
+
const actual = await scrypt(password, salt);
|
|
26
|
+
if (expected.length !== actual.length)
|
|
27
|
+
return false;
|
|
28
|
+
return timingSafeEqual(expected, actual);
|
|
29
|
+
}
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// UserStore (C4)
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
const GITIGNORE_CONTENT = '# Airdraft — local secrets\nusers.json\ntokens.json\n';
|
|
34
|
+
const INVITE_TTL_SECONDS = 48 * 60 * 60;
|
|
35
|
+
/**
|
|
36
|
+
* Persistent user store backed by `.airdraft/users.json`.
|
|
37
|
+
*
|
|
38
|
+
* - Passwords are hashed with `scrypt` (N=16384, r=8, p=1) — OWASP compliant.
|
|
39
|
+
* - On first write, auto-creates `.airdraft/.gitignore` to prevent accidental commits.
|
|
40
|
+
* - Emits a console.warn on serverless runtimes (no persistent filesystem).
|
|
41
|
+
*
|
|
42
|
+
* ```ts
|
|
43
|
+
* const store = UserStore.json() // uses .airdraft/users.json relative to cwd
|
|
44
|
+
* await store.create('alice@example.com', 'hunter2', 'admin', 'Alice')
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export class UserStore {
|
|
48
|
+
filePath;
|
|
49
|
+
data = null;
|
|
50
|
+
constructor(filePath) {
|
|
51
|
+
this.filePath = filePath;
|
|
52
|
+
this.warnIfServerless();
|
|
53
|
+
}
|
|
54
|
+
/** Creates a UserStore backed by `.airdraft/users.json` (or custom path). */
|
|
55
|
+
static json(path) {
|
|
56
|
+
const p = path ?? join(process.cwd(), '.airdraft', 'users.json');
|
|
57
|
+
return new UserStore(p);
|
|
58
|
+
}
|
|
59
|
+
// -------------------------------------------------------------------------
|
|
60
|
+
// Private I/O
|
|
61
|
+
// -------------------------------------------------------------------------
|
|
62
|
+
async load() {
|
|
63
|
+
if (this.data)
|
|
64
|
+
return this.data;
|
|
65
|
+
try {
|
|
66
|
+
const raw = await readFile(this.filePath, 'utf8');
|
|
67
|
+
this.data = JSON.parse(raw);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
if (err.code === 'ENOENT') {
|
|
71
|
+
this.data = { users: [], invites: [] };
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return this.data;
|
|
78
|
+
}
|
|
79
|
+
async save(data) {
|
|
80
|
+
this.data = data;
|
|
81
|
+
const dir = dirname(this.filePath);
|
|
82
|
+
await mkdir(dir, { recursive: true });
|
|
83
|
+
// Auto-write .gitignore on first use (C4)
|
|
84
|
+
const gitignorePath = join(dir, '.gitignore');
|
|
85
|
+
const hasGitignore = await access(gitignorePath).then(() => true).catch(() => false);
|
|
86
|
+
if (!hasGitignore) {
|
|
87
|
+
await writeFile(gitignorePath, GITIGNORE_CONTENT, 'utf8');
|
|
88
|
+
}
|
|
89
|
+
await writeFile(this.filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
90
|
+
}
|
|
91
|
+
warnIfServerless() {
|
|
92
|
+
const platform = process.env['VERCEL'] ?? process.env['AWS_LAMBDA_FUNCTION_NAME'];
|
|
93
|
+
if (platform) {
|
|
94
|
+
console.warn('[airdraft/plugin-auth] UserStore uses a local JSON file which may not persist ' +
|
|
95
|
+
'on serverless platforms. Consider an alternative persistence layer for production.');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// -------------------------------------------------------------------------
|
|
99
|
+
// User operations
|
|
100
|
+
// -------------------------------------------------------------------------
|
|
101
|
+
async findByEmail(email) {
|
|
102
|
+
const data = await this.load();
|
|
103
|
+
return data.users.find((u) => u.email.toLowerCase() === email.toLowerCase()) ?? null;
|
|
104
|
+
}
|
|
105
|
+
async list() {
|
|
106
|
+
const data = await this.load();
|
|
107
|
+
return data.users.map(({ passwordHash: _, ...u }) => ({ ...u, passwordHash: '***' }));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Creates a new user with a scrypt-hashed password.
|
|
111
|
+
* Throws if a user with the same email already exists.
|
|
112
|
+
*/
|
|
113
|
+
async create(email, password, role, name) {
|
|
114
|
+
const data = await this.load();
|
|
115
|
+
const existing = data.users.find((u) => u.email.toLowerCase() === email.toLowerCase());
|
|
116
|
+
if (existing)
|
|
117
|
+
throw new Error(`User '${email}' already exists`);
|
|
118
|
+
const passwordHash = await hashPassword(password);
|
|
119
|
+
const user = {
|
|
120
|
+
id: randomBytes(12).toString('hex'),
|
|
121
|
+
email: email.toLowerCase(),
|
|
122
|
+
name,
|
|
123
|
+
role,
|
|
124
|
+
passwordHash,
|
|
125
|
+
createdAt: Math.floor(Date.now() / 1000),
|
|
126
|
+
};
|
|
127
|
+
data.users.push(user);
|
|
128
|
+
await this.save(data);
|
|
129
|
+
const { passwordHash: _, ...safeUser } = user;
|
|
130
|
+
return safeUser;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Verifies email + password and returns the user record (without hash) on success.
|
|
134
|
+
* Returns null if the user doesn't exist or the password is wrong.
|
|
135
|
+
*/
|
|
136
|
+
async verifyPassword(email, password) {
|
|
137
|
+
const data = await this.load();
|
|
138
|
+
const user = data.users.find((u) => u.email.toLowerCase() === email.toLowerCase());
|
|
139
|
+
if (!user)
|
|
140
|
+
return null;
|
|
141
|
+
const ok = await checkPassword(password, user.passwordHash);
|
|
142
|
+
if (!ok)
|
|
143
|
+
return null;
|
|
144
|
+
const { passwordHash: _, ...safeUser } = user;
|
|
145
|
+
return safeUser;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Updates the role of an existing user. Config-level roles take precedence
|
|
149
|
+
* over the store at runtime (C3), but this persists the base role.
|
|
150
|
+
*/
|
|
151
|
+
async updateRole(email, role) {
|
|
152
|
+
const data = await this.load();
|
|
153
|
+
const idx = data.users.findIndex((u) => u.email.toLowerCase() === email.toLowerCase());
|
|
154
|
+
if (idx === -1)
|
|
155
|
+
return null;
|
|
156
|
+
data.users[idx].role = role;
|
|
157
|
+
await this.save(data);
|
|
158
|
+
const { passwordHash: _, ...safeUser } = data.users[idx];
|
|
159
|
+
return safeUser;
|
|
160
|
+
}
|
|
161
|
+
/** Removes a user. Returns true if the user existed. */
|
|
162
|
+
async remove(email) {
|
|
163
|
+
const data = await this.load();
|
|
164
|
+
const before = data.users.length;
|
|
165
|
+
data.users = data.users.filter((u) => u.email.toLowerCase() !== email.toLowerCase());
|
|
166
|
+
if (data.users.length === before)
|
|
167
|
+
return false;
|
|
168
|
+
await this.save(data);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
// -------------------------------------------------------------------------
|
|
172
|
+
// Invite operations (C11)
|
|
173
|
+
// -------------------------------------------------------------------------
|
|
174
|
+
/**
|
|
175
|
+
* Creates an admin-generated invite token for the given email + role.
|
|
176
|
+
* The invite link is: `{basePath}/auth/invite/{token}` — displayed to the admin.
|
|
177
|
+
* Expiry: 48 hours.
|
|
178
|
+
*/
|
|
179
|
+
async createInvite(email, role) {
|
|
180
|
+
const data = await this.load();
|
|
181
|
+
// Invalidate any existing invites for this email
|
|
182
|
+
data.invites = data.invites.filter((i) => i.email.toLowerCase() !== email.toLowerCase());
|
|
183
|
+
const invite = {
|
|
184
|
+
token: randomBytes(32).toString('hex'),
|
|
185
|
+
email: email.toLowerCase(),
|
|
186
|
+
role,
|
|
187
|
+
expiresAt: Math.floor(Date.now() / 1000) + INVITE_TTL_SECONDS,
|
|
188
|
+
};
|
|
189
|
+
data.invites.push(invite);
|
|
190
|
+
await this.save(data);
|
|
191
|
+
return invite;
|
|
192
|
+
}
|
|
193
|
+
/** Returns all stored invites (including expired ones). */
|
|
194
|
+
async listInvites() {
|
|
195
|
+
const data = await this.load();
|
|
196
|
+
return [...data.invites];
|
|
197
|
+
}
|
|
198
|
+
/** Removes an invite by token. No-op if the token doesn't exist. */
|
|
199
|
+
async revokeInvite(token) {
|
|
200
|
+
const data = await this.load();
|
|
201
|
+
data.invites = data.invites.filter((i) => i.token !== token);
|
|
202
|
+
await this.save(data);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Accepts an invite: sets the user's password and creates the account.
|
|
206
|
+
* Returns the new user record, or throws if the token is invalid/expired.
|
|
207
|
+
*/
|
|
208
|
+
async acceptInvite(token, password, name) {
|
|
209
|
+
const data = await this.load();
|
|
210
|
+
const now = Math.floor(Date.now() / 1000);
|
|
211
|
+
const invite = data.invites.find((i) => i.token === token && i.expiresAt > now);
|
|
212
|
+
if (!invite)
|
|
213
|
+
throw new Error('Invalid or expired invite token');
|
|
214
|
+
// Remove the invite
|
|
215
|
+
data.invites = data.invites.filter((i) => i.token !== token);
|
|
216
|
+
await this.save(data);
|
|
217
|
+
return this.create(invite.email, password, invite.role, name);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=UserStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UserStore.js","sourceRoot":"","sources":["../src/UserStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blocklist.d.ts","sourceRoot":"","sources":["../src/blocklist.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,qBAAqB;eACrB,MAAM,GAAG,IAAI;eAGb,MAAM,GAAG,OAAO;IAG3B,sDAAsD;aAC7C,IAAI;CAGd,CAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory refresh token blocklist (C8).
|
|
3
|
+
*
|
|
4
|
+
* When a refresh token is rotated or a user logs out, the old token is added
|
|
5
|
+
* here. This prevents reuse of intercepted refresh tokens.
|
|
6
|
+
*
|
|
7
|
+
* Note: this list is cleared on server restart. If you need persistence across
|
|
8
|
+
* restarts, store revoked tokens in your database or cache layer instead.
|
|
9
|
+
*/
|
|
10
|
+
const blocked = new Set();
|
|
11
|
+
export const refreshTokenBlocklist = {
|
|
12
|
+
add(token) {
|
|
13
|
+
blocked.add(token);
|
|
14
|
+
},
|
|
15
|
+
has(token) {
|
|
16
|
+
return blocked.has(token);
|
|
17
|
+
},
|
|
18
|
+
/** For testing only — clears the entire blocklist. */
|
|
19
|
+
clear() {
|
|
20
|
+
blocked.clear();
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=blocklist.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blocklist.js","sourceRoot":"","sources":["../src/blocklist.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAA;AAEjC,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,GAAG,CAAC,KAAa;QACf,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;IACpB,CAAC;IACD,GAAG,CAAC,KAAa;QACf,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;IAC3B,CAAC;IACD,sDAAsD;IACtD,KAAK;QACH,OAAO,CAAC,KAAK,EAAE,CAAA;IACjB,CAAC;CACF,CAAA"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { withAuth, getRequestUser } from './withAuth.js';
|
|
2
|
+
export type { WithAuthOptions, AuthProvider, AuthUser, Role, RolePermissions, RouteHandlerDef } from './types.js';
|
|
3
|
+
export { ROLE_PERMISSIONS, can } from './types.js';
|
|
4
|
+
export { UserStore } from './UserStore.js';
|
|
5
|
+
export type { UserRecord, InviteRecord } from './UserStore.js';
|
|
6
|
+
export { CredentialsProvider } from './CredentialsProvider.js';
|
|
7
|
+
export type { CredentialsProviderOptions } from './CredentialsProvider.js';
|
|
8
|
+
export { GitHubOAuthProvider } from './GitHubOAuthProvider.js';
|
|
9
|
+
export type { GitHubOAuthProviderOptions } from './GitHubOAuthProvider.js';
|
|
10
|
+
export { GoogleOAuthProvider } from './GoogleOAuthProvider.js';
|
|
11
|
+
export type { GoogleOAuthProviderOptions } from './GoogleOAuthProvider.js';
|
|
12
|
+
export { refreshTokenBlocklist } from './blocklist.js';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AACxD,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AACjH,OAAO,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,YAAY,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAA;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,YAAY,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAA;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,YAAY,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAA;AAC1E,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { withAuth, getRequestUser } from './withAuth.js';
|
|
2
|
+
export { ROLE_PERMISSIONS, can } from './types.js';
|
|
3
|
+
export { UserStore } from './UserStore.js';
|
|
4
|
+
export { CredentialsProvider } from './CredentialsProvider.js';
|
|
5
|
+
export { GitHubOAuthProvider } from './GitHubOAuthProvider.js';
|
|
6
|
+
export { GoogleOAuthProvider } from './GoogleOAuthProvider.js';
|
|
7
|
+
export { refreshTokenBlocklist } from './blocklist.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAExD,OAAO,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE1C,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAE9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAE9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAE9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { PluginRouteContext } from '@airdraft/core';
|
|
2
|
+
import type { AuthUser, Role, RolePermissions } from '@airdraft/auth';
|
|
3
|
+
export type { AuthUser, Role, RolePermissions };
|
|
4
|
+
export { ROLE_PERMISSIONS, can, ACCESS_TOKEN_TTL, REFRESH_TOKEN_TTL } from '@airdraft/auth';
|
|
5
|
+
export interface RouteHandlerDef {
|
|
6
|
+
/** Route key used in plugin.routes (e.g. '/auth/login'). */
|
|
7
|
+
path: string;
|
|
8
|
+
handler: (req: Request, ctx: PluginRouteContext) => Promise<Response>;
|
|
9
|
+
/**
|
|
10
|
+
* When `true` (the default), this route is added to `publicPaths` so the
|
|
11
|
+
* auth middleware bypasses it — useful for login/logout/refresh endpoints.
|
|
12
|
+
* Set to `false` for routes that must go through the middleware so the
|
|
13
|
+
* verified user is available via `getRequestUser()`.
|
|
14
|
+
*/
|
|
15
|
+
public?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Pluggable authentication provider interface (C2).
|
|
19
|
+
*
|
|
20
|
+
* - `verify` is called on every protected request.
|
|
21
|
+
* - `handlers` returns additional CMS routes (login, logout, refresh, me).
|
|
22
|
+
* - `refresh` issues a new access token from a valid refresh token.
|
|
23
|
+
* - `getUser` is an optional cached alternative to `verify` for middleware-to-route handoff.
|
|
24
|
+
*/
|
|
25
|
+
export interface AuthProvider {
|
|
26
|
+
/** Verify the request and return the authenticated user, or null to deny. */
|
|
27
|
+
verify(req: Request): Promise<AuthUser | null>;
|
|
28
|
+
/** Additional route handlers to register under the CMS base path. */
|
|
29
|
+
handlers?(): RouteHandlerDef[];
|
|
30
|
+
/** Exchange a refresh token for a refreshed AuthUser. Returns null if invalid. */
|
|
31
|
+
refresh?(refreshToken: string): Promise<AuthUser | null>;
|
|
32
|
+
/** Optional: fast user lookup for already-verified requests (avoids re-verify). */
|
|
33
|
+
getUser?(req: Request): Promise<AuthUser | null>;
|
|
34
|
+
}
|
|
35
|
+
export interface WithAuthOptions {
|
|
36
|
+
provider: AuthProvider;
|
|
37
|
+
/**
|
|
38
|
+
* URL path suffixes that bypass auth entirely (e.g. '/openapi.json').
|
|
39
|
+
* The provider's own route paths are always bypassed automatically.
|
|
40
|
+
*/
|
|
41
|
+
publicPaths?: string[];
|
|
42
|
+
/**
|
|
43
|
+
* Email-keyed role overrides. Config takes precedence over the user store (C3).
|
|
44
|
+
* e.g. `{ 'alice@example.com': 'admin' }`
|
|
45
|
+
*/
|
|
46
|
+
roles?: Record<string, Role>;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACxD,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAGrE,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,eAAe,EAAE,CAAA;AAC/C,OAAO,EAAE,gBAAgB,EAAE,GAAG,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAM3F,MAAM,WAAW,eAAe;IAC9B,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,kBAAkB,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IACrE;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,6EAA6E;IAC7E,MAAM,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAA;IAC9C,qEAAqE;IACrE,QAAQ,CAAC,IAAI,eAAe,EAAE,CAAA;IAC9B,kFAAkF;IAClF,OAAO,CAAC,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAA;IACxD,mFAAmF;IACnF,OAAO,CAAC,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAA;CACjD;AAMD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,YAAY,CAAA;IACtB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;CAC7B"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,gBAAgB,EAAE,GAAG,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Plugin } from '@airdraft/core';
|
|
2
|
+
import type { AuthUser } from '@airdraft/auth';
|
|
3
|
+
import type { WithAuthOptions } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Returns the authenticated user attached to the request by `withAuth` middleware.
|
|
6
|
+
* Returns `undefined` if the request is unauthenticated or was a public route.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getRequestUser(req: Request): AuthUser | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Creates an Airdraft plugin that enforces authentication on all CMS routes
|
|
11
|
+
* using a pluggable `AuthProvider`.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { withAuth, CredentialsProvider, UserStore } from '@airdraft/plugin-auth'
|
|
15
|
+
*
|
|
16
|
+
* export default defineConfig({
|
|
17
|
+
* adapter,
|
|
18
|
+
* collections,
|
|
19
|
+
* plugins: [
|
|
20
|
+
* withAuth({
|
|
21
|
+
* provider: CredentialsProvider({
|
|
22
|
+
* userStore: UserStore.json(),
|
|
23
|
+
* secret: process.env.AIRDRAFT_JWT_SECRET!,
|
|
24
|
+
* roles: { 'alice@example.com': 'admin' },
|
|
25
|
+
* }),
|
|
26
|
+
* }),
|
|
27
|
+
* ],
|
|
28
|
+
* })
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function withAuth(options: WithAuthOptions): Plugin;
|
|
32
|
+
//# sourceMappingURL=withAuth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"withAuth.d.ts","sourceRoot":"","sources":["../src/withAuth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAE5C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAC9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAQjD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,QAAQ,GAAG,SAAS,CAEjE;AAMD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAmDzD"}
|
package/dist/withAuth.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { UnauthorizedError } from '@airdraft/core';
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Request → AuthUser map (allows downstream route handlers to read user)
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
const requestUsers = new WeakMap();
|
|
6
|
+
/**
|
|
7
|
+
* Returns the authenticated user attached to the request by `withAuth` middleware.
|
|
8
|
+
* Returns `undefined` if the request is unauthenticated or was a public route.
|
|
9
|
+
*/
|
|
10
|
+
export function getRequestUser(req) {
|
|
11
|
+
return requestUsers.get(req);
|
|
12
|
+
}
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// withAuth (C2, C3, C7)
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
/**
|
|
17
|
+
* Creates an Airdraft plugin that enforces authentication on all CMS routes
|
|
18
|
+
* using a pluggable `AuthProvider`.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* import { withAuth, CredentialsProvider, UserStore } from '@airdraft/plugin-auth'
|
|
22
|
+
*
|
|
23
|
+
* export default defineConfig({
|
|
24
|
+
* adapter,
|
|
25
|
+
* collections,
|
|
26
|
+
* plugins: [
|
|
27
|
+
* withAuth({
|
|
28
|
+
* provider: CredentialsProvider({
|
|
29
|
+
* userStore: UserStore.json(),
|
|
30
|
+
* secret: process.env.AIRDRAFT_JWT_SECRET!,
|
|
31
|
+
* roles: { 'alice@example.com': 'admin' },
|
|
32
|
+
* }),
|
|
33
|
+
* }),
|
|
34
|
+
* ],
|
|
35
|
+
* })
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export function withAuth(options) {
|
|
39
|
+
const { provider } = options;
|
|
40
|
+
// Collect route paths that should bypass auth.
|
|
41
|
+
// Routes with public !== false are bypassed (login, logout, refresh, invite acceptance).
|
|
42
|
+
// Routes with public === false go through middleware so getRequestUser() is populated.
|
|
43
|
+
const providerRoutes = (provider.handlers?.() ?? [])
|
|
44
|
+
.filter((h) => h.public !== false)
|
|
45
|
+
.map((h) => h.path);
|
|
46
|
+
const publicPaths = [...(options.publicPaths ?? []), ...providerRoutes];
|
|
47
|
+
/** Returns true if `pathname` matches a public path pattern (supports `:param` wildcards). */
|
|
48
|
+
function isPublicPath(pathname) {
|
|
49
|
+
return publicPaths.some((p) => {
|
|
50
|
+
if (!p.includes(':'))
|
|
51
|
+
return pathname.endsWith(p);
|
|
52
|
+
// Build a regex that replaces :param segments with a non-slash wildcard
|
|
53
|
+
const escaped = p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
54
|
+
const pattern = escaped.replace(/:[^/]+/g, '[^/]+');
|
|
55
|
+
return new RegExp(`${pattern}$`).test(pathname);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async function middleware(request) {
|
|
59
|
+
const { pathname } = new URL(request.url);
|
|
60
|
+
// Bypass auth for public and provider-owned paths
|
|
61
|
+
if (isPublicPath(pathname))
|
|
62
|
+
return;
|
|
63
|
+
// Verify via the provider
|
|
64
|
+
const user = await provider.verify(request);
|
|
65
|
+
if (!user)
|
|
66
|
+
throw new UnauthorizedError('Authentication required');
|
|
67
|
+
// Apply config-level role overrides (C3)
|
|
68
|
+
const role = options.roles?.[user.email.toLowerCase()] ?? user.role;
|
|
69
|
+
const enriched = { ...user, role };
|
|
70
|
+
// Attach to request for downstream consumers
|
|
71
|
+
requestUsers.set(request, enriched);
|
|
72
|
+
}
|
|
73
|
+
// Build the plugin routes map from provider handlers
|
|
74
|
+
const routes = {};
|
|
75
|
+
for (const { path, handler } of provider.handlers?.() ?? []) {
|
|
76
|
+
routes[path] = handler;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
name: 'auth',
|
|
80
|
+
routes: Object.keys(routes).length > 0 ? routes : undefined,
|
|
81
|
+
middleware,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=withAuth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"withAuth.js","sourceRoot":"","sources":["../src/withAuth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAIlD,8EAA8E;AAC9E,0EAA0E;AAC1E,8EAA8E;AAE9E,MAAM,YAAY,GAAG,IAAI,OAAO,EAAqB,CAAA;AAErD;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,GAAY;IACzC,OAAO,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;AAC9B,CAAC;AAED,8EAA8E;AAC9E,yBAAyB;AACzB,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAwB;IAC/C,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAA;IAE5B,+CAA+C;IAC/C,yFAAyF;IACzF,uFAAuF;IACvF,MAAM,cAAc,GAAG,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC;SACjD,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,KAAK,CAAC;SACjC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;IACrB,MAAM,WAAW,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,IAAI,EAAE,CAAC,EAAE,GAAG,cAAc,CAAC,CAAA;IAEvE,8FAA8F;IAC9F,SAAS,YAAY,CAAC,QAAgB;QACpC,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;YAC5B,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC;gBAAE,OAAO,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA;YACjD,wEAAwE;YACxE,MAAM,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAA;YACxD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;YACnD,OAAO,IAAI,MAAM,CAAC,GAAG,OAAO,GAAG,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACjD,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,KAAK,UAAU,UAAU,CAAC,OAAgB;QACxC,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAEzC,kDAAkD;QAClD,IAAI,YAAY,CAAC,QAAQ,CAAC;YAAE,OAAM;QAElC,0BAA0B;QAC1B,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QAC3C,IAAI,CAAC,IAAI;YAAE,MAAM,IAAI,iBAAiB,CAAC,yBAAyB,CAAC,CAAA;QAEjE,yCAAyC;QACzC,MAAM,IAAI,GAAI,OAAO,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAkC,IAAI,IAAI,CAAC,IAAI,CAAA;QACrG,MAAM,QAAQ,GAAa,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,CAAA;QAE5C,6CAA6C;QAC7C,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IACrC,CAAC;IAED,qDAAqD;IACrD,MAAM,MAAM,GAAqB,EAAE,CAAA;IACnC,KAAK,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,QAAQ,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC;QAC5D,MAAM,CAAC,IAAI,CAAC,GAAG,OAAO,CAAA;IACxB,CAAC;IAED,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS;QAC3D,UAAU;KACX,CAAA;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@airdraft/plugin-auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Airdraft auth plugin — GitHub OAuth, email/password providers",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": ["dist", "README.md", "CHANGELOG.md"],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"dev": "tsc --watch",
|
|
18
|
+
"clean": "rm -rf dist",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"prepublishOnly": "npm run build",
|
|
23
|
+
"release": "standard-version",
|
|
24
|
+
"release:patch": "standard-version --release-as patch",
|
|
25
|
+
"release:minor": "standard-version --release-as minor",
|
|
26
|
+
"release:major": "standard-version --release-as major"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": { "access": "public" },
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^20.0.0",
|
|
32
|
+
"standard-version": "^9.5.0",
|
|
33
|
+
"tsx": "^4.7.0",
|
|
34
|
+
"typescript": "^5.4.0",
|
|
35
|
+
"vitest": "^1.5.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@airdraft/core": "*",
|
|
39
|
+
"@airdraft/auth": "*"
|
|
40
|
+
},
|
|
41
|
+
"engines": { "node": ">=18.0.0" }
|
|
42
|
+
}
|