@withstudiocms/auth-kit 0.1.0-beta.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.
@@ -0,0 +1,220 @@
1
+ import type { BinaryLike } from 'node:crypto';
2
+ import type { Effect } from '@withstudiocms/effect';
3
+ import type { ScryptError } from '@withstudiocms/effect/scrypt';
4
+ /**
5
+ * Mocked internal Scrypt type
6
+ */
7
+ export type IScrypt = Effect.Effect<{
8
+ run: (password: BinaryLike) => Effect.Effect<Buffer<ArrayBufferLike>, ScryptError, never>;
9
+ }, never, never>;
10
+ /**
11
+ * Represents a user session with expiration information.
12
+ *
13
+ * @property expiresAt - The date and time when the session expires.
14
+ * @property id - The unique identifier for the session.
15
+ * @property userId - The unique identifier of the user associated with the session.
16
+ */
17
+ export interface UserSession {
18
+ expiresAt: Date;
19
+ id: string;
20
+ userId: string;
21
+ }
22
+ /**
23
+ * Represents the user data structure for authentication and profile management.
24
+ *
25
+ * @property name - The full name of the user.
26
+ * @property id - The unique identifier for the user.
27
+ * @property url - The user's personal or profile URL, if available.
28
+ * @property email - The user's email address, or null if not set.
29
+ * @property avatar - The URL to the user's avatar image, or null if not set.
30
+ * @property username - The user's unique username.
31
+ * @property password - The user's password (hashed or plain), or null if not set.
32
+ * @property updatedAt - The date and time when the user was last updated, or null if not set.
33
+ * @property createdAt - The date and time when the user was created, or null if not set.
34
+ * @property emailVerified - Indicates whether the user's email has been verified.
35
+ * @property notifications - Notification settings or preferences for the user, or null if not set.
36
+ */
37
+ export interface UserData {
38
+ name: string;
39
+ username: string;
40
+ id?: string | undefined;
41
+ url?: string | null | undefined;
42
+ email?: string | null | undefined;
43
+ avatar?: string | null | undefined;
44
+ password?: string | null | undefined;
45
+ updatedAt?: Date | null | undefined;
46
+ createdAt?: Date | null | undefined;
47
+ emailVerified?: boolean | undefined;
48
+ notifications?: string | null | undefined;
49
+ }
50
+ /**
51
+ * Represents the data associated with an OAuth authentication event.
52
+ *
53
+ * @property userId - The unique identifier of the user in the local system.
54
+ * @property provider - The name of the OAuth provider (e.g., 'google', 'github').
55
+ * @property providerUserId - The unique identifier of the user as provided by the OAuth provider.
56
+ */
57
+ export interface OAuthData {
58
+ userId: string;
59
+ provider: string;
60
+ providerUserId: string;
61
+ }
62
+ /**
63
+ * An array of available permission ranks for users.
64
+ *
65
+ * The permission ranks are defined as a constant tuple and include the following values:
66
+ * - 'owner': The highest level of permission, typically the creator or primary administrator.
67
+ * - 'admin': A high level of permission, usually for users who manage the system.
68
+ * - 'editor': A mid-level permission, for users who can modify content.
69
+ * - 'visitor': A low-level permission, for users who can view content but not modify it.
70
+ * - 'unknown': A default or fallback permission rank for users with undefined roles.
71
+ */
72
+ export declare const availablePermissionRanks: readonly ["owner", "admin", "editor", "visitor", "unknown"];
73
+ /**
74
+ * Represents the available permission ranks for a user.
75
+ *
76
+ * This type is derived from the `availablePermissionRanks` array,
77
+ * and it includes all the possible values that a user can have as a permission rank.
78
+ */
79
+ export type AvailablePermissionRanks = (typeof availablePermissionRanks)[number];
80
+ /**
81
+ * Represents the permissions data for a user.
82
+ *
83
+ * @property user - The unique identifier of the user.
84
+ * @property rank - The permission rank assigned to the user.
85
+ */
86
+ export interface PermissionsData {
87
+ user: string;
88
+ rank: AvailablePermissionRanks;
89
+ }
90
+ type Present<T> = {
91
+ [K in keyof T]-?: Exclude<T[K], undefined>;
92
+ };
93
+ /**
94
+ * Represents the session data for a user.
95
+ *
96
+ * @property isLoggedIn - Indicates whether the user is currently logged in.
97
+ * @property user - The user data object, or `null` if no user is logged in.
98
+ * @property permissionLevel - The user's permission level, represented by an available permission rank.
99
+ */
100
+ export type UserSessionData = {
101
+ isLoggedIn: boolean;
102
+ user: Present<UserData> | null;
103
+ permissionLevel: AvailablePermissionRanks;
104
+ };
105
+ /**
106
+ * Represents a user object that combines base user data with optional OAuth and permissions data.
107
+ *
108
+ * @extends UserData
109
+ * @property {OAuthData[] | undefined} oAuthData - An array of OAuth provider data associated with the user, or undefined if not available.
110
+ * @property {PermissionsData | undefined} permissionsData - The permissions data for the user, or undefined if not available.
111
+ */
112
+ export interface CombinedUserData extends UserData {
113
+ oAuthData?: OAuthData[];
114
+ permissionsData?: PermissionsData;
115
+ }
116
+ /**
117
+ * Represents the combined session and user data.
118
+ *
119
+ * @property session - The current user's session information.
120
+ * @property user - The current user's profile and related data.
121
+ */
122
+ export interface SessionAndUserData {
123
+ session: UserSession;
124
+ user: Present<UserData>;
125
+ }
126
+ /**
127
+ * Represents the result of validating a session.
128
+ *
129
+ * This type is either a valid session and user data (`SessionAndUserData`),
130
+ * or an object indicating that there is no valid session or user.
131
+ *
132
+ * - If the session is valid, it returns `SessionAndUserData`.
133
+ * - If the session is invalid or not found, it returns an object with both `session` and `user` set to `null`.
134
+ */
135
+ export type SessionValidationResult = SessionAndUserData | {
136
+ session: null;
137
+ user: null;
138
+ };
139
+ /**
140
+ * Provides methods for managing user sessions.
141
+ *
142
+ * @interface SessionTools
143
+ */
144
+ export interface SessionTools {
145
+ createSession(params: UserSession): Promise<UserSession>;
146
+ sessionAndUserData(sessionId: string): Promise<SessionAndUserData[]>;
147
+ deleteSession(sessionId: string): Promise<void>;
148
+ updateSession(sessionId: string, data: UserSession): Promise<UserSession[]>;
149
+ }
150
+ /**
151
+ * Configuration options for managing user sessions.
152
+ *
153
+ * @property expTime - The expiration time for the session, in milliseconds.
154
+ * @property cookieName - The name of the cookie used to store the session identifier.
155
+ * @property sessionTools - Utilities or tools for session management.
156
+ */
157
+ export interface SessionConfig {
158
+ expTime: number;
159
+ cookieName: string;
160
+ sessionTools?: SessionTools;
161
+ }
162
+ /**
163
+ * Provides utility methods for user management and notification within the authentication system.
164
+ *
165
+ * @remarks
166
+ * This interface defines methods for creating, updating, and retrieving user data,
167
+ * as well as generating unique IDs and sending notifications to administrators.
168
+ */
169
+ export interface UserTools {
170
+ idGenerator(): string;
171
+ notifier?: {
172
+ admin(type: 'new_user', message: string): Promise<void>;
173
+ };
174
+ createLocalUser(data: UserData): Promise<Present<UserData>>;
175
+ createOAuthUser(data: {
176
+ provider: string;
177
+ providerUserId: string;
178
+ userId: string;
179
+ }): Promise<{
180
+ userId: string;
181
+ provider: string;
182
+ providerUserId: string;
183
+ }>;
184
+ updateLocalUser(id: string, data: Partial<UserData>): Promise<Present<UserData>>;
185
+ getUserById(id: string): Promise<CombinedUserData | undefined | null>;
186
+ getUserByEmail(email: string): Promise<CombinedUserData | undefined | null>;
187
+ getCurrentPermissions(userId: string): Promise<PermissionsData | undefined | null>;
188
+ }
189
+ /**
190
+ * Configuration options for user authentication and session management.
191
+ *
192
+ * @property Scrypt - The Scrypt configuration used for password hashing.
193
+ * @property session - The required session configuration object.
194
+ * @property userTools - Optional utilities or tools related to user management.
195
+ */
196
+ export interface UserConfig {
197
+ Scrypt: IScrypt;
198
+ session: Required<SessionConfig>;
199
+ userTools?: UserTools;
200
+ }
201
+ /**
202
+ * An enumeration representing different user permission levels.
203
+ *
204
+ * The permission levels are defined as follows:
205
+ * - visitor: 1
206
+ * - editor: 2
207
+ * - admin: 3
208
+ * - owner: 4
209
+ * - unknown: 0
210
+ */
211
+ export declare enum UserPermissionLevel {
212
+ visitor = 1,
213
+ editor = 2,
214
+ admin = 3,
215
+ owner = 4,
216
+ unknown = 0
217
+ }
218
+ export declare const rankToLevel: Record<AvailablePermissionRanks, UserPermissionLevel>;
219
+ export declare const levelToRank: Record<UserPermissionLevel, AvailablePermissionRanks>;
220
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1,29 @@
1
+ const availablePermissionRanks = ["owner", "admin", "editor", "visitor", "unknown"];
2
+ var UserPermissionLevel = /* @__PURE__ */ ((UserPermissionLevel2) => {
3
+ UserPermissionLevel2[UserPermissionLevel2["visitor"] = 1] = "visitor";
4
+ UserPermissionLevel2[UserPermissionLevel2["editor"] = 2] = "editor";
5
+ UserPermissionLevel2[UserPermissionLevel2["admin"] = 3] = "admin";
6
+ UserPermissionLevel2[UserPermissionLevel2["owner"] = 4] = "owner";
7
+ UserPermissionLevel2[UserPermissionLevel2["unknown"] = 0] = "unknown";
8
+ return UserPermissionLevel2;
9
+ })(UserPermissionLevel || {});
10
+ const rankToLevel = {
11
+ unknown: 0 /* unknown */,
12
+ visitor: 1 /* visitor */,
13
+ editor: 2 /* editor */,
14
+ admin: 3 /* admin */,
15
+ owner: 4 /* owner */
16
+ };
17
+ const levelToRank = {
18
+ [0 /* unknown */]: "unknown",
19
+ [1 /* visitor */]: "visitor",
20
+ [2 /* editor */]: "editor",
21
+ [3 /* admin */]: "admin",
22
+ [4 /* owner */]: "owner"
23
+ };
24
+ export {
25
+ UserPermissionLevel,
26
+ availablePermissionRanks,
27
+ levelToRank,
28
+ rankToLevel
29
+ };
@@ -0,0 +1,57 @@
1
+ interface SrvRecord {
2
+ priority: number;
3
+ weight: number;
4
+ port: number;
5
+ name: string;
6
+ }
7
+ interface AvatarOptions {
8
+ email?: string;
9
+ openid?: string;
10
+ https?: boolean;
11
+ [key: string]: any;
12
+ }
13
+ interface UserIdentity {
14
+ hash: string | null;
15
+ domain: string | null;
16
+ }
17
+ type TargetComponents = [string | null, number | null];
18
+ /**
19
+ * Return the right (target, port) pair from a list of SRV records.
20
+ */
21
+ export declare const srvHostname: (records: SrvRecord[]) => TargetComponents;
22
+ /**
23
+ * Ensure we are getting a (mostly) valid hostname and port number
24
+ * from the DNS resolver and return the final hostname:port string.
25
+ */
26
+ export declare const sanitizedTarget: (targetComponents: TargetComponents, https: boolean) => string | null;
27
+ /**
28
+ * Generate user hash based on the email address or OpenID and return
29
+ * it along with the relevant domain.
30
+ */
31
+ export declare const parseUserIdentity: (email?: string, openid?: string) => UserIdentity;
32
+ /**
33
+ * Return the DNS service to query for a given domain and scheme.
34
+ */
35
+ export declare const serviceName: (domain: string | null, https: boolean) => string | null;
36
+ /**
37
+ * Assemble the final avatar URL based on the provided components.
38
+ */
39
+ export declare const composeAvatarUrl: (delegationServer: string | null, avatarHash: string, queryString: string, https: boolean) => string;
40
+ /**
41
+ * Get the delegation server for a given domain.
42
+ */
43
+ export declare const getDelegationServer: (domain: string | null, https: boolean) => Promise<string | null>;
44
+ /**
45
+ * Generate an avatar URL for the given options.
46
+ */
47
+ export declare const getAvatarUrl: (options: AvatarOptions) => Promise<string>;
48
+ declare const _default: {
49
+ getAvatarUrl: (options: AvatarOptions) => Promise<string>;
50
+ sanitizedTarget: (targetComponents: TargetComponents, https: boolean) => string | null;
51
+ srvHostname: (records: SrvRecord[]) => TargetComponents;
52
+ parseUserIdentity: (email?: string, openid?: string) => UserIdentity;
53
+ serviceName: (domain: string | null, https: boolean) => string | null;
54
+ composeAvatarUrl: (delegationServer: string | null, avatarHash: string, queryString: string, https: boolean) => string;
55
+ getDelegationServer: (domain: string | null, https: boolean) => Promise<string | null>;
56
+ };
57
+ export default _default;
@@ -0,0 +1,157 @@
1
+ import { createHash } from "node:crypto";
2
+ import { resolveSrv } from "node:dns/promises";
3
+ import { stringify } from "node:querystring";
4
+ const BASE_URL = "http://cdn.libravatar.org/avatar/";
5
+ const SECURE_BASE_URL = "https://seccdn.libravatar.org/avatar/";
6
+ const SERVICE_BASE = "_avatars._tcp";
7
+ const SECURE_SERVICE_BASE = "_avatars-sec._tcp";
8
+ const srvHostname = (records) => {
9
+ if (records.length < 1) {
10
+ return [null, null];
11
+ }
12
+ if (records.length === 1) {
13
+ return [records[0].name, records[0].port];
14
+ }
15
+ let priorityRecords = [];
16
+ let totalWeight = 0;
17
+ let topPriority = records[0].priority;
18
+ records.forEach((srvRecord) => {
19
+ if (srvRecord.priority <= topPriority) {
20
+ if (srvRecord.priority < topPriority) {
21
+ topPriority = srvRecord.priority;
22
+ totalWeight = 0;
23
+ priorityRecords = [];
24
+ }
25
+ totalWeight += srvRecord.weight;
26
+ if (srvRecord.weight > 0) {
27
+ priorityRecords.push([totalWeight, srvRecord]);
28
+ } else {
29
+ priorityRecords.unshift([0, srvRecord]);
30
+ }
31
+ }
32
+ });
33
+ if (priorityRecords.length === 1) {
34
+ const srvRecord = priorityRecords[0][1];
35
+ return [srvRecord.name, srvRecord.port];
36
+ }
37
+ const randomNumber = Math.floor(Math.random() * (totalWeight + 1));
38
+ for (let i = 0; i < priorityRecords.length; i++) {
39
+ const weightedIndex = priorityRecords[i][0];
40
+ const target = priorityRecords[i][1];
41
+ if (weightedIndex >= randomNumber) {
42
+ return [target.name, target.port];
43
+ }
44
+ }
45
+ console.log("There is something wrong with our SRV weight ordering algorithm");
46
+ return [null, null];
47
+ };
48
+ const sanitizedTarget = (targetComponents, https) => {
49
+ const target = targetComponents[0];
50
+ const port = targetComponents[1];
51
+ if (target === null || port === null || Number.isNaN(port)) {
52
+ return null;
53
+ }
54
+ if (port < 1 || port > 65535) {
55
+ return null;
56
+ }
57
+ if (target.search(/^[0-9a-zA-Z\-.]+$/) === -1) {
58
+ return null;
59
+ }
60
+ if (target && (https && port !== 443 || !https && port !== 80)) {
61
+ return `${target}:${port}`;
62
+ }
63
+ return target;
64
+ };
65
+ const parseUserIdentity = (email, openid) => {
66
+ let hash = null;
67
+ let domain = null;
68
+ if (email != null) {
69
+ const lowercaseValue = email.trim().toLowerCase();
70
+ const emailParts = lowercaseValue.split("@");
71
+ if (emailParts.length > 1) {
72
+ domain = emailParts[emailParts.length - 1];
73
+ hash = createHash("md5").update(lowercaseValue).digest("hex");
74
+ }
75
+ } else if (openid != null) {
76
+ try {
77
+ const parsedUrl = new URL(openid);
78
+ let normalizedUrl = parsedUrl.protocol.toLowerCase();
79
+ normalizedUrl += "//";
80
+ if (parsedUrl.username || parsedUrl.password) {
81
+ const auth = parsedUrl.username + (parsedUrl.password ? `:${parsedUrl.password}` : "");
82
+ normalizedUrl += `${auth}@`;
83
+ }
84
+ normalizedUrl += parsedUrl.hostname.toLowerCase() + parsedUrl.pathname;
85
+ domain = parsedUrl.hostname.toLowerCase();
86
+ hash = createHash("sha256").update(normalizedUrl).digest("hex");
87
+ } catch (_error) {
88
+ return { hash: null, domain: null };
89
+ }
90
+ }
91
+ return { hash, domain };
92
+ };
93
+ const serviceName = (domain, https) => {
94
+ if (domain) {
95
+ return `${https ? SECURE_SERVICE_BASE : SERVICE_BASE}.${domain}`;
96
+ }
97
+ return null;
98
+ };
99
+ const composeAvatarUrl = (delegationServer, avatarHash, queryString, https) => {
100
+ let baseUrl = https ? SECURE_BASE_URL : BASE_URL;
101
+ if (delegationServer) {
102
+ baseUrl = `http${https ? "s" : ""}://${delegationServer}/avatar/`;
103
+ }
104
+ return baseUrl + avatarHash + queryString;
105
+ };
106
+ const getDelegationServer = async (domain, https) => {
107
+ if (!domain) {
108
+ return null;
109
+ }
110
+ const service = serviceName(domain, https);
111
+ if (!service) {
112
+ return null;
113
+ }
114
+ try {
115
+ const addresses = await resolveSrv(service);
116
+ return sanitizedTarget(srvHostname(addresses), https);
117
+ } catch (_error) {
118
+ return null;
119
+ }
120
+ };
121
+ const getAvatarUrl = async (options) => {
122
+ const identity = parseUserIdentity(options.email, options.openid);
123
+ const hash = identity.hash;
124
+ const domain = identity.domain;
125
+ const https = options.https || false;
126
+ if (!hash) {
127
+ throw new Error("An email or an OpenID must be provided.");
128
+ }
129
+ const queryOptions = { ...options };
130
+ delete queryOptions.email;
131
+ delete queryOptions.openid;
132
+ delete queryOptions.https;
133
+ const queryData = stringify(queryOptions);
134
+ const query = queryData ? `?${queryData}` : "";
135
+ const delegationServer = await getDelegationServer(domain, https);
136
+ return composeAvatarUrl(delegationServer, hash, query, https);
137
+ };
138
+ var libravatar_default = {
139
+ getAvatarUrl,
140
+ // Export utility functions for testing
141
+ sanitizedTarget,
142
+ srvHostname,
143
+ parseUserIdentity,
144
+ serviceName,
145
+ composeAvatarUrl,
146
+ getDelegationServer
147
+ };
148
+ export {
149
+ composeAvatarUrl,
150
+ libravatar_default as default,
151
+ getAvatarUrl,
152
+ getDelegationServer,
153
+ parseUserIdentity,
154
+ sanitizedTarget,
155
+ serviceName,
156
+ srvHostname
157
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Set of common passwords that should be blocked for security reasons.
3
+ * This list contains the most commonly used passwords from various data breaches.
4
+ *
5
+ * Usage:
6
+ * import { blockedPasswords } from './passwords';
7
+ *
8
+ * const isPasswordBlocked = blockedPasswords.has(userPassword.toLowerCase());
9
+ */
10
+ export declare const passwords: Set<string>;
11
+ /**
12
+ * Check if a password is in the blocked list (case-insensitive)
13
+ * @param pass - The password to check
14
+ * @returns true if the password is blocked, false otherwise
15
+ */
16
+ export declare const isReservedPassword: (pass: string) => boolean;
17
+ export default passwords;