@emdash-cms/auth 0.0.1
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/adapters/kysely.d.mts +62 -0
- package/dist/adapters/kysely.d.mts.map +1 -0
- package/dist/adapters/kysely.mjs +379 -0
- package/dist/adapters/kysely.mjs.map +1 -0
- package/dist/authenticate-D5UgaoTH.d.mts +124 -0
- package/dist/authenticate-D5UgaoTH.d.mts.map +1 -0
- package/dist/authenticate-j5GayLXB.mjs +373 -0
- package/dist/authenticate-j5GayLXB.mjs.map +1 -0
- package/dist/index.d.mts +444 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +728 -0
- package/dist/index.mjs.map +1 -0
- package/dist/oauth/providers/github.d.mts +12 -0
- package/dist/oauth/providers/github.d.mts.map +1 -0
- package/dist/oauth/providers/github.mjs +55 -0
- package/dist/oauth/providers/github.mjs.map +1 -0
- package/dist/oauth/providers/google.d.mts +7 -0
- package/dist/oauth/providers/google.d.mts.map +1 -0
- package/dist/oauth/providers/google.mjs +38 -0
- package/dist/oauth/providers/google.mjs.map +1 -0
- package/dist/passkey/index.d.mts +2 -0
- package/dist/passkey/index.mjs +3 -0
- package/dist/types-Bu4irX9A.d.mts +35 -0
- package/dist/types-Bu4irX9A.d.mts.map +1 -0
- package/dist/types-CiSNpRI9.mjs +60 -0
- package/dist/types-CiSNpRI9.mjs.map +1 -0
- package/dist/types-HtRc90Wi.d.mts +208 -0
- package/dist/types-HtRc90Wi.d.mts.map +1 -0
- package/package.json +72 -0
- package/src/adapters/kysely.ts +715 -0
- package/src/config.ts +214 -0
- package/src/index.ts +135 -0
- package/src/invite.ts +205 -0
- package/src/magic-link/index.ts +150 -0
- package/src/oauth/consumer.ts +324 -0
- package/src/oauth/providers/github.ts +68 -0
- package/src/oauth/providers/google.ts +34 -0
- package/src/oauth/types.ts +36 -0
- package/src/passkey/authenticate.ts +183 -0
- package/src/passkey/index.ts +27 -0
- package/src/passkey/register.ts +232 -0
- package/src/passkey/types.ts +120 -0
- package/src/rbac.test.ts +141 -0
- package/src/rbac.ts +205 -0
- package/src/signup.ts +210 -0
- package/src/tokens.test.ts +141 -0
- package/src/tokens.ts +238 -0
- package/src/types.ts +352 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for @emdash-cms/auth
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Roles & Permissions
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
export const Role = {
|
|
10
|
+
SUBSCRIBER: 10,
|
|
11
|
+
CONTRIBUTOR: 20,
|
|
12
|
+
AUTHOR: 30,
|
|
13
|
+
EDITOR: 40,
|
|
14
|
+
ADMIN: 50,
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export type RoleLevel = (typeof Role)[keyof typeof Role];
|
|
18
|
+
export type RoleName = keyof typeof Role;
|
|
19
|
+
|
|
20
|
+
export function roleFromLevel(level: number): RoleName | undefined {
|
|
21
|
+
const entry = Object.entries(Role).find(([, v]) => v === level);
|
|
22
|
+
if (!entry) return undefined;
|
|
23
|
+
const name = entry[0];
|
|
24
|
+
if (isRoleName(name)) return name;
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isRoleName(value: string): value is RoleName {
|
|
29
|
+
return value in Role;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ROLE_LEVEL_MAP = new Map<number, RoleLevel>(Object.values(Role).map((v) => [v, v]));
|
|
33
|
+
|
|
34
|
+
export function toRoleLevel(value: number): RoleLevel {
|
|
35
|
+
const level = ROLE_LEVEL_MAP.get(value);
|
|
36
|
+
if (level !== undefined) return level;
|
|
37
|
+
throw new Error(`Invalid role level: ${value}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const DEVICE_TYPE_MAP: Record<string, DeviceType | undefined> = {
|
|
41
|
+
singleDevice: "singleDevice",
|
|
42
|
+
multiDevice: "multiDevice",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function toDeviceType(value: string): DeviceType {
|
|
46
|
+
const dt = DEVICE_TYPE_MAP[value];
|
|
47
|
+
if (dt !== undefined) return dt;
|
|
48
|
+
throw new Error(`Invalid device type: ${value}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const TOKEN_TYPE_MAP: Record<string, TokenType | undefined> = {
|
|
52
|
+
magic_link: "magic_link",
|
|
53
|
+
email_verify: "email_verify",
|
|
54
|
+
invite: "invite",
|
|
55
|
+
recovery: "recovery",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export function toTokenType(value: string): TokenType {
|
|
59
|
+
const tt = TOKEN_TYPE_MAP[value];
|
|
60
|
+
if (tt !== undefined) return tt;
|
|
61
|
+
throw new Error(`Invalid token type: ${value}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function roleToLevel(name: RoleName): RoleLevel {
|
|
65
|
+
return Role[name];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// User
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
export interface User {
|
|
73
|
+
id: string;
|
|
74
|
+
email: string;
|
|
75
|
+
name: string | null;
|
|
76
|
+
avatarUrl: string | null;
|
|
77
|
+
role: RoleLevel;
|
|
78
|
+
emailVerified: boolean;
|
|
79
|
+
disabled: boolean;
|
|
80
|
+
data: Record<string, unknown> | null;
|
|
81
|
+
createdAt: Date;
|
|
82
|
+
updatedAt: Date;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface NewUser {
|
|
86
|
+
email: string;
|
|
87
|
+
name?: string | null;
|
|
88
|
+
avatarUrl?: string | null;
|
|
89
|
+
role?: RoleLevel;
|
|
90
|
+
emailVerified?: boolean;
|
|
91
|
+
data?: Record<string, unknown> | null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface UpdateUser {
|
|
95
|
+
email?: string;
|
|
96
|
+
name?: string | null;
|
|
97
|
+
avatarUrl?: string | null;
|
|
98
|
+
role?: RoleLevel;
|
|
99
|
+
emailVerified?: boolean;
|
|
100
|
+
disabled?: boolean;
|
|
101
|
+
data?: Record<string, unknown> | null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// Credentials (Passkeys)
|
|
106
|
+
// ============================================================================
|
|
107
|
+
|
|
108
|
+
export type AuthenticatorTransport = "usb" | "nfc" | "ble" | "internal" | "hybrid";
|
|
109
|
+
export type DeviceType = "singleDevice" | "multiDevice";
|
|
110
|
+
|
|
111
|
+
export interface Credential {
|
|
112
|
+
id: string; // Base64url credential ID
|
|
113
|
+
userId: string;
|
|
114
|
+
publicKey: Uint8Array; // COSE public key
|
|
115
|
+
counter: number;
|
|
116
|
+
deviceType: DeviceType;
|
|
117
|
+
backedUp: boolean;
|
|
118
|
+
transports: AuthenticatorTransport[];
|
|
119
|
+
name: string | null;
|
|
120
|
+
createdAt: Date;
|
|
121
|
+
lastUsedAt: Date;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface NewCredential {
|
|
125
|
+
id: string;
|
|
126
|
+
userId: string;
|
|
127
|
+
publicKey: Uint8Array;
|
|
128
|
+
counter: number;
|
|
129
|
+
deviceType: DeviceType;
|
|
130
|
+
backedUp: boolean;
|
|
131
|
+
transports: AuthenticatorTransport[];
|
|
132
|
+
name?: string | null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// Sessions
|
|
137
|
+
// ============================================================================
|
|
138
|
+
|
|
139
|
+
export interface Session {
|
|
140
|
+
id: string;
|
|
141
|
+
userId: string;
|
|
142
|
+
expiresAt: Date;
|
|
143
|
+
ipAddress: string | null;
|
|
144
|
+
userAgent: string | null;
|
|
145
|
+
createdAt: Date;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface SessionData {
|
|
149
|
+
userId: string;
|
|
150
|
+
expiresAt: number; // Unix timestamp
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// Auth Tokens (magic links, invites, etc.)
|
|
155
|
+
// ============================================================================
|
|
156
|
+
|
|
157
|
+
export type TokenType = "magic_link" | "email_verify" | "invite" | "recovery";
|
|
158
|
+
|
|
159
|
+
export interface AuthToken {
|
|
160
|
+
hash: string; // SHA-256 hash of the raw token
|
|
161
|
+
userId: string | null; // null for pre-user tokens (invite/signup)
|
|
162
|
+
email: string | null; // For pre-user tokens
|
|
163
|
+
type: TokenType;
|
|
164
|
+
role: RoleLevel | null; // For invites
|
|
165
|
+
invitedBy: string | null;
|
|
166
|
+
expiresAt: Date;
|
|
167
|
+
createdAt: Date;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface NewAuthToken {
|
|
171
|
+
hash: string;
|
|
172
|
+
userId?: string | null;
|
|
173
|
+
email?: string | null;
|
|
174
|
+
type: TokenType;
|
|
175
|
+
role?: RoleLevel | null;
|
|
176
|
+
invitedBy?: string | null;
|
|
177
|
+
expiresAt: Date;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ============================================================================
|
|
181
|
+
// OAuth Accounts
|
|
182
|
+
// ============================================================================
|
|
183
|
+
|
|
184
|
+
export interface OAuthAccount {
|
|
185
|
+
provider: string;
|
|
186
|
+
providerAccountId: string;
|
|
187
|
+
userId: string;
|
|
188
|
+
createdAt: Date;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface NewOAuthAccount {
|
|
192
|
+
provider: string;
|
|
193
|
+
providerAccountId: string;
|
|
194
|
+
userId: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ============================================================================
|
|
198
|
+
// OAuth Connections (SSO config)
|
|
199
|
+
// ============================================================================
|
|
200
|
+
|
|
201
|
+
export interface OAuthConnection {
|
|
202
|
+
id: string;
|
|
203
|
+
name: string;
|
|
204
|
+
provider: "oidc" | "github" | "google";
|
|
205
|
+
clientId: string;
|
|
206
|
+
clientSecretEnc: string; // Encrypted
|
|
207
|
+
issuerUrl: string | null;
|
|
208
|
+
config: Record<string, unknown> | null;
|
|
209
|
+
enabled: boolean;
|
|
210
|
+
createdAt: Date;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ============================================================================
|
|
214
|
+
// OAuth Clients (when EmDash is provider)
|
|
215
|
+
// ============================================================================
|
|
216
|
+
|
|
217
|
+
export interface OAuthClient {
|
|
218
|
+
id: string;
|
|
219
|
+
name: string;
|
|
220
|
+
secretHash: string;
|
|
221
|
+
redirectUris: string[];
|
|
222
|
+
scopes: string[];
|
|
223
|
+
createdAt: Date;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ============================================================================
|
|
227
|
+
// Allowed Domains (self-signup)
|
|
228
|
+
// ============================================================================
|
|
229
|
+
|
|
230
|
+
export interface AllowedDomain {
|
|
231
|
+
domain: string;
|
|
232
|
+
defaultRole: RoleLevel;
|
|
233
|
+
enabled: boolean;
|
|
234
|
+
createdAt: Date;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ============================================================================
|
|
238
|
+
// User Listing Types (for admin UI)
|
|
239
|
+
// ============================================================================
|
|
240
|
+
|
|
241
|
+
/** Extended user with list view computed fields */
|
|
242
|
+
export interface UserListItem extends User {
|
|
243
|
+
lastLogin: Date | null;
|
|
244
|
+
credentialCount: number;
|
|
245
|
+
oauthProviders: string[];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** User with full details including related data */
|
|
249
|
+
export interface UserWithDetails {
|
|
250
|
+
user: User;
|
|
251
|
+
credentials: Credential[];
|
|
252
|
+
oauthAccounts: OAuthAccount[];
|
|
253
|
+
lastLogin: Date | null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ============================================================================
|
|
257
|
+
// Auth Adapter Interface
|
|
258
|
+
// ============================================================================
|
|
259
|
+
|
|
260
|
+
export interface AuthAdapter {
|
|
261
|
+
// Users
|
|
262
|
+
getUserById(id: string): Promise<User | null>;
|
|
263
|
+
getUserByEmail(email: string): Promise<User | null>;
|
|
264
|
+
createUser(user: NewUser): Promise<User>;
|
|
265
|
+
updateUser(id: string, data: UpdateUser): Promise<void>;
|
|
266
|
+
deleteUser(id: string): Promise<void>;
|
|
267
|
+
countUsers(): Promise<number>;
|
|
268
|
+
|
|
269
|
+
// User listing and details (for admin)
|
|
270
|
+
getUsers(options?: {
|
|
271
|
+
search?: string;
|
|
272
|
+
role?: number;
|
|
273
|
+
cursor?: string;
|
|
274
|
+
limit?: number;
|
|
275
|
+
}): Promise<{ items: UserListItem[]; nextCursor?: string }>;
|
|
276
|
+
getUserWithDetails(id: string): Promise<UserWithDetails | null>;
|
|
277
|
+
countAdmins(): Promise<number>;
|
|
278
|
+
|
|
279
|
+
// Credentials
|
|
280
|
+
getCredentialById(id: string): Promise<Credential | null>;
|
|
281
|
+
getCredentialsByUserId(userId: string): Promise<Credential[]>;
|
|
282
|
+
createCredential(credential: NewCredential): Promise<Credential>;
|
|
283
|
+
updateCredentialCounter(id: string, counter: number): Promise<void>;
|
|
284
|
+
updateCredentialName(id: string, name: string | null): Promise<void>;
|
|
285
|
+
deleteCredential(id: string): Promise<void>;
|
|
286
|
+
countCredentialsByUserId(userId: string): Promise<number>;
|
|
287
|
+
|
|
288
|
+
// Auth Tokens
|
|
289
|
+
createToken(token: NewAuthToken): Promise<void>;
|
|
290
|
+
getToken(hash: string, type: TokenType): Promise<AuthToken | null>;
|
|
291
|
+
deleteToken(hash: string): Promise<void>;
|
|
292
|
+
deleteExpiredTokens(): Promise<void>;
|
|
293
|
+
|
|
294
|
+
// OAuth Accounts
|
|
295
|
+
getOAuthAccount(provider: string, providerAccountId: string): Promise<OAuthAccount | null>;
|
|
296
|
+
getOAuthAccountsByUserId(userId: string): Promise<OAuthAccount[]>;
|
|
297
|
+
createOAuthAccount(account: NewOAuthAccount): Promise<OAuthAccount>;
|
|
298
|
+
deleteOAuthAccount(provider: string, providerAccountId: string): Promise<void>;
|
|
299
|
+
|
|
300
|
+
// Allowed Domains
|
|
301
|
+
getAllowedDomain(domain: string): Promise<AllowedDomain | null>;
|
|
302
|
+
getAllowedDomains(): Promise<AllowedDomain[]>;
|
|
303
|
+
createAllowedDomain(domain: string, defaultRole: RoleLevel): Promise<AllowedDomain>;
|
|
304
|
+
updateAllowedDomain(domain: string, enabled: boolean, defaultRole?: RoleLevel): Promise<void>;
|
|
305
|
+
deleteAllowedDomain(domain: string): Promise<void>;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ============================================================================
|
|
309
|
+
// Email Adapter Interface
|
|
310
|
+
// ============================================================================
|
|
311
|
+
|
|
312
|
+
export interface EmailMessage {
|
|
313
|
+
to: string;
|
|
314
|
+
subject: string;
|
|
315
|
+
text: string;
|
|
316
|
+
html?: string;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export interface EmailAdapter {
|
|
320
|
+
send(message: EmailMessage): Promise<void>;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ============================================================================
|
|
324
|
+
// Auth Errors
|
|
325
|
+
// ============================================================================
|
|
326
|
+
|
|
327
|
+
export class AuthError extends Error {
|
|
328
|
+
constructor(
|
|
329
|
+
public code: AuthErrorCode,
|
|
330
|
+
message?: string,
|
|
331
|
+
) {
|
|
332
|
+
super(message ?? code);
|
|
333
|
+
this.name = "AuthError";
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export type AuthErrorCode =
|
|
338
|
+
| "invalid_credentials"
|
|
339
|
+
| "invalid_token"
|
|
340
|
+
| "token_expired"
|
|
341
|
+
| "user_not_found"
|
|
342
|
+
| "user_exists"
|
|
343
|
+
| "credential_exists"
|
|
344
|
+
| "max_credentials"
|
|
345
|
+
| "email_not_verified"
|
|
346
|
+
| "signup_not_allowed"
|
|
347
|
+
| "domain_not_allowed"
|
|
348
|
+
| "forbidden"
|
|
349
|
+
| "unauthorized"
|
|
350
|
+
| "rate_limited"
|
|
351
|
+
| "invalid_request"
|
|
352
|
+
| "internal_error";
|