astro-altcha 0.3.0 → 0.4.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/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright 2026 Lukas Trombach
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the “Software”), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -5,15 +5,19 @@
5
5
  ![NPM Version](https://img.shields.io/npm/v/astro-altcha)
6
6
 
7
7
 
8
- > **Warning** Early development. Expect bugs and frequent breaking changes.
8
+ > **Warning** Early development. Expect bugs and breaking changes.
9
9
 
10
- This is an [Astro integration](https://docs.astro.build/en/guides/integrations-guide/) that sets up altcha for Astro.
10
+ 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
+
12
+ - automatic middleware and challenge endpoint creation
13
+ - preconfigured challenge storage to prevent replay attacks
14
+ - `AltchaForm` and `AltchaObfuscationWidget` Astro components, optimized for speed; only load altcha's javascript when it's actually needed
11
15
 
12
16
  ## Usage
13
17
 
14
18
  ### Prerequisites
15
19
 
16
- TODO:
20
+ Since `astro-altcha` requires middleware and an endpoint to create and verify challenges at runtime it does not work in a fully static build. At least Astro version 6 is required.
17
21
 
18
22
  ### Installation
19
23
 
@@ -59,9 +63,132 @@ export default defineConfig({
59
63
  });
60
64
  ```
61
65
 
66
+ 4. Set `ALTCHA_HMAC_SIGNATURE` in your local .env file and in your deployment environment. This is the signing key for your altcha challenges and should be kept secret. `astro-altcha` currently does not (and might never) support unsigned challenge generation so this value must be provided.
67
+
62
68
  ### Configuration
63
69
 
64
- TODO:configuration
70
+ #### Integration Options
71
+
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
+ - `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
75
+ - `challengeExpiresAfterMinutes`: time in minutes for a challenge to remain valid. Defaults to 20m
76
+ - `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
+
79
+ #### Customize sSorage
80
+
81
+ `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
+
83
+ ```ts
84
+ // src/astro-altcha/storage.ts
85
+ import { defineStorage } from "astro-altcha";
86
+
87
+ const storage = defineStorage({
88
+ get: (key: string) => console.log(`getting ${key}`),
89
+ set: (key: string, value: boolean) =>
90
+ console.log(`setting ${key} with ${value}`),
91
+ });
92
+ export default storage;
93
+ ````
94
+
95
+ This can be useful if you want to want to use an external storage provider such as redis or if you simply want to provide your own custom implementation.
96
+
97
+ ### Components
98
+
99
+ While you can just use the provided middleware and challenge handlers on their own, the easiest way to get started with `astro-altcha` are the bundled astro components. They are designed to work with the injected challenge endpoint and middleware without further configuration.
100
+
101
+ #### AltchaForm
102
+
103
+ A form component, which includes an `altcha-widget` and handles its initialisation for you.
104
+
105
+ ```astro
106
+ ---
107
+ import { AltchaForm } from "astro-altcha/components";
108
+ import { actions } from "astro:actions";
109
+ ---
110
+
111
+ <AltchaForm action={actions.submitForm}>
112
+ <input name="firstname">
113
+ <input name="lastname">
114
+ <button type="submit">Submit</button>
115
+ </AltchaForm>
116
+ ```
117
+
118
+ With the default integration configuration this will render a standard altcha-widget element on page load. The result of the verification can be easily checked via the Astro locals object either in an Astro action or any other route handler that runs after middleware.
119
+
120
+ ```ts
121
+ // src/actions/index.ts
122
+ import { type ActionAPIContext, defineAction } from "astro:actions";
123
+
124
+ export const server = {
125
+ submitForm: defineAction({
126
+ accept: "form",
127
+ handler: async (input, context: ActionAPIContext) =>
128
+ console.log("is verified?", context.locals.altcha.verified),
129
+ }),
130
+ };
131
+ ````
132
+
133
+ The `AltchaForm` component exposes the raw [Altcha widget configuration options](https://altcha.org/docs/integration/widget/#configuration), but includes a few properties for convenience:
134
+
135
+ ```astro
136
+ ---
137
+ import { AltchaForm } from "astro-altcha/components";
138
+ ---
139
+
140
+ <AltchaForm widget={
141
+ {
142
+ debug: true,
143
+ hydrationStrategy: "onload",
144
+ theme: "cyberpunk",
145
+ }
146
+ }>
147
+ ...
148
+ </AltchaForm>
149
+ ````
150
+
151
+ - the widget's debug mode can be enabled via the `debug` prop on the widget. This will call the widgets `configure` method for you after the widget script has been loaded in your browser
152
+ - the hydration strategy of the widget can be chosen via the `hydrationStrategy` property. By default the widget script is loaded when the page loads. Two other strategies are available: `onvisible` and `onfocus`. `onvisible` uses an `IntersectionObserver` to initialise the widget when the containing form enters the viewport. `onfocus` initialises when the user clicks into the form.
153
+ - the widget's [theme](https://altcha.org/docs/integration/widget/#themes-and-customization) can be set via the `theme` property on the widget. `astro-altcha` takes care of importing the required css files for you.
154
+
155
+ `AltchaForm` emits an `astro-altcha:init` event which fires right after the altcha widget was initialised and gives access to an instance of the contained `altcha-widget` via the details prop.
156
+
157
+ ```astro
158
+ ---
159
+ import { AltchaForm } from "astro-altcha/components";
160
+ ---
161
+
162
+ <AltchaForm name="my-form">...</AltchaForm>
163
+
164
+ <script>
165
+ const form = document.querySelector("form[name=my-form]") as HTMLFormElement;
166
+
167
+ form.addEventListener("astro-altcha:init", (event) => {
168
+ console.log("altcha widget initialised");
169
+ event.detail.widget.configure({...})
170
+ })
171
+ </script>
172
+ ````
173
+
174
+ #### AltchaObfuscationWidget
175
+
176
+ The widget can be used in the same way as described in the [obfuscation documentation](https://altcha.org/docs/integration/obfuscation/), with the advantage that `AltchaObfuscationWidget` takes care of obfuscating the data on the server for you. Just pass data via the `data` prop. Note that this follows the same rules as `altcha-widget`, so if you want to obfuscate a link, just pass the plain url; the widget will render it as an `a` element after deobfuscation.
177
+
178
+ The widget can be themed in the same way as the `AltchaForm` widget. By default its hydration strategy is set to `onvisible` and display to `floating`.
179
+
180
+ ```astro
181
+ ---
182
+ import { AltchaObfuscationWidget } from "astro-altcha/components";
183
+ ---
184
+
185
+ <AltchaObfuscationWidget data="https://astro.build" />
186
+
187
+ <!-- Trigger can be customized -->
188
+ <AltchaObfuscationWidget data="https://astro.build">
189
+ <button type="button">I am a custom reveal button</button>
190
+ </AltchaObfuscationWidget>
191
+ ```
65
192
 
66
193
  ## Contributing
67
194
 
@@ -86,9 +213,8 @@ You can now edit files in `package`. Please note that making changes to those fi
86
213
 
87
214
  ## Licensing
88
215
 
89
- [MIT Licensed](https://github.com/TODO:/blob/main/LICENSE). Made with ❤️ by [TODO:](https://github.com/TODO:).
216
+ [MIT Licensed](https://codeberg.org/Trombach/astro-altcha/blob/main/LICENSE). Made with ❤️ by [Trombach](https://codeberg.org/Trombach).
90
217
 
91
218
  ## Acknowledgements
92
219
 
93
220
  - Created using [astro-integration-template](https://github.com/florian-lefebvre/astro-integration-template).
94
- - TODO:
@@ -2,6 +2,7 @@ import { Algorithm, ResolvedAstroAltchaConfig } from "../types/index.js";
2
2
  import { DeriveKeyFunction } from "altcha-lib";
3
3
  import { z } from "astro/zod";
4
4
  import { MiddlewareHandler } from "astro";
5
+ import { Store } from "altcha-lib/frameworks/types";
5
6
  declare const altchaPayloadSchema: z.ZodObject<{
6
7
  challenge: z.ZodObject<{
7
8
  parameters: z.ZodObject<{
@@ -23,5 +24,5 @@ declare const altchaPayloadSchema: z.ZodObject<{
23
24
  }, z.core.$loose>;
24
25
  }, z.core.$loose>;
25
26
  export type AltchaPayload = z.infer<typeof altchaPayloadSchema>;
26
- export declare function createAltchaMiddlewareHandler(hmacSecret: string, deriveKeyFunc: DeriveKeyFunction, { setCookie }: ResolvedAstroAltchaConfig<Algorithm>): MiddlewareHandler;
27
+ export declare function createAltchaMiddlewareHandler(hmacSecret: string, deriveKeyFunc: DeriveKeyFunction, { setCookie }: ResolvedAstroAltchaConfig<Algorithm>, store: Store): MiddlewareHandler;
27
28
  export {};
@@ -1,4 +1,3 @@
1
- import { CappedMap } from "altcha-lib";
2
1
  import { deriveHmacKeySecret, verify } from "altcha-lib/frameworks/shared";
3
2
  import { z } from "astro/zod";
4
3
  import { base64ToString } from "uint8array-extras";
@@ -24,9 +23,8 @@ const altchaPayloadSchema = z.looseObject({
24
23
  time: z.number()
25
24
  })
26
25
  });
27
- const store = new CappedMap({ maxSize: 499 });
28
26
  const keyRegexp = /.*astro-altcha.*/;
29
- function createAltchaMiddlewareHandler(hmacSecret, deriveKeyFunc, { setCookie }) {
27
+ function createAltchaMiddlewareHandler(hmacSecret, deriveKeyFunc, { setCookie }, store) {
30
28
  return async (context, next) => {
31
29
  if (context.request.method !== "POST") return next();
32
30
  let altchaBase64 = setCookie?.name ? context.cookies.get(setCookie.name)?.value : void 0;
@@ -3,11 +3,12 @@ import { pickDeriveKeyFunc } from "./pickDeriveKey.js";
3
3
  import { getSecret } from "astro:env/server";
4
4
  import { config } from "astro-altcha:config";
5
5
  import { defineMiddleware } from "astro:middleware";
6
+ import store from "astro-altcha:storage";
6
7
 
7
8
  // src/api/middleware.ts
8
9
  const ALTCHA_HMAC_SIGNATURE = getSecret("ALTCHA_HMAC_SIGNATURE");
9
10
  if (!ALTCHA_HMAC_SIGNATURE) throw new Error("ALTCHA_HMAC_SIGNATURE not set");
10
- const altchaMiddleware = defineMiddleware(createAltchaMiddlewareHandler(ALTCHA_HMAC_SIGNATURE, await pickDeriveKeyFunc(config.challengeOptions.algorithm), config));
11
+ const altchaMiddleware = defineMiddleware(createAltchaMiddlewareHandler(ALTCHA_HMAC_SIGNATURE, await pickDeriveKeyFunc(config.challengeOptions.algorithm), config, store));
11
12
  const onRequest = altchaMiddleware;
12
13
 
13
14
  export { onRequest };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  import { integration } from "./integration.js";
2
+ import { Store } from "altcha-lib/frameworks/types";
3
+ export declare function defineStorage<T extends Store>(store: T): T;
2
4
  export { integration as default };
package/dist/index.js CHANGED
@@ -2,5 +2,8 @@ import { integration } from "./integration.js";
2
2
 
3
3
  // src/index.ts
4
4
  var src_default = integration;
5
+ function defineStorage(store) {
6
+ return store;
7
+ }
5
8
 
6
- export { src_default as default };
9
+ export { src_default as default, defineStorage };
@@ -7,12 +7,9 @@ export declare function mergeDefaultConfig(command: "dev" | "build" | "preview"
7
7
  name: string;
8
8
  maxAge: number;
9
9
  path: string;
10
- sameSite: "none" | "strict" | "lax";
10
+ sameSite: "lax" | "strict" | "none";
11
11
  secure: boolean;
12
12
  };
13
- readonly defaultName: `${string}astro-altcha${string}`;
14
- readonly challengePath: string;
15
- readonly challengeExpiresAfterMinutes: number;
16
13
  readonly challengeOptions: {
17
14
  readonly algorithm: "PBKDF2/SHA-256";
18
15
  readonly cost: number;
@@ -42,4 +39,8 @@ export declare function mergeDefaultConfig(command: "dev" | "build" | "preview"
42
39
  readonly parallelism?: number;
43
40
  cost: number;
44
41
  };
42
+ readonly storage?: string;
43
+ readonly defaultName: `${string}astro-altcha${string}`;
44
+ readonly challengePath: string;
45
+ readonly challengeExpiresAfterMinutes: number;
45
46
  };
@@ -1,4 +1,5 @@
1
- import plugin from "./vite-plugin.js";
1
+ import plugin from "./vite-plugin-config.js";
2
+ import plugin$1 from "./vite-plugin-storage.js";
2
3
  import { randomInt } from "altcha-lib";
3
4
  import { envField } from "astro/config";
4
5
 
@@ -10,7 +11,7 @@ function integration(options = {}) {
10
11
  "astro:config:setup": ({ injectRoute, updateConfig, addMiddleware, command }) => {
11
12
  const config = mergeDefaultConfig(command, options);
12
13
  updateConfig({
13
- vite: { plugins: [plugin(config)] },
14
+ vite: { plugins: [plugin(config), plugin$1(config.storage)] },
14
15
  env: { schema: { ALTCHA_HMAC_SIGNATURE: envField.string({
15
16
  context: "server",
16
17
  access: "secret"
@@ -87,6 +88,7 @@ function mergeDefaultConfig(command, options = {}) {
87
88
  defaultName,
88
89
  challengePath: options.challengePath ?? "/api/altcha/challenge",
89
90
  challengeExpiresAfterMinutes,
91
+ ...!options.storage ? { storage: options.storage } : {},
90
92
  challengeOptions: {
91
93
  algorithm: "PBKDF2/SHA-256",
92
94
  cost: 5e3,
@@ -38,13 +38,20 @@ export type AstroAltchaConfig = {
38
38
  setCookie?: true | (SetCookieOptions & {
39
39
  sameSite?: "lax" | "strict" | "none";
40
40
  });
41
+ /**
42
+ * Path or file URL to the module containing a custom storage adapter.
43
+ *
44
+ * @default "./src/astro-altcha/storage.ts"
45
+ */
46
+ storage?: string | URL;
41
47
  };
42
48
  export type ResolvedSetCookieOptions = WithRequired<SetCookieOptions & {
43
49
  sameSite?: "lax" | "strict" | "none";
44
50
  }, "name" | "maxAge" | "path" | "sameSite" | "secure">;
45
- export type ResolvedAstroAltchaConfig<Algo extends Algorithm> = Required<Omit<AstroAltchaConfig, "setCookie">> & {
51
+ export type ResolvedAstroAltchaConfig<Algo extends Algorithm> = Omit<Required<AstroAltchaConfig>, "setCookie" | "storage" | "challengeOptions"> & {
46
52
  challengeOptions: AltchaChallengeOptions<Algo>;
47
53
  setCookie?: ResolvedSetCookieOptions;
54
+ storage?: string | URL;
48
55
  };
49
56
  type WithOptional<T, K extends keyof T> = T extends unknown ? Omit<T, K> & Partial<Pick<T, K>> : never;
50
57
  type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P]; };
@@ -1,4 +1,4 @@
1
- // src/vite-plugin.ts
1
+ // src/vite-plugin-config.ts
2
2
  const virtualModuleId = "astro-altcha:config";
3
3
  const resolvedVirtualModuleId = `\0${virtualModuleId}`;
4
4
  const plugin = (config) => {
@@ -0,0 +1,3 @@
1
+ import { AstroConfig } from "astro";
2
+ declare const plugin: (storagePath?: string | URL) => NonNullable<AstroConfig["vite"]["plugins"]>[number];
3
+ export { plugin as default };
@@ -0,0 +1,34 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ // src/vite-plugin-storage.ts
6
+ const virtualModuleId = "astro-altcha:storage";
7
+ const resolvedVirtualModuleId = `\0${virtualModuleId}`;
8
+ const plugin = (storagePath) => {
9
+ let root = process.cwd();
10
+ return {
11
+ name: "vite-plugin-altcha-storage",
12
+ configResolved(config) {
13
+ root = config.root;
14
+ },
15
+ resolveId(id) {
16
+ if (id !== virtualModuleId) return;
17
+ const found = (storagePath ? [path.resolve(root, storagePath instanceof URL ? fileURLToPath(storagePath) : storagePath)] : [
18
+ "ts",
19
+ "js",
20
+ "mts",
21
+ "mjs"
22
+ ].map((ext) => path.resolve(root, `./src/astro-altcha/storage.${ext}`))).find((candidate) => fs.existsSync(candidate));
23
+ if (!found && storagePath) throw new Error(`storage file not found at: ${storagePath}`);
24
+ return found ?? resolvedVirtualModuleId;
25
+ },
26
+ load(id) {
27
+ if (id === resolvedVirtualModuleId) return `import { CappedMap } from "altcha-lib";
28
+ export default new CappedMap({ maxSize: 499 });
29
+ `;
30
+ }
31
+ };
32
+ };
33
+
34
+ export { plugin as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-altcha",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Altcha integration for Astro",
5
5
  "type": "module",
6
6
  "sideEffects": false,