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 CHANGED
@@ -1,12 +1,10 @@
1
1
  # `astro-altcha`
2
2
 
3
- ![CI](https://codeberg.org/Trombach/astro-altcha/badges/workflows/ci.yml/badge.svg)
3
+ ![CI](https://codeberg.org/Trombach/astro-altcha/badges/workflows/ci.yml/badge.svg?branch=main)
4
4
  ![Release](https://codeberg.org/Trombach/astro-altcha/badges/release.svg)
5
5
  ![NPM Version](https://img.shields.io/npm/v/astro-altcha)
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 sSorage
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/lib";
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 const altchaPayloadSchema: z.ZodObject<{
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,4 @@
1
+ import { DeriveKeyFunction } from "altcha-lib/types";
2
+ import { APIRoute } from "astro";
3
+ import { Store } from "altcha-lib/frameworks/types";
4
+ export declare function createVerifyHandler(hmacSecret: string, deriveKeyFunc: DeriveKeyFunction, store: Store): APIRoute;
@@ -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 };
@@ -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: "lax" | "strict" | "none";
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;
@@ -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 config = mergeDefaultConfig(command, options);
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(config), plugin$1(config.storage)] },
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: config.challengePath,
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
- addMiddleware({
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
- ...setCookie ? { setCookie } : {}
111
+ verifyPath: options.verifyPath ?? "/api/altcha/verify",
112
+ ...setCookie ? { setCookie } : {},
113
+ features
99
114
  };
100
115
  }
101
116
 
@@ -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.4.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.5",
53
+ "tsdown": "0.23.7",
54
54
  "vite": "^8.3.4"
55
55
  },
56
56
  "dependencies": {