@withstudiocms/auth-kit 0.1.0-beta.1 → 0.1.0-beta.2
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/README.md +2 -0
- package/dist/config.d.ts +63 -0
- package/dist/config.js +48 -34
- package/dist/index.d.ts +12 -3
- package/dist/index.js +18 -9
- package/dist/utils/unsafeCheck.d.ts +1 -0
- package/dist/utils/user.js +1 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @withstudiocms/auth-kit
|
|
2
2
|
|
|
3
|
+
[](https://codecov.io/github/withstudiocms/studiocms)
|
|
4
|
+
|
|
3
5
|
Authentication Management utilities for StudioCMS
|
|
4
6
|
|
|
5
7
|
## Example Astro DB Tables
|
package/dist/config.d.ts
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
import { Brand, Context, Layer } from '@withstudiocms/effect';
|
|
2
2
|
import { ScryptConfigOptions } from '@withstudiocms/effect/scrypt';
|
|
3
3
|
import type { SessionConfig, UserTools } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for the password module.
|
|
6
|
+
*
|
|
7
|
+
* @property CMS_ENCRYPTION_KEY - The encryption key used for securing CMS passwords.
|
|
8
|
+
*/
|
|
9
|
+
export type PasswordModConfig = {
|
|
10
|
+
CMS_ENCRYPTION_KEY: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The finalized configuration object for the password module.
|
|
14
|
+
*
|
|
15
|
+
* @property CMS_ENCRYPTION_KEY - The encryption key used by the CMS for securing sensitive data.
|
|
16
|
+
* @property scrypt - Configuration options for the scrypt password hashing algorithm.
|
|
17
|
+
* @remarks
|
|
18
|
+
* This type is branded as 'PasswordModConfigFinal' to provide nominal typing and prevent accidental misuse.
|
|
19
|
+
*/
|
|
20
|
+
export type PasswordModConfigFinal = {
|
|
21
|
+
scrypt: ScryptConfigOptions;
|
|
22
|
+
} & Brand.Brand<'PasswordModConfigFinal'>;
|
|
23
|
+
/**
|
|
24
|
+
* A nominal type for the finalized password module configuration object.
|
|
25
|
+
*
|
|
26
|
+
* This constant uses the `Brand.nominal` utility to create a strongly-typed
|
|
27
|
+
* identifier for the `PasswordModConfigFinal` type, ensuring type safety and preventing
|
|
28
|
+
* accidental misuse of configuration objects.
|
|
29
|
+
*
|
|
30
|
+
* @see PasswordModConfigFinal
|
|
31
|
+
* @see Brand.nominal
|
|
32
|
+
*/
|
|
33
|
+
export declare const PasswordModConfigFinal: Brand.Brand.Constructor<PasswordModConfigFinal>;
|
|
4
34
|
/**
|
|
5
35
|
* Represents the raw configuration object for the Auth Kit.
|
|
6
36
|
*
|
|
@@ -40,6 +70,39 @@ export type AuthKitConfig = {
|
|
|
40
70
|
* @see Brand.nominal
|
|
41
71
|
*/
|
|
42
72
|
export declare const AuthKitConfig: Brand.Brand.Constructor<AuthKitConfig>;
|
|
73
|
+
/**
|
|
74
|
+
* Generates and validates the final password module configuration using the provided encryption key and optional scrypt parameters.
|
|
75
|
+
*
|
|
76
|
+
* @param config - An object containing the `CMS_ENCRYPTION_KEY` as a base64-encoded string.
|
|
77
|
+
* @returns The finalized password module configuration object.
|
|
78
|
+
*
|
|
79
|
+
* @throws {Error} If the `CMS_ENCRYPTION_KEY` is empty, not valid base64, or does not decode to exactly 16 bytes.
|
|
80
|
+
*
|
|
81
|
+
* @remarks
|
|
82
|
+
* - The `CMS_ENCRYPTION_KEY` must be a base64-encoded string representing 16 bytes (128 bits) for AES-128 encryption.
|
|
83
|
+
* - Scrypt parameters (`SCRYPT_N`, `SCRYPT_R`, `SCRYPT_P`) can be overridden via environment variables. They are clamped to safe ranges.
|
|
84
|
+
* - The function normalizes and validates the encryption key, then constructs the scrypt configuration for password hashing.
|
|
85
|
+
*/
|
|
86
|
+
export declare function makePasswordModConfig({ CMS_ENCRYPTION_KEY, }: PasswordModConfig): PasswordModConfigFinal;
|
|
87
|
+
declare const PasswordModOptions_base: Context.TagClass<PasswordModOptions, "PasswordModOptions", PasswordModConfigFinal>;
|
|
88
|
+
/**
|
|
89
|
+
* Represents the options for the Password Module, extending the Context.Tag utility.
|
|
90
|
+
*
|
|
91
|
+
* @template PasswordModOptions - The type of the options for the Password Module.
|
|
92
|
+
* @template PasswordModConfigFinal - The finalized configuration type for the Password Module.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* // Create a live layer with the provided configuration
|
|
96
|
+
* const layer = PasswordModOptions.Live({ CMS_ENCRYPTION_KEY: 'your-key' });
|
|
97
|
+
*
|
|
98
|
+
* @method
|
|
99
|
+
* @static
|
|
100
|
+
* @param {PasswordModConfig} config - The configuration object containing the CMS encryption key.
|
|
101
|
+
* @returns {Layer<PasswordModOptions, PasswordModConfigFinal>} A Layer instance with the provided configuration.
|
|
102
|
+
*/
|
|
103
|
+
export declare class PasswordModOptions extends PasswordModOptions_base {
|
|
104
|
+
static Live: ({ CMS_ENCRYPTION_KEY }: PasswordModConfig) => Layer.Layer<PasswordModOptions, never, never>;
|
|
105
|
+
}
|
|
43
106
|
declare const AuthKitOptions_base: Context.TagClass<AuthKitOptions, "AuthKitOptions", AuthKitConfig>;
|
|
44
107
|
/**
|
|
45
108
|
* Provides configuration options for the AuthKit module.
|
package/dist/config.js
CHANGED
|
@@ -1,7 +1,50 @@
|
|
|
1
1
|
import { Brand, Context, Layer } from "@withstudiocms/effect";
|
|
2
2
|
import { ScryptConfigOptions } from "@withstudiocms/effect/scrypt";
|
|
3
3
|
import { defaultSessionConfig } from "./utils/session.js";
|
|
4
|
+
const PasswordModConfigFinal = Brand.nominal();
|
|
4
5
|
const AuthKitConfig = Brand.nominal();
|
|
6
|
+
function makePasswordModConfig({
|
|
7
|
+
CMS_ENCRYPTION_KEY
|
|
8
|
+
}) {
|
|
9
|
+
const normalizedKey = CMS_ENCRYPTION_KEY.trim();
|
|
10
|
+
if (normalizedKey.length === 0) {
|
|
11
|
+
throw new Error("CMS_ENCRYPTION_KEY must be a non-empty base64 string");
|
|
12
|
+
}
|
|
13
|
+
let raw;
|
|
14
|
+
try {
|
|
15
|
+
raw = typeof Buffer !== "undefined" ? Buffer.from(normalizedKey, "base64") : new Uint8Array(
|
|
16
|
+
atob(normalizedKey).split("").map((c) => c.charCodeAt(0))
|
|
17
|
+
);
|
|
18
|
+
} catch {
|
|
19
|
+
throw new Error("CMS_ENCRYPTION_KEY is not valid base64");
|
|
20
|
+
}
|
|
21
|
+
if (raw.byteLength !== 16) {
|
|
22
|
+
throw new Error(`CMS_ENCRYPTION_KEY must decode to 16 bytes, got ${raw.byteLength}`);
|
|
23
|
+
}
|
|
24
|
+
const clamp = (v, min, max) => Number.isSafeInteger(v) ? Math.min(max, Math.max(min, v)) : min;
|
|
25
|
+
const env = (k) => typeof process !== "undefined" && process.env ? process.env[k] : void 0;
|
|
26
|
+
const parsedN = Number.parseInt(env("SCRYPT_N") ?? "", 10);
|
|
27
|
+
const parsedR = Number.parseInt(env("SCRYPT_R") ?? "", 10);
|
|
28
|
+
const parsedP = Number.parseInt(env("SCRYPT_P") ?? "", 10);
|
|
29
|
+
const toPowerOfTwo = (n) => 1 << Math.floor(Math.log2(n));
|
|
30
|
+
const baseN = clamp(parsedN, 16384, 1 << 20);
|
|
31
|
+
const SCRYPT_N = toPowerOfTwo(baseN);
|
|
32
|
+
const SCRYPT_R = clamp(parsedR, 8, 32);
|
|
33
|
+
const SCRYPT_P = clamp(parsedP, 1, 16);
|
|
34
|
+
const scrypt = ScryptConfigOptions({
|
|
35
|
+
encryptionKey: normalizedKey,
|
|
36
|
+
keylen: 64,
|
|
37
|
+
options: {
|
|
38
|
+
N: SCRYPT_N,
|
|
39
|
+
r: SCRYPT_R,
|
|
40
|
+
p: SCRYPT_P
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return PasswordModConfigFinal({ scrypt });
|
|
44
|
+
}
|
|
45
|
+
class PasswordModOptions extends Context.Tag("PasswordModOptions")() {
|
|
46
|
+
static Live = ({ CMS_ENCRYPTION_KEY }) => Layer.succeed(this, makePasswordModConfig({ CMS_ENCRYPTION_KEY }));
|
|
47
|
+
}
|
|
5
48
|
class AuthKitOptions extends Context.Tag("AuthKitOptions")() {
|
|
6
49
|
/**
|
|
7
50
|
* Creates a live instance of `AuthKitOptions` using the provided raw configuration.
|
|
@@ -18,39 +61,7 @@ class AuthKitOptions extends Context.Tag("AuthKitOptions")() {
|
|
|
18
61
|
* - The returned Layer can be used for dependency injection in the application.
|
|
19
62
|
*/
|
|
20
63
|
static Live = ({ CMS_ENCRYPTION_KEY, session: _session, userTools }) => {
|
|
21
|
-
const
|
|
22
|
-
if (normalizedKey.length === 0) {
|
|
23
|
-
throw new Error("CMS_ENCRYPTION_KEY must be a non-empty base64 string");
|
|
24
|
-
}
|
|
25
|
-
try {
|
|
26
|
-
const raw = typeof Buffer !== "undefined" ? Buffer.from(CMS_ENCRYPTION_KEY, "base64") : new Uint8Array(
|
|
27
|
-
atob(CMS_ENCRYPTION_KEY).split("").map((c) => c.charCodeAt(0))
|
|
28
|
-
);
|
|
29
|
-
if (raw.byteLength !== 16) {
|
|
30
|
-
throw new Error(`CMS_ENCRYPTION_KEY must decode to 16 bytes, got ${raw.byteLength}`);
|
|
31
|
-
}
|
|
32
|
-
} catch {
|
|
33
|
-
throw new Error("CMS_ENCRYPTION_KEY is not valid base64");
|
|
34
|
-
}
|
|
35
|
-
const clamp = (v, min, max) => Number.isSafeInteger(v) ? Math.min(max, Math.max(min, v)) : min;
|
|
36
|
-
const env = (k) => typeof process !== "undefined" && process.env ? process.env[k] : void 0;
|
|
37
|
-
const parsedN = Number.parseInt(env("SCRYPT_N") ?? "", 10);
|
|
38
|
-
const parsedR = Number.parseInt(env("SCRYPT_R") ?? "", 10);
|
|
39
|
-
const parsedP = Number.parseInt(env("SCRYPT_P") ?? "", 10);
|
|
40
|
-
const toPowerOfTwo = (n) => 1 << Math.floor(Math.log2(n));
|
|
41
|
-
const baseN = clamp(parsedN, 16384, 1 << 20);
|
|
42
|
-
const SCRYPT_N = toPowerOfTwo(baseN);
|
|
43
|
-
const SCRYPT_R = clamp(parsedR, 8, 32);
|
|
44
|
-
const SCRYPT_P = clamp(parsedP, 1, 16);
|
|
45
|
-
const scrypt = ScryptConfigOptions({
|
|
46
|
-
encryptionKey: normalizedKey,
|
|
47
|
-
keylen: 64,
|
|
48
|
-
options: {
|
|
49
|
-
N: SCRYPT_N,
|
|
50
|
-
r: SCRYPT_R,
|
|
51
|
-
p: SCRYPT_P
|
|
52
|
-
}
|
|
53
|
-
});
|
|
64
|
+
const { scrypt } = makePasswordModConfig({ CMS_ENCRYPTION_KEY });
|
|
54
65
|
const session = {
|
|
55
66
|
...defaultSessionConfig,
|
|
56
67
|
..._session ?? {}
|
|
@@ -69,5 +80,8 @@ class AuthKitOptions extends Context.Tag("AuthKitOptions")() {
|
|
|
69
80
|
}
|
|
70
81
|
export {
|
|
71
82
|
AuthKitConfig,
|
|
72
|
-
AuthKitOptions
|
|
83
|
+
AuthKitOptions,
|
|
84
|
+
PasswordModConfigFinal,
|
|
85
|
+
PasswordModOptions,
|
|
86
|
+
makePasswordModConfig
|
|
73
87
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { Effect } from '@withstudiocms/effect';
|
|
2
|
-
import { AuthKitOptions, type RawAuthKitConfig } from './config.js';
|
|
2
|
+
import { AuthKitOptions, PasswordModConfigFinal, type RawAuthKitConfig } from './config.js';
|
|
3
|
+
export { Password } from './modules/password.js';
|
|
4
|
+
/**
|
|
5
|
+
* Creates a scrypt password hashing utility using the provided scrypt configuration.
|
|
6
|
+
*
|
|
7
|
+
* @param scrypt - The scrypt configuration object from `PasswordModConfigFinal`.
|
|
8
|
+
* @returns An Effect that provides an object with a `run` method for performing scrypt operations.
|
|
9
|
+
*/
|
|
10
|
+
export declare const makeScrypt: (config: PasswordModConfigFinal) => Effect.Effect.AsEffect<Effect.Effect<Effect.Effect<{
|
|
11
|
+
run: (password: import("crypto").BinaryLike) => Effect.Effect<Buffer<ArrayBufferLike>, import("@withstudiocms/effect/scrypt").ScryptError, never>;
|
|
12
|
+
}, never, never>, Error, never>>;
|
|
3
13
|
declare const AuthKit_base: Effect.Service.Class<AuthKit, "@withstudiocms/AuthKit", {
|
|
4
14
|
readonly effect: Effect.Effect<{
|
|
5
15
|
readonly Encryption: Effect.Effect<{
|
|
@@ -74,7 +84,7 @@ declare const AuthKit_base: Effect.Service.Class<AuthKit, "@withstudiocms/AuthKi
|
|
|
74
84
|
readonly getUserPermissionLevel: (userData: import("./types.js").UserSessionData | import("./types.js").CombinedUserData | null) => Effect.Effect<import("./types.js").UserPermissionLevel, import("./errors.js").UserError, never>;
|
|
75
85
|
readonly isUserAllowed: (userData: import("./types.js").UserSessionData | import("./types.js").CombinedUserData | null, requiredPerms: "owner" | "admin" | "editor" | "visitor" | "unknown") => Effect.Effect<boolean, import("./errors.js").UserError, never>;
|
|
76
86
|
}, import("./errors.js").SessionError | import("./errors.js").UserError, never>;
|
|
77
|
-
},
|
|
87
|
+
}, Error, AuthKitOptions>;
|
|
78
88
|
}>;
|
|
79
89
|
/**
|
|
80
90
|
* The `AuthKit` service provides a collection of authentication utilities for use within the
|
|
@@ -126,4 +136,3 @@ export declare class AuthKit extends AuthKit_base {
|
|
|
126
136
|
*/
|
|
127
137
|
static makeConfig: (config: RawAuthKitConfig) => import("effect/Layer").Layer<AuthKitOptions, never, never>;
|
|
128
138
|
}
|
|
129
|
-
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,27 +1,34 @@
|
|
|
1
1
|
import { Effect } from "@withstudiocms/effect";
|
|
2
2
|
import { Scrypt as _Scrypt } from "@withstudiocms/effect/scrypt";
|
|
3
|
-
import { AuthKitOptions } from "./config.js";
|
|
3
|
+
import { AuthKitOptions, PasswordModConfigFinal } from "./config.js";
|
|
4
4
|
import { _Encryption, _Password, _Session, _User } from "./modules/index.js";
|
|
5
|
+
import { Password } from "./modules/password.js";
|
|
6
|
+
const makeScrypt = Effect.fn(
|
|
7
|
+
(config) => Effect.try({
|
|
8
|
+
try: () => Effect.gen(function* () {
|
|
9
|
+
const { run } = yield* _Scrypt;
|
|
10
|
+
return { run };
|
|
11
|
+
}).pipe(Effect.provide(_Scrypt.makeLive(config.scrypt))),
|
|
12
|
+
catch: (error) => new Error(`Failed to create Scrypt instance: ${error.message}`)
|
|
13
|
+
})
|
|
14
|
+
);
|
|
5
15
|
class AuthKit extends Effect.Service()("@withstudiocms/AuthKit", {
|
|
6
16
|
effect: Effect.gen(function* () {
|
|
7
17
|
const { CMS_ENCRYPTION_KEY, scrypt, session, userTools } = yield* AuthKitOptions;
|
|
8
|
-
const Scrypt = Effect.withSpan("@withstudiocms/AuthKit.Scrypt")(
|
|
9
|
-
|
|
10
|
-
const { run } = yield* _Scrypt;
|
|
11
|
-
return { run };
|
|
12
|
-
}).pipe(Effect.provide(_Scrypt.makeLive(scrypt)))
|
|
18
|
+
const Scrypt = yield* Effect.withSpan("@withstudiocms/AuthKit.Scrypt")(
|
|
19
|
+
makeScrypt(PasswordModConfigFinal({ scrypt }))
|
|
13
20
|
);
|
|
14
21
|
const Encryption = Effect.withSpan("@withstudiocms/AuthKit.Encryption")(
|
|
15
22
|
_Encryption(CMS_ENCRYPTION_KEY)
|
|
16
23
|
);
|
|
17
|
-
const
|
|
24
|
+
const Password2 = Effect.withSpan("@withstudiocms/AuthKit.Password")(_Password(Scrypt));
|
|
18
25
|
const Session = Effect.withSpan("@withstudiocms/AuthKit.Session")(_Session(session));
|
|
19
26
|
const User = Effect.withSpan("@withstudiocms/AuthKit.User")(
|
|
20
27
|
_User({ Scrypt, session, userTools })
|
|
21
28
|
);
|
|
22
29
|
return {
|
|
23
30
|
Encryption,
|
|
24
|
-
Password,
|
|
31
|
+
Password: Password2,
|
|
25
32
|
Session,
|
|
26
33
|
User
|
|
27
34
|
};
|
|
@@ -44,5 +51,7 @@ class AuthKit extends Effect.Service()("@withstudiocms/AuthKit", {
|
|
|
44
51
|
static makeConfig = (config) => AuthKitOptions.Live(config);
|
|
45
52
|
}
|
|
46
53
|
export {
|
|
47
|
-
AuthKit
|
|
54
|
+
AuthKit,
|
|
55
|
+
Password,
|
|
56
|
+
makeScrypt
|
|
48
57
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** biome-ignore-all lint/suspicious/noTsIgnore: Working with auto-generated modules */
|
|
1
2
|
import { Effect } from '@withstudiocms/effect';
|
|
2
3
|
declare const CheckIfUnsafe_base: Effect.Service.Class<CheckIfUnsafe, "studiocms/virtuals/auth/utils/unsafeCheck/CheckIfUnsafe", {
|
|
3
4
|
readonly effect: Effect.Effect<{
|
package/dist/utils/user.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@withstudiocms/auth-kit",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
4
4
|
"description": "Utilities for managing authentication",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "withstudiocms",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"@oslojs/binary": "^1.0.0",
|
|
60
60
|
"@oslojs/crypto": "^1.0.1",
|
|
61
61
|
"@oslojs/encoding": "^1.1.0",
|
|
62
|
-
"@withstudiocms/effect": "0.1.0-beta.
|
|
62
|
+
"@withstudiocms/effect": "0.1.0-beta.3"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@types/node": "^22.0.0"
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"update:lists": "pnpm update:username-list && pnpm update:password-list",
|
|
74
74
|
"build": "pnpm update:lists && pnpm buildkit build 'src/**/*.{ts,astro,css,json,png}'",
|
|
75
75
|
"dev": "pnpm update:lists && pnpm buildkit dev 'src/**/*.{ts,astro,css,json,png}'",
|
|
76
|
-
"test": "
|
|
76
|
+
"test": "vitest",
|
|
77
77
|
"typecheck": "tspc -p tsconfig.tspc.json"
|
|
78
78
|
}
|
|
79
79
|
}
|