astro-altcha 0.4.0 → 0.5.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/README.md +8 -6
- package/dist/api/altcha-challenge.d.ts +1 -1
- package/dist/api/altcha-middleware.d.ts +1 -24
- package/dist/api/altcha-middleware.js +1 -20
- package/dist/api/altcha-verify.d.ts +4 -0
- package/dist/api/altcha-verify.js +23 -0
- package/dist/api/payload-schema.d.ts +22 -0
- package/dist/api/payload-schema.js +25 -0
- package/dist/api/verify.d.ts +1 -0
- package/dist/api/verify.js +12 -0
- package/dist/integration.d.ts +11 -5
- package/dist/integration.js +22 -7
- package/dist/types/index.d.ts +16 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
# `astro-altcha`
|
|
2
2
|
|
|
3
|
-

|
|
3
|
+

|
|
4
4
|

|
|
5
5
|

|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
> **Warning** Early development. Expect bugs and breaking changes.
|
|
9
|
-
|
|
10
8
|
This is an [Astro integration](https://docs.astro.build/en/guides/integrations-guide/) that helps setting up altcha for Astro. It includes some optimizations and convenience features that should make it really easy to use altcha in your application. The main features are:
|
|
11
9
|
|
|
12
10
|
- automatic middleware and challenge endpoint creation
|
|
@@ -67,16 +65,20 @@ export default defineConfig({
|
|
|
67
65
|
|
|
68
66
|
### Configuration
|
|
69
67
|
|
|
68
|
+
All configuration options are optional. Default values are documented in this README and in the Typescript declarations for each option.
|
|
69
|
+
|
|
70
70
|
#### Integration Options
|
|
71
71
|
|
|
72
72
|
- `defaultName`: the global default name of the `altcha-widget` or the name of the cookie when using cookie mode. Must contain the string "astro-altcha" (this is so that the middleware can find the challenge solution). Defaults to `astro-altcha`
|
|
73
73
|
- `challengePath`: the path under which the challenge creation API is made available. Defaults to `/api/altcha/challenge`
|
|
74
|
-
- `challengeOptions`: can be used to change challange algorithm and parameters. See [challenge creation](https://altcha.org/docs/integration/server/#challenge-generation) for more info
|
|
74
|
+
- `challengeOptions`: can be used to change challange algorithm and parameters. See [challenge creation](https://altcha.org/docs/integration/server/#challenge-generation) for more info. Defaults to `{ name: "astro-altcha", maxAge: 1200, path: "/", sameSite: "lax", secure: command !== "dev" }`
|
|
75
|
+
- `verifyPath`: the path under wich the challenge verification API is made available. Defaults to `/api/altcha/verify`
|
|
75
76
|
- `challengeExpiresAfterMinutes`: time in minutes for a challenge to remain valid. Defaults to 20m
|
|
76
77
|
- `setCookie`: set a cookie instead of using an invisible form field in `AltchaForm`. Can be set to `true` to use default options: `{ name: "astro-altcha", maxAge: 1200, path: "/", sameSite: "lax", secure: command !== "dev" }`
|
|
77
|
-
- `storage`: custom path for a storage module. See [Customize Storage](#customize-storage)
|
|
78
|
+
- `storage`: custom path for a storage module. See [Customize Storage](#customize-storage). By default a `CappedMap` with `maxSize: 499` is used.
|
|
79
|
+
- `features`: enable or disable middleware and endpoint injection. By default middleware and challenge endpoints are injected.
|
|
78
80
|
|
|
79
|
-
#### Customize
|
|
81
|
+
#### Customize Storage
|
|
80
82
|
|
|
81
83
|
`astro-altcha` prevents replay attacks by always configuring a storage to remember already issued challenges. By default it uses `altcha-lib`'s `CappedMap`, but this can be changed by creating a `.ts/.js` file which default exports a custom storage implementation in `src/astro-altcha/storage.ts`. This path can be customized via the `storage` global option. For typings the library provides a `defineStorage` helper function.
|
|
82
84
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { Algorithm, ResolvedAstroAltchaConfig } from "../types/index.js";
|
|
2
|
-
import { DeriveKeyFunction } from "altcha/
|
|
2
|
+
import { DeriveKeyFunction } from "altcha/types";
|
|
3
3
|
import { APIRoute } from "astro";
|
|
4
4
|
export declare function createChallengeHandler(hmacSecret: string, { challengeOptions, challengeExpiresAfterMinutes, setCookie }: ResolvedAstroAltchaConfig<Algorithm>, deriveKeyFunc: DeriveKeyFunction): APIRoute;
|
|
@@ -1,28 +1,5 @@
|
|
|
1
1
|
import { Algorithm, ResolvedAstroAltchaConfig } from "../types/index.js";
|
|
2
2
|
import { DeriveKeyFunction } from "altcha-lib";
|
|
3
|
-
import { z } from "astro/zod";
|
|
4
3
|
import { MiddlewareHandler } from "astro";
|
|
5
4
|
import { Store } from "altcha-lib/frameworks/types";
|
|
6
|
-
declare
|
|
7
|
-
challenge: z.ZodObject<{
|
|
8
|
-
parameters: z.ZodObject<{
|
|
9
|
-
algorithm: z.ZodString;
|
|
10
|
-
cost: z.ZodNumber;
|
|
11
|
-
keyLength: z.ZodNumber;
|
|
12
|
-
keyPrefix: z.ZodString;
|
|
13
|
-
keySignature: z.ZodOptional<z.ZodString>;
|
|
14
|
-
nonce: z.ZodString;
|
|
15
|
-
salt: z.ZodString;
|
|
16
|
-
expiresAt: z.ZodOptional<z.ZodInt>;
|
|
17
|
-
}, z.core.$loose>;
|
|
18
|
-
signature: z.ZodString;
|
|
19
|
-
}, z.core.$loose>;
|
|
20
|
-
solution: z.ZodObject<{
|
|
21
|
-
counter: z.ZodNumber;
|
|
22
|
-
derivedKey: z.ZodString;
|
|
23
|
-
time: z.ZodNumber;
|
|
24
|
-
}, z.core.$loose>;
|
|
25
|
-
}, z.core.$loose>;
|
|
26
|
-
export type AltchaPayload = z.infer<typeof altchaPayloadSchema>;
|
|
27
|
-
export declare function createAltchaMiddlewareHandler(hmacSecret: string, deriveKeyFunc: DeriveKeyFunction, { setCookie }: ResolvedAstroAltchaConfig<Algorithm>, store: Store): MiddlewareHandler;
|
|
28
|
-
export {};
|
|
5
|
+
export declare function createAltchaMiddlewareHandler(hmacSecret: string, deriveKeyFunc: DeriveKeyFunction, { setCookie }: ResolvedAstroAltchaConfig<Algorithm>, store: Store): MiddlewareHandler;
|
|
@@ -1,28 +1,9 @@
|
|
|
1
|
+
import { altchaPayloadSchema } from "./payload-schema.js";
|
|
1
2
|
import { deriveHmacKeySecret, verify } from "altcha-lib/frameworks/shared";
|
|
2
3
|
import { z } from "astro/zod";
|
|
3
4
|
import { base64ToString } from "uint8array-extras";
|
|
4
5
|
|
|
5
6
|
// src/api/altcha-middleware.ts
|
|
6
|
-
const altchaPayloadSchema = z.looseObject({
|
|
7
|
-
challenge: z.looseObject({
|
|
8
|
-
parameters: z.looseObject({
|
|
9
|
-
algorithm: z.string(),
|
|
10
|
-
cost: z.number(),
|
|
11
|
-
keyLength: z.number(),
|
|
12
|
-
keyPrefix: z.string(),
|
|
13
|
-
keySignature: z.string().optional(),
|
|
14
|
-
nonce: z.string(),
|
|
15
|
-
salt: z.string(),
|
|
16
|
-
expiresAt: z.int().optional()
|
|
17
|
-
}),
|
|
18
|
-
signature: z.string()
|
|
19
|
-
}),
|
|
20
|
-
solution: z.looseObject({
|
|
21
|
-
counter: z.number(),
|
|
22
|
-
derivedKey: z.string(),
|
|
23
|
-
time: z.number()
|
|
24
|
-
})
|
|
25
|
-
});
|
|
26
7
|
const keyRegexp = /.*astro-altcha.*/;
|
|
27
8
|
function createAltchaMiddlewareHandler(hmacSecret, deriveKeyFunc, { setCookie }, store) {
|
|
28
9
|
return async (context, next) => {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { altchaPayloadSchema } from "./payload-schema.js";
|
|
2
|
+
import { deriveHmacKeySecret, verify } from "altcha-lib/frameworks/shared";
|
|
3
|
+
import { z } from "astro/zod";
|
|
4
|
+
|
|
5
|
+
// src/api/altcha-verify.ts
|
|
6
|
+
function createVerifyHandler(hmacSecret, deriveKeyFunc, store) {
|
|
7
|
+
return async (context) => {
|
|
8
|
+
if (context.cache.enabled) context.cache.set(false);
|
|
9
|
+
const { success, data: payload, error } = altchaPayloadSchema.safeParse(await context.request.json());
|
|
10
|
+
if (!success) {
|
|
11
|
+
const e = `Error parsing altcha payload: ${z.prettifyError(error)}`;
|
|
12
|
+
context.logger?.error(e);
|
|
13
|
+
return new Response(e, {
|
|
14
|
+
status: 400,
|
|
15
|
+
statusText: "Bad Request"
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const result = await verify(payload, deriveKeyFunc, hmacSecret, await deriveHmacKeySecret(hmacSecret), store);
|
|
19
|
+
return Response.json(result, { headers: { "Cache-Control": "no-store" } });
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { createVerifyHandler };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "astro/zod";
|
|
2
|
+
export declare const altchaPayloadSchema: z.ZodObject<{
|
|
3
|
+
challenge: z.ZodObject<{
|
|
4
|
+
parameters: z.ZodObject<{
|
|
5
|
+
algorithm: z.ZodString;
|
|
6
|
+
cost: z.ZodNumber;
|
|
7
|
+
keyLength: z.ZodNumber;
|
|
8
|
+
keyPrefix: z.ZodString;
|
|
9
|
+
keySignature: z.ZodOptional<z.ZodString>;
|
|
10
|
+
nonce: z.ZodString;
|
|
11
|
+
salt: z.ZodString;
|
|
12
|
+
expiresAt: z.ZodOptional<z.ZodInt>;
|
|
13
|
+
}, z.core.$loose>;
|
|
14
|
+
signature: z.ZodString;
|
|
15
|
+
}, z.core.$loose>;
|
|
16
|
+
solution: z.ZodObject<{
|
|
17
|
+
counter: z.ZodNumber;
|
|
18
|
+
derivedKey: z.ZodString;
|
|
19
|
+
time: z.ZodNumber;
|
|
20
|
+
}, z.core.$loose>;
|
|
21
|
+
}, z.core.$loose>;
|
|
22
|
+
export type AltchaPayload = z.infer<typeof altchaPayloadSchema>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "astro/zod";
|
|
2
|
+
|
|
3
|
+
// src/api/payload-schema.ts
|
|
4
|
+
const altchaPayloadSchema = z.looseObject({
|
|
5
|
+
challenge: z.looseObject({
|
|
6
|
+
parameters: z.looseObject({
|
|
7
|
+
algorithm: z.string(),
|
|
8
|
+
cost: z.number(),
|
|
9
|
+
keyLength: z.number(),
|
|
10
|
+
keyPrefix: z.string(),
|
|
11
|
+
keySignature: z.string().optional(),
|
|
12
|
+
nonce: z.string(),
|
|
13
|
+
salt: z.string(),
|
|
14
|
+
expiresAt: z.int().optional()
|
|
15
|
+
}),
|
|
16
|
+
signature: z.string()
|
|
17
|
+
}),
|
|
18
|
+
solution: z.looseObject({
|
|
19
|
+
counter: z.number(),
|
|
20
|
+
derivedKey: z.string(),
|
|
21
|
+
time: z.number()
|
|
22
|
+
})
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export { altchaPayloadSchema };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const POST: import("astro").APIRoute;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createVerifyHandler } from "./altcha-verify.js";
|
|
2
|
+
import { pickDeriveKeyFunc } from "./pickDeriveKey.js";
|
|
3
|
+
import { getSecret } from "astro:env/server";
|
|
4
|
+
import { config } from "astro-altcha:config";
|
|
5
|
+
import store from "astro-altcha:storage";
|
|
6
|
+
|
|
7
|
+
// src/api/verify.ts
|
|
8
|
+
const ALTCHA_HMAC_SIGNATURE = getSecret("ALTCHA_HMAC_SIGNATURE");
|
|
9
|
+
if (!ALTCHA_HMAC_SIGNATURE) throw new Error("ALTCHA_HMAC_SIGNATURE not set");
|
|
10
|
+
const POST = createVerifyHandler(ALTCHA_HMAC_SIGNATURE, await pickDeriveKeyFunc(config.challengeOptions.algorithm), store);
|
|
11
|
+
|
|
12
|
+
export { POST };
|
package/dist/integration.d.ts
CHANGED
|
@@ -2,12 +2,17 @@ import { Algorithm, AstroAltchaConfig } from "./types/index.js";
|
|
|
2
2
|
import { AstroIntegration } from "astro";
|
|
3
3
|
export declare function integration(options?: AstroAltchaConfig): AstroIntegration;
|
|
4
4
|
export declare function mergeDefaultConfig(command: "dev" | "build" | "preview" | "sync", options?: AstroAltchaConfig): {
|
|
5
|
+
readonly features: {
|
|
6
|
+
middleware: boolean;
|
|
7
|
+
challengeEndpoint: boolean;
|
|
8
|
+
verifyEndpoint: boolean;
|
|
9
|
+
};
|
|
5
10
|
readonly setCookie?: {
|
|
6
11
|
domain?: string;
|
|
7
12
|
name: string;
|
|
8
13
|
maxAge: number;
|
|
9
14
|
path: string;
|
|
10
|
-
sameSite: "
|
|
15
|
+
sameSite: "none" | "strict" | "lax";
|
|
11
16
|
secure: boolean;
|
|
12
17
|
};
|
|
13
18
|
readonly challengeOptions: {
|
|
@@ -16,29 +21,30 @@ export declare function mergeDefaultConfig(command: "dev" | "build" | "preview"
|
|
|
16
21
|
readonly counter: number;
|
|
17
22
|
} | {
|
|
18
23
|
readonly algorithm: Exclude<Algorithm, "Argon2id" | "Scrypt">;
|
|
24
|
+
readonly keyLength?: number;
|
|
25
|
+
readonly keyPrefix?: string;
|
|
19
26
|
counter: number;
|
|
20
27
|
readonly counterMode?: "uint32" | "string";
|
|
21
28
|
readonly data?: Record<string, string | number | boolean | null>;
|
|
22
29
|
readonly hmacAlgorithm?: import("altcha-lib").HmacAlgorithm;
|
|
23
|
-
readonly keyLength?: number;
|
|
24
|
-
readonly keyPrefix?: string;
|
|
25
30
|
readonly keyPrefixLength?: number;
|
|
26
31
|
readonly memoryCost?: number;
|
|
27
32
|
readonly parallelism?: number;
|
|
28
33
|
cost: number;
|
|
29
34
|
} | {
|
|
30
35
|
readonly algorithm: "Argon2id" | "Scrypt";
|
|
36
|
+
readonly keyLength?: number;
|
|
37
|
+
readonly keyPrefix?: string;
|
|
31
38
|
counter: number;
|
|
32
39
|
readonly counterMode?: "uint32" | "string";
|
|
33
40
|
readonly data?: Record<string, string | number | boolean | null>;
|
|
34
41
|
readonly hmacAlgorithm?: import("altcha-lib").HmacAlgorithm;
|
|
35
|
-
readonly keyLength?: number;
|
|
36
|
-
readonly keyPrefix?: string;
|
|
37
42
|
readonly keyPrefixLength?: number;
|
|
38
43
|
readonly memoryCost: number;
|
|
39
44
|
readonly parallelism?: number;
|
|
40
45
|
cost: number;
|
|
41
46
|
};
|
|
47
|
+
readonly verifyPath: string;
|
|
42
48
|
readonly storage?: string;
|
|
43
49
|
readonly defaultName: `${string}astro-altcha${string}`;
|
|
44
50
|
readonly challengePath: string;
|
package/dist/integration.js
CHANGED
|
@@ -8,21 +8,28 @@ function integration(options = {}) {
|
|
|
8
8
|
return {
|
|
9
9
|
name: "astro-altcha",
|
|
10
10
|
hooks: {
|
|
11
|
-
"astro:config:setup": ({ injectRoute, updateConfig, addMiddleware, command }) => {
|
|
12
|
-
const
|
|
11
|
+
"astro:config:setup": ({ injectRoute, updateConfig, addMiddleware, command, logger }) => {
|
|
12
|
+
const resolvedConfig = mergeDefaultConfig(command, options);
|
|
13
|
+
if (options.challengePath && resolvedConfig.features.challengeEndpoint === false) logger.warn(`A custom challenge endpoint path was configured but the feature is disabled. The challenge endpoint at ${options.challengePath} will not be created unless the feature flag is set to enabled.`);
|
|
14
|
+
if (options.verifyPath && resolvedConfig.features.verifyEndpoint === false) logger.warn(`A custom verify endpoint path was configured but the feature is disabled. The verify endpoint at ${options.verifyPath} will not be created unless the feature flag is set to enabled.`);
|
|
13
15
|
updateConfig({
|
|
14
|
-
vite: { plugins: [plugin(
|
|
16
|
+
vite: { plugins: [plugin(resolvedConfig), plugin$1(resolvedConfig.storage)] },
|
|
15
17
|
env: { schema: { ALTCHA_HMAC_SIGNATURE: envField.string({
|
|
16
18
|
context: "server",
|
|
17
19
|
access: "secret"
|
|
18
20
|
}) } }
|
|
19
21
|
});
|
|
20
|
-
injectRoute({
|
|
21
|
-
pattern:
|
|
22
|
+
if (resolvedConfig.features.challengeEndpoint) injectRoute({
|
|
23
|
+
pattern: resolvedConfig.challengePath,
|
|
22
24
|
entrypoint: new URL("./api/challenge.js", import.meta.url),
|
|
23
25
|
prerender: false
|
|
24
26
|
});
|
|
25
|
-
|
|
27
|
+
if (resolvedConfig.features.verifyEndpoint) injectRoute({
|
|
28
|
+
pattern: resolvedConfig.verifyPath,
|
|
29
|
+
entrypoint: new URL("./api/verify.js", import.meta.url),
|
|
30
|
+
prerender: false
|
|
31
|
+
});
|
|
32
|
+
if (resolvedConfig.features.middleware) addMiddleware({
|
|
26
33
|
order: "pre",
|
|
27
34
|
entrypoint: new URL("./api/middleware.js", import.meta.url)
|
|
28
35
|
});
|
|
@@ -84,6 +91,12 @@ function mergeDefaultConfig(command, options = {}) {
|
|
|
84
91
|
secure: command !== "dev",
|
|
85
92
|
...options.setCookie === true ? {} : options.setCookie
|
|
86
93
|
};
|
|
94
|
+
const features = {
|
|
95
|
+
middleware: true,
|
|
96
|
+
challengeEndpoint: true,
|
|
97
|
+
verifyEndpoint: false,
|
|
98
|
+
...options.features
|
|
99
|
+
};
|
|
87
100
|
return {
|
|
88
101
|
defaultName,
|
|
89
102
|
challengePath: options.challengePath ?? "/api/altcha/challenge",
|
|
@@ -95,7 +108,9 @@ function mergeDefaultConfig(command, options = {}) {
|
|
|
95
108
|
counter: randomInt(5e3, 1e4),
|
|
96
109
|
...options.challengeOptions
|
|
97
110
|
},
|
|
98
|
-
|
|
111
|
+
verifyPath: options.verifyPath ?? "/api/altcha/verify",
|
|
112
|
+
...setCookie ? { setCookie } : {},
|
|
113
|
+
features
|
|
99
114
|
};
|
|
100
115
|
}
|
|
101
116
|
|
package/dist/types/index.d.ts
CHANGED
|
@@ -24,6 +24,12 @@ export type AstroAltchaConfig = {
|
|
|
24
24
|
*
|
|
25
25
|
*/
|
|
26
26
|
challengeOptions?: WithOptional<AltchaChallengeOptions<Algorithm>, "cost">;
|
|
27
|
+
/**
|
|
28
|
+
* Path of the verify API endpoint.
|
|
29
|
+
|
|
30
|
+
* @default /api/altcha/verify
|
|
31
|
+
*/
|
|
32
|
+
verifyPath?: string;
|
|
27
33
|
/**
|
|
28
34
|
* Challenge expiration in minutes
|
|
29
35
|
*
|
|
@@ -44,6 +50,16 @@ export type AstroAltchaConfig = {
|
|
|
44
50
|
* @default "./src/astro-altcha/storage.ts"
|
|
45
51
|
*/
|
|
46
52
|
storage?: string | URL;
|
|
53
|
+
/**
|
|
54
|
+
* Enable or disable integration features.
|
|
55
|
+
*
|
|
56
|
+
* @default { middleware: true, challengeEndpoint: true, verifyEndpoint: false }
|
|
57
|
+
*/
|
|
58
|
+
features?: {
|
|
59
|
+
middleware?: boolean;
|
|
60
|
+
challengeEndpoint?: boolean;
|
|
61
|
+
verifyEndpoint?: boolean;
|
|
62
|
+
};
|
|
47
63
|
};
|
|
48
64
|
export type ResolvedSetCookieOptions = WithRequired<SetCookieOptions & {
|
|
49
65
|
sameSite?: "lax" | "strict" | "none";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-altcha",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Altcha integration for Astro",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@astrojs/check": "0.9.12",
|
|
53
|
-
"tsdown": "0.23.
|
|
53
|
+
"tsdown": "0.23.7",
|
|
54
54
|
"vite": "^8.3.4"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|