astro-altcha 0.1.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 +87 -0
- package/dist/api/altcha-challenge.d.ts +6 -0
- package/dist/api/altcha-challenge.js +20 -0
- package/dist/api/altcha-middleware.d.ts +26 -0
- package/dist/api/altcha-middleware.js +60 -0
- package/dist/api/challenge.d.ts +3 -0
- package/dist/api/challenge.js +12 -0
- package/dist/api/middleware.d.ts +3 -0
- package/dist/api/middleware.js +14 -0
- package/dist/api/pickDeriveKey.d.ts +4 -0
- package/dist/api/pickDeriveKey.js +16 -0
- package/dist/components/altcha-form-script.d.ts +1 -0
- package/dist/components/altcha-form-script.js +21 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +4 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -0
- package/dist/integration.d.ts +39 -0
- package/dist/integration.js +85 -0
- package/dist/types/altcha-lib.d.ts +1 -0
- package/dist/types/altcha-lib.js +3 -0
- package/dist/types/altcha.d.ts +1 -0
- package/dist/types/altcha.js +3 -0
- package/dist/types/index.d.ts +65 -0
- package/dist/types/index.js +1 -0
- package/dist/vite-plugin.d.ts +6 -0
- package/dist/vite-plugin.js +18 -0
- package/package.json +65 -0
- package/src/components/AltchaForm.astro +35 -0
- package/src/components/AltchaObfuscationWidget.astro +21 -0
- package/src/components/altcha-form-script.ts +47 -0
- package/src/components/index.ts +2 -0
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# `astro-altcha`
|
|
2
|
+
|
|
3
|
+
This is an [Astro integration](https://docs.astro.build/en/guides/integrations-guide/) that sets up altcha for Astro.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
### Prerequisites
|
|
8
|
+
|
|
9
|
+
TODO:
|
|
10
|
+
|
|
11
|
+
### Installation
|
|
12
|
+
|
|
13
|
+
Install the integration **automatically** using the Astro CLI:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm astro add astro-altcha
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx astro add astro-altcha
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
yarn astro add astro-altcha
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or install it **manually**:
|
|
28
|
+
|
|
29
|
+
1. Install the required dependencies
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pnpm add astro-altcha
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install astro-altcha
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
yarn add astro-altcha
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
2. Add the integration to your astro config
|
|
44
|
+
|
|
45
|
+
```diff
|
|
46
|
+
+import altcha from "astro-altcha";
|
|
47
|
+
|
|
48
|
+
export default defineConfig({
|
|
49
|
+
integrations: [
|
|
50
|
+
+ altcha(),
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Configuration
|
|
56
|
+
|
|
57
|
+
TODO:configuration
|
|
58
|
+
|
|
59
|
+
## Contributing
|
|
60
|
+
|
|
61
|
+
This package is structured as a monorepo:
|
|
62
|
+
|
|
63
|
+
- `playground` contains code for testing the package
|
|
64
|
+
- `package` contains the actual package
|
|
65
|
+
|
|
66
|
+
Install dependencies using pnpm:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pnpm i --frozen-lockfile
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Start the playground and package watcher:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pnpm dev
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
You can now edit files in `package`. Please note that making changes to those files may require restarting the playground dev server.
|
|
79
|
+
|
|
80
|
+
## Licensing
|
|
81
|
+
|
|
82
|
+
[MIT Licensed](https://github.com/TODO:/blob/main/LICENSE). Made with ❤️ by [TODO:](https://github.com/TODO:).
|
|
83
|
+
|
|
84
|
+
## Acknowledgements
|
|
85
|
+
|
|
86
|
+
- Created using [astro-integration-template](https://github.com/florian-lefebvre/astro-integration-template).
|
|
87
|
+
- TODO:
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Algorithm, ResolvedAstroAltchaConfig } from "../types/index.js";
|
|
2
|
+
import { DeriveKeyFunction } from "altcha/lib";
|
|
3
|
+
import { APIRoute } from "astro";
|
|
4
|
+
//#region src/api/altcha-challenge.d.ts
|
|
5
|
+
export declare function createChallengeHandler(hmacSecret: string, config: ResolvedAstroAltchaConfig<Algorithm>, deriveKeyFunc: DeriveKeyFunction): APIRoute;
|
|
6
|
+
//#endregion
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createChallenge } from "altcha-lib";
|
|
2
|
+
import { deriveHmacKeySecret } from "altcha-lib/frameworks/shared";
|
|
3
|
+
|
|
4
|
+
//#region src/api/altcha-challenge.ts
|
|
5
|
+
function createChallengeHandler(hmacSecret, config, deriveKeyFunc) {
|
|
6
|
+
return (async (context) => {
|
|
7
|
+
if (context.cache.enabled) context.cache.set(false);
|
|
8
|
+
const challenge = await createChallenge({
|
|
9
|
+
...config.challengeOptions,
|
|
10
|
+
expiresAt: Date.now() * 1e3 + config.challengeExpiresAfterMinutes * 60,
|
|
11
|
+
deriveKey: deriveKeyFunc,
|
|
12
|
+
hmacSignatureSecret: hmacSecret,
|
|
13
|
+
hmacKeySignatureSecret: await deriveHmacKeySecret(hmacSecret)
|
|
14
|
+
});
|
|
15
|
+
return Response.json(challenge);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { createChallengeHandler };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { DeriveKeyFunction } from "altcha-lib";
|
|
2
|
+
import { z } from "astro/zod";
|
|
3
|
+
import { MiddlewareHandler } from "astro";
|
|
4
|
+
//#region src/api/altcha-middleware.d.ts
|
|
5
|
+
declare const altchaPayloadSchema: z.ZodObject<{
|
|
6
|
+
challenge: z.ZodObject<{
|
|
7
|
+
parameters: z.ZodObject<{
|
|
8
|
+
algorithm: z.ZodString;
|
|
9
|
+
cost: z.ZodNumber;
|
|
10
|
+
keyLength: z.ZodNumber;
|
|
11
|
+
keyPrefix: z.ZodString;
|
|
12
|
+
keySignature: z.ZodString;
|
|
13
|
+
nonce: z.ZodString;
|
|
14
|
+
salt: z.ZodString;
|
|
15
|
+
}, z.core.$strip>;
|
|
16
|
+
signature: z.ZodString;
|
|
17
|
+
}, z.core.$strip>;
|
|
18
|
+
solution: z.ZodObject<{
|
|
19
|
+
counter: z.ZodNumber;
|
|
20
|
+
derivedKey: z.ZodString;
|
|
21
|
+
time: z.ZodNumber;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
export type AltchaPayload = z.infer<typeof altchaPayloadSchema>;
|
|
25
|
+
export declare function createAltchaMiddlewareHandler(hmacSecret: string, deriveKeyFunc: DeriveKeyFunction): MiddlewareHandler;
|
|
26
|
+
//#endregion
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { CappedMap } from "altcha-lib";
|
|
2
|
+
import { deriveHmacKeySecret, verify } from "altcha-lib/frameworks/shared";
|
|
3
|
+
import { z } from "astro/zod";
|
|
4
|
+
import { base64ToString } from "uint8array-extras";
|
|
5
|
+
|
|
6
|
+
//#region src/api/altcha-middleware.ts
|
|
7
|
+
const altchaPayloadSchema = z.object({
|
|
8
|
+
challenge: z.object({
|
|
9
|
+
parameters: z.object({
|
|
10
|
+
algorithm: z.string(),
|
|
11
|
+
cost: z.number(),
|
|
12
|
+
keyLength: z.number(),
|
|
13
|
+
keyPrefix: z.string(),
|
|
14
|
+
keySignature: z.string(),
|
|
15
|
+
nonce: z.string(),
|
|
16
|
+
salt: z.string()
|
|
17
|
+
}),
|
|
18
|
+
signature: z.string()
|
|
19
|
+
}),
|
|
20
|
+
solution: z.object({
|
|
21
|
+
counter: z.number(),
|
|
22
|
+
derivedKey: z.string(),
|
|
23
|
+
time: z.number()
|
|
24
|
+
})
|
|
25
|
+
});
|
|
26
|
+
const store = new CappedMap({ maxSize: 499 });
|
|
27
|
+
const keyRegexp = /.*astro-altcha.*/;
|
|
28
|
+
function createAltchaMiddlewareHandler(hmacSecret, deriveKeyFunc) {
|
|
29
|
+
return async (context, next) => {
|
|
30
|
+
if (context.request.method !== "POST") return next();
|
|
31
|
+
if (!context.request.headers.get("content-type")?.includes("multipart/form-data") && !context.request.headers.get("content-type")?.includes("application/x-www-form-urlencoded")) return next();
|
|
32
|
+
const body = Object.fromEntries((await context.request.clone().formData()).entries());
|
|
33
|
+
const key = Object.keys(body).find((key) => keyRegexp.test(key));
|
|
34
|
+
if (!key) return next();
|
|
35
|
+
const altchaBase64 = body?.[key]?.toString();
|
|
36
|
+
if (!altchaBase64) return next();
|
|
37
|
+
let parsed;
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(base64ToString(altchaBase64));
|
|
40
|
+
} catch (e) {
|
|
41
|
+
context.logger?.error(`Error parsing altcha payload: ${e}`);
|
|
42
|
+
return next();
|
|
43
|
+
}
|
|
44
|
+
const { success, data: altchaPayload, error } = altchaPayloadSchema.safeParse(parsed);
|
|
45
|
+
if (!success) {
|
|
46
|
+
context.logger?.error(`Error parsing altcha payload: ${z.prettifyError(error)}`);
|
|
47
|
+
return next();
|
|
48
|
+
}
|
|
49
|
+
const result = await verify(altchaPayload, deriveKeyFunc, hmacSecret, await deriveHmacKeySecret(hmacSecret), store);
|
|
50
|
+
if (result.error) {
|
|
51
|
+
context.logger?.error(`Error verifying altcha payload: ${result.error}`);
|
|
52
|
+
return next();
|
|
53
|
+
}
|
|
54
|
+
context.locals.altcha = result.verification;
|
|
55
|
+
return next();
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
//#endregion
|
|
60
|
+
export { createAltchaMiddlewareHandler };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createChallengeHandler } from "./altcha-challenge.js";
|
|
2
|
+
import { pickDeriveKeyFunc } from "./pickDeriveKey.js";
|
|
3
|
+
import { getSecret } from "astro:env/server";
|
|
4
|
+
import { config } from "astro-altcha:config";
|
|
5
|
+
|
|
6
|
+
//#region src/api/challenge.ts
|
|
7
|
+
const ALTCHA_HMAC_SIGNATURE = getSecret("ALTCHA_HMAC_SIGNATURE");
|
|
8
|
+
if (!ALTCHA_HMAC_SIGNATURE) throw new Error("ALTCHA_HMAC_SIGNATURE not set");
|
|
9
|
+
const GET = createChallengeHandler(ALTCHA_HMAC_SIGNATURE, config, await pickDeriveKeyFunc(config.challengeOptions.algorithm));
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
export { GET };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createAltchaMiddlewareHandler } from "./altcha-middleware.js";
|
|
2
|
+
import { pickDeriveKeyFunc } from "./pickDeriveKey.js";
|
|
3
|
+
import { getSecret } from "astro:env/server";
|
|
4
|
+
import { config } from "astro-altcha:config";
|
|
5
|
+
import { defineMiddleware } from "astro:middleware";
|
|
6
|
+
|
|
7
|
+
//#region src/api/middleware.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 altchaMiddleware = defineMiddleware(createAltchaMiddlewareHandler(ALTCHA_HMAC_SIGNATURE, await pickDeriveKeyFunc(config.challengeOptions.algorithm)));
|
|
11
|
+
const onRequest = altchaMiddleware;
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
export { onRequest };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/api/pickDeriveKey.ts
|
|
2
|
+
async function pickDeriveKeyFunc(algorithm) {
|
|
3
|
+
switch (algorithm) {
|
|
4
|
+
case "PBKDF2/SHA-256":
|
|
5
|
+
case "PBKDF2/SHA-384":
|
|
6
|
+
case "PBKDF2/SHA-512": return await import("altcha-lib/algorithms/pbkdf2").then((m) => m.deriveKey);
|
|
7
|
+
case "Argon2id": return await import("altcha-lib/algorithms/argon2id").then((m) => m.deriveKey);
|
|
8
|
+
case "Scrypt": return await import("altcha-lib/algorithms/scrypt").then((m) => m.deriveKey);
|
|
9
|
+
case "SHA-256":
|
|
10
|
+
case "SHA-384":
|
|
11
|
+
case "SHA-512": return await import("altcha-lib/algorithms/sha").then((m) => m.deriveKey);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
export { pickDeriveKeyFunc };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/components/altcha-form-script.ts
|
|
2
|
+
const altchaWidgets = document.querySelectorAll("altcha-widget");
|
|
3
|
+
for (const widget of altchaWidgets) {
|
|
4
|
+
if (widget.getAttribute("data-astro-altcha") !== "astro-altcha-form-widget") continue;
|
|
5
|
+
const form = widget.closest("form");
|
|
6
|
+
if (!form) throw new Error("Widget must be placed inside a form element");
|
|
7
|
+
const config = form.querySelector("script");
|
|
8
|
+
if (config?.getAttribute("data-astro-altcha") !== "config") continue;
|
|
9
|
+
const { hydrationStrategy, debug } = JSON.parse(config.innerText);
|
|
10
|
+
if (hydrationStrategy === "onload") await loadAltcha(form, widget, debug);
|
|
11
|
+
else form.addEventListener("focusin", () => loadAltcha(form, widget, debug), { once: true });
|
|
12
|
+
}
|
|
13
|
+
async function loadAltcha(form, widget, debug) {
|
|
14
|
+
await import("altcha");
|
|
15
|
+
widget.configure({ debug });
|
|
16
|
+
const configureEvent = new CustomEvent("astro-altcha:init", { detail: { widget } });
|
|
17
|
+
form.dispatchEvent(configureEvent);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
export { };
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Algorithm, AltchaWidgetName, AstroAltchaConfig } from "./types/index.js";
|
|
2
|
+
import { AstroIntegration } from "astro";
|
|
3
|
+
//#region src/integration.d.ts
|
|
4
|
+
export declare function integration(options?: AstroAltchaConfig): AstroIntegration;
|
|
5
|
+
export declare function mergeDefaultConfig(options?: AstroAltchaConfig): {
|
|
6
|
+
readonly challengeOptions: {
|
|
7
|
+
readonly algorithm: "PBKDF2/SHA-256";
|
|
8
|
+
readonly cost: number;
|
|
9
|
+
readonly counter: number;
|
|
10
|
+
} | {
|
|
11
|
+
readonly algorithm: Exclude<Algorithm, "Argon2id" | "Scrypt">;
|
|
12
|
+
readonly keyLength?: number;
|
|
13
|
+
readonly keyPrefix?: string;
|
|
14
|
+
counter: number;
|
|
15
|
+
readonly counterMode?: "uint32" | "string";
|
|
16
|
+
readonly data?: Record<string, string | number | boolean | null>;
|
|
17
|
+
readonly hmacAlgorithm?: import("altcha-lib").HmacAlgorithm;
|
|
18
|
+
readonly keyPrefixLength?: number;
|
|
19
|
+
readonly memoryCost?: number;
|
|
20
|
+
readonly parallelism?: number;
|
|
21
|
+
cost: number;
|
|
22
|
+
} | {
|
|
23
|
+
readonly algorithm: "Argon2id" | "Scrypt";
|
|
24
|
+
readonly keyLength?: number;
|
|
25
|
+
readonly keyPrefix?: string;
|
|
26
|
+
counter: number;
|
|
27
|
+
readonly counterMode?: "uint32" | "string";
|
|
28
|
+
readonly data?: Record<string, string | number | boolean | null>;
|
|
29
|
+
readonly hmacAlgorithm?: import("altcha-lib").HmacAlgorithm;
|
|
30
|
+
readonly keyPrefixLength?: number;
|
|
31
|
+
readonly memoryCost: number;
|
|
32
|
+
readonly parallelism?: number;
|
|
33
|
+
cost: number;
|
|
34
|
+
};
|
|
35
|
+
readonly defaultName: AltchaWidgetName;
|
|
36
|
+
readonly challengePath: string;
|
|
37
|
+
readonly challengeExpiresAfterMinutes: number;
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import plugin from "./vite-plugin.js";
|
|
2
|
+
import { randomInt } from "altcha-lib";
|
|
3
|
+
import { envField } from "astro/config";
|
|
4
|
+
|
|
5
|
+
//#region src/integration.ts
|
|
6
|
+
function integration(options = {}) {
|
|
7
|
+
const config = mergeDefaultConfig(options);
|
|
8
|
+
return {
|
|
9
|
+
name: "astro-altcha",
|
|
10
|
+
hooks: {
|
|
11
|
+
"astro:config:setup": ({ injectRoute, updateConfig, addMiddleware }) => {
|
|
12
|
+
updateConfig({
|
|
13
|
+
vite: { plugins: [plugin(config)] },
|
|
14
|
+
env: { schema: { ALTCHA_HMAC_SIGNATURE: envField.string({
|
|
15
|
+
context: "server",
|
|
16
|
+
access: "secret"
|
|
17
|
+
}) } }
|
|
18
|
+
});
|
|
19
|
+
injectRoute({
|
|
20
|
+
pattern: config.challengePath,
|
|
21
|
+
entrypoint: new URL("./api/challenge.js", import.meta.url),
|
|
22
|
+
prerender: false
|
|
23
|
+
});
|
|
24
|
+
addMiddleware({
|
|
25
|
+
order: "pre",
|
|
26
|
+
entrypoint: new URL("./api/middleware.js", import.meta.url)
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
"astro:config:done": ({ injectTypes }) => {
|
|
30
|
+
injectTypes({
|
|
31
|
+
filename: "astro-altcha-events.d.ts",
|
|
32
|
+
content: `import type { WidgetAttributes, WidgetMethods } from "astro-altcha/types/altcha"
|
|
33
|
+
|
|
34
|
+
interface CustomEventMap {
|
|
35
|
+
"astro-altcha:init": {
|
|
36
|
+
widget: WidgetAttributes & WidgetMethods & HTMLElement;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
declare global {
|
|
41
|
+
interface HTMLFormElement {
|
|
42
|
+
addEventListener<K extends keyof CustomEventMap>(
|
|
43
|
+
type: K,
|
|
44
|
+
listener: (this: EventTarget, ev: CustomEvent<CustomEventMap[K]>) => void,
|
|
45
|
+
): void;
|
|
46
|
+
|
|
47
|
+
dispatchEvent<K extends keyof CustomEventMap>(
|
|
48
|
+
ev: CustomEvent<CustomEventMap[K]>,
|
|
49
|
+
): boolean;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export {};
|
|
54
|
+
`
|
|
55
|
+
});
|
|
56
|
+
injectTypes({
|
|
57
|
+
filename: "astro-altcha-locals.d.ts",
|
|
58
|
+
content: `declare namespace App {
|
|
59
|
+
interface Locals {
|
|
60
|
+
altcha: import("astro-altcha/types/altcha").VerifySolutionResult | import("astro-altcha/types/altcha-lib").VerifyServerResult | null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
`
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function mergeDefaultConfig(options = {}) {
|
|
70
|
+
return {
|
|
71
|
+
defaultName: "astro-altcha",
|
|
72
|
+
challengePath: "/api/altcha/challenge",
|
|
73
|
+
challengeExpiresAfterMinutes: 20,
|
|
74
|
+
...options,
|
|
75
|
+
challengeOptions: {
|
|
76
|
+
algorithm: "PBKDF2/SHA-256",
|
|
77
|
+
cost: 5e3,
|
|
78
|
+
counter: randomInt(5e3, 1e4),
|
|
79
|
+
...options.challengeOptions
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
//#endregion
|
|
85
|
+
export { integration, mergeDefaultConfig };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "altcha-lib/types";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "altcha/types";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { CreateChallengeOptions } from "altcha-lib/types";
|
|
2
|
+
import { WidgetAttributes } from "altcha/types";
|
|
3
|
+
//#region src/types/index.d.ts
|
|
4
|
+
export type AltchaWidgetName = `${string}astro-altcha${string}`;
|
|
5
|
+
export type Algorithm = `PBKDF2/${Digest}` | "Argon2id" | "Scrypt" | Digest;
|
|
6
|
+
type Digest = `SHA-${"256" | "384" | 512}`;
|
|
7
|
+
export type AstroAltchaConfig = {
|
|
8
|
+
/**
|
|
9
|
+
* Default altcha-widget name. Must contain the string `astro-altcha`.
|
|
10
|
+
*
|
|
11
|
+
* Default: `astro-altcha`
|
|
12
|
+
*/
|
|
13
|
+
defaultName?: AltchaWidgetName;
|
|
14
|
+
/**
|
|
15
|
+
* Path of the challenge API endpoint.
|
|
16
|
+
*
|
|
17
|
+
* Default: `/api/altcha/challenge`
|
|
18
|
+
*/
|
|
19
|
+
challengePath?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Challenge options for [Challange creation](https://altcha.org/docs/integration/server/#challenge-generation)
|
|
22
|
+
*
|
|
23
|
+
* Default:
|
|
24
|
+
* ```
|
|
25
|
+
* algorithm: "PBKDF2/SHA-256"
|
|
26
|
+
* cost: 5_000
|
|
27
|
+
* counter: randomInt(5_000, 10_000)
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
challengeOptions?: WithOptional<AltchaChallengeOptions<Algorithm>, "cost">;
|
|
31
|
+
/**
|
|
32
|
+
* Challenge expiration in minutes
|
|
33
|
+
*
|
|
34
|
+
* Default: 20 min
|
|
35
|
+
*/
|
|
36
|
+
challengeExpiresAfterMinutes?: number;
|
|
37
|
+
};
|
|
38
|
+
export type ResolvedAstroAltchaConfig<Algo extends Algorithm> = Required<AstroAltchaConfig> & {
|
|
39
|
+
challengeOptions: AltchaChallengeOptions<Algo>;
|
|
40
|
+
};
|
|
41
|
+
type WithOptional<T, K extends keyof T> = T extends unknown ? Omit<T, K> & Partial<Pick<T, K>> : never;
|
|
42
|
+
type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P]; };
|
|
43
|
+
type OmitCreateChallengeOptions = Omit<CreateChallengeOptions, "hmacSignatureSecret" | "hmacKeySignatureSecret" | "deriveKey" | "expiresAt" | "algorithm">;
|
|
44
|
+
export type AltchaChallengeOptions<Algo extends Algorithm> = Algo extends "Argon2id" | "Scrypt" ? WithRequired<OmitCreateChallengeOptions, "memoryCost"> & {
|
|
45
|
+
algorithm: "Argon2id" | "Scrypt";
|
|
46
|
+
} : OmitCreateChallengeOptions & {
|
|
47
|
+
algorithm: Exclude<Algorithm, "Argon2id" | "Scrypt">;
|
|
48
|
+
};
|
|
49
|
+
export type AstroAltchaHydrationStrategy = "onfocus" | "onload";
|
|
50
|
+
export type AltchaFormWidgetProps = {
|
|
51
|
+
/** When the altcha-widget should be hydrated.
|
|
52
|
+
*
|
|
53
|
+
* `onfocus` - when the form is first focused
|
|
54
|
+
*
|
|
55
|
+
* `onload` - when the page first loads
|
|
56
|
+
*/
|
|
57
|
+
hydrationStrategy?: "onfocus" | "onload";
|
|
58
|
+
debug?: boolean;
|
|
59
|
+
} & Omit<WidgetAttributes, "name">;
|
|
60
|
+
export type AltchaFormProps = {
|
|
61
|
+
widget?: AltchaFormWidgetProps & {
|
|
62
|
+
name?: AltchaWidgetName;
|
|
63
|
+
};
|
|
64
|
+
} & astroHTML.JSX.FormHTMLAttributes;
|
|
65
|
+
//#endregion
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Algorithm, ResolvedAstroAltchaConfig } from "./types/index.js";
|
|
2
|
+
import { AstroConfig } from "astro";
|
|
3
|
+
//#region src/vite-plugin.d.ts
|
|
4
|
+
declare const plugin: (config: ResolvedAstroAltchaConfig<Algorithm>) => NonNullable<AstroConfig["vite"]["plugins"]>[number];
|
|
5
|
+
//#endregion
|
|
6
|
+
export { plugin as default };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/vite-plugin.ts
|
|
2
|
+
const virtualModuleId = "astro-altcha:config";
|
|
3
|
+
const resolvedVirtualModuleId = `\0${virtualModuleId}`;
|
|
4
|
+
const plugin = (config) => {
|
|
5
|
+
return {
|
|
6
|
+
name: "vite-plugin-altcha-config",
|
|
7
|
+
resolveId(id) {
|
|
8
|
+
if (id === virtualModuleId) return resolvedVirtualModuleId;
|
|
9
|
+
},
|
|
10
|
+
load(id) {
|
|
11
|
+
if (id === resolvedVirtualModuleId) return `export const config =
|
|
12
|
+
${JSON.stringify(config)}`;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
export { plugin as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "astro-altcha",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Altcha integration for Astro",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./types": {
|
|
13
|
+
"types": "./dist/types.d.ts"
|
|
14
|
+
},
|
|
15
|
+
"./types/altcha": {
|
|
16
|
+
"types": "./dist/types/altcha.d.ts"
|
|
17
|
+
},
|
|
18
|
+
"./types/altcha-lib": {
|
|
19
|
+
"types": "./dist/types/altcha-lib.d.ts"
|
|
20
|
+
},
|
|
21
|
+
"./components": "./src/components/index.ts",
|
|
22
|
+
"./components/*": "./src/components/*"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src/components"
|
|
27
|
+
],
|
|
28
|
+
"keywords": [
|
|
29
|
+
"astro-integration",
|
|
30
|
+
"astro-component",
|
|
31
|
+
"withastro",
|
|
32
|
+
"astro",
|
|
33
|
+
"altcha",
|
|
34
|
+
"astro-altcha"
|
|
35
|
+
],
|
|
36
|
+
"author": "Lukas Trombach",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://codeberg.org/Trombach/astro-altcha"
|
|
40
|
+
},
|
|
41
|
+
"bugs": "https://codeberg.org/Trombach/astro-altcha/issues",
|
|
42
|
+
"homepage": "https://codeberg.org/Trombach/astro-altcha",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"astro": "^6.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@astrojs/check": "0.9.10",
|
|
49
|
+
"tsdown": "0.23.0"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"altcha": "^3.2.3",
|
|
53
|
+
"altcha-lib": "^2.5.0",
|
|
54
|
+
"uint8array-extras": "^1.5.0"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"dev": "tsdown --watch",
|
|
58
|
+
"build": "tsdown",
|
|
59
|
+
"ts:check": "pnpm astro check",
|
|
60
|
+
"test": "node --experimental-test-snapshots --experimental-test-module-mocks --test 'test/**/*.test.ts'",
|
|
61
|
+
"test:ci": "node --experimental-test-snapshots --experimental-test-module-mocks --experimental-test-coverage --test 'test/**/*.test.ts'",
|
|
62
|
+
"test:watch": "node --experimental-test-snapshots --experimental-test-module-mocks --watch --test 'test/**/*.test.ts'",
|
|
63
|
+
"test:update-snapshots": "node --test-update-snapshots --experimental-test-snapshots --experimental-test-module-mocks --test 'test/**/*.test.ts'"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { config } from "astro-altcha:config";
|
|
3
|
+
import type { AltchaFormProps } from "../types/index.js";
|
|
4
|
+
|
|
5
|
+
type Props = AltchaFormProps;
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
widget: {
|
|
9
|
+
name = config.defaultName,
|
|
10
|
+
hydrationStrategy = "onload",
|
|
11
|
+
debug,
|
|
12
|
+
challenge = config.challengePath,
|
|
13
|
+
...widgetProps
|
|
14
|
+
} = {},
|
|
15
|
+
...formProps
|
|
16
|
+
} = Astro.props;
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<form {...formProps}>
|
|
20
|
+
<slot />
|
|
21
|
+
<altcha-widget
|
|
22
|
+
data-astro-altcha="astro-altcha-form-widget"
|
|
23
|
+
{name}
|
|
24
|
+
{challenge}
|
|
25
|
+
{...widgetProps}
|
|
26
|
+
></altcha-widget>
|
|
27
|
+
<script
|
|
28
|
+
is:inline
|
|
29
|
+
data-astro-altcha="config"
|
|
30
|
+
type="application/json"
|
|
31
|
+
set:html={JSON.stringify({ hydrationStrategy, debug})}
|
|
32
|
+
/>
|
|
33
|
+
</form>
|
|
34
|
+
|
|
35
|
+
<script src="./altcha-form-script.ts" />
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { WidgetAttributes } from "altcha/types";
|
|
3
|
+
import { obfuscate } from "altcha-lib/obfuscation";
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
data: string;
|
|
7
|
+
} & WidgetAttributes;
|
|
8
|
+
|
|
9
|
+
const { data, display = "floating" } = Astro.props;
|
|
10
|
+
const dataObfuscated = await obfuscate(data);
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<altcha-widget data-obfuscated={dataObfuscated} {display}>
|
|
14
|
+
<slot name="reveal">
|
|
15
|
+
<button type="button">Click to reveal</button>
|
|
16
|
+
</slot>
|
|
17
|
+
</altcha-widget>
|
|
18
|
+
|
|
19
|
+
<script>
|
|
20
|
+
import "altcha/plugins/obfuscation";
|
|
21
|
+
</script>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { WidgetAttributes, WidgetMethods } from "altcha/types";
|
|
2
|
+
import type { AstroAltchaHydrationStrategy } from "../types/index.js";
|
|
3
|
+
|
|
4
|
+
const altchaWidgets = document.querySelectorAll("altcha-widget");
|
|
5
|
+
|
|
6
|
+
for (const widget of altchaWidgets) {
|
|
7
|
+
if (widget.getAttribute("data-astro-altcha") !== "astro-altcha-form-widget") {
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const form = widget.closest("form");
|
|
12
|
+
if (!form) {
|
|
13
|
+
throw new Error("Widget must be placed inside a form element");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const config = form.querySelector("script");
|
|
17
|
+
if (config?.getAttribute("data-astro-altcha") !== "config") continue;
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
hydrationStrategy,
|
|
21
|
+
debug,
|
|
22
|
+
}: {
|
|
23
|
+
hydrationStrategy: AstroAltchaHydrationStrategy;
|
|
24
|
+
debug: boolean;
|
|
25
|
+
} = JSON.parse(config.innerText);
|
|
26
|
+
if (hydrationStrategy === "onload") {
|
|
27
|
+
await loadAltcha(form, widget, debug);
|
|
28
|
+
} else {
|
|
29
|
+
form.addEventListener("focusin", () => loadAltcha(form, widget, debug), {
|
|
30
|
+
once: true,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function loadAltcha(
|
|
36
|
+
form: HTMLFormElement,
|
|
37
|
+
widget: WidgetAttributes & WidgetMethods & HTMLElement,
|
|
38
|
+
debug: boolean,
|
|
39
|
+
) {
|
|
40
|
+
await import("altcha");
|
|
41
|
+
widget.configure({ debug });
|
|
42
|
+
|
|
43
|
+
const configureEvent = new CustomEvent("astro-altcha:init", {
|
|
44
|
+
detail: { widget },
|
|
45
|
+
});
|
|
46
|
+
form.dispatchEvent(configureEvent);
|
|
47
|
+
}
|