astro-altcha 0.2.0 → 0.3.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 +2 -2
- package/dist/api/altcha-challenge.d.ts +1 -3
- package/dist/api/altcha-challenge.js +9 -6
- package/dist/api/altcha-middleware.d.ts +2 -3
- package/dist/api/altcha-middleware.js +12 -9
- package/dist/api/challenge.d.ts +1 -3
- package/dist/api/challenge.js +1 -2
- package/dist/api/middleware.d.ts +1 -3
- package/dist/api/middleware.js +2 -3
- package/dist/api/pickDeriveKey.d.ts +1 -3
- package/dist/api/pickDeriveKey.js +1 -2
- package/dist/components/common.d.ts +3 -0
- package/dist/components/common.js +46 -0
- package/dist/index.js +1 -2
- package/dist/integration.d.ts +18 -12
- package/dist/integration.js +22 -15
- package/dist/types/index.d.ts +55 -22
- package/dist/vite-plugin.d.ts +0 -2
- package/dist/vite-plugin.js +1 -2
- package/package.json +9 -3
- package/src/components/AltchaForm.astro +15 -15
- package/src/components/AltchaObfuscationWidget.astro +82 -9
- package/src/components/common.ts +63 -0
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
> **Warning** Early development. Expect bugs and frequent 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 sets up altcha for Astro.
|
|
11
11
|
|
|
12
12
|
## Usage
|
|
13
13
|
|
|
@@ -70,7 +70,7 @@ This package is structured as a monorepo:
|
|
|
70
70
|
- `playground` contains code for testing the package
|
|
71
71
|
- `package` contains the actual package
|
|
72
72
|
|
|
73
|
-
Install dependencies using pnpm:
|
|
73
|
+
Install dependencies using pnpm:
|
|
74
74
|
|
|
75
75
|
```bash
|
|
76
76
|
pnpm i --frozen-lockfile
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { Algorithm, ResolvedAstroAltchaConfig } from "../types/index.js";
|
|
2
2
|
import { DeriveKeyFunction } from "altcha/lib";
|
|
3
3
|
import { APIRoute } from "astro";
|
|
4
|
-
|
|
5
|
-
export declare function createChallengeHandler(hmacSecret: string, config: ResolvedAstroAltchaConfig<Algorithm>, deriveKeyFunc: DeriveKeyFunction): APIRoute;
|
|
6
|
-
//#endregion
|
|
4
|
+
export declare function createChallengeHandler(hmacSecret: string, { challengeOptions, challengeExpiresAfterMinutes, setCookie }: ResolvedAstroAltchaConfig<Algorithm>, deriveKeyFunc: DeriveKeyFunction): APIRoute;
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { createChallenge } from "altcha-lib";
|
|
2
2
|
import { deriveHmacKeySecret } from "altcha-lib/frameworks/shared";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
function createChallengeHandler(hmacSecret,
|
|
4
|
+
// src/api/altcha-challenge.ts
|
|
5
|
+
function createChallengeHandler(hmacSecret, { challengeOptions, challengeExpiresAfterMinutes, setCookie }, deriveKeyFunc) {
|
|
6
6
|
return (async (context) => {
|
|
7
7
|
if (context.cache.enabled) context.cache.set(false);
|
|
8
8
|
const challenge = await createChallenge({
|
|
9
|
-
...
|
|
10
|
-
expiresAt: Math.floor(Date.now() / 1e3 +
|
|
9
|
+
...challengeOptions,
|
|
10
|
+
expiresAt: Math.floor(Date.now() / 1e3 + challengeExpiresAfterMinutes * 60),
|
|
11
11
|
deriveKey: deriveKeyFunc,
|
|
12
12
|
hmacSignatureSecret: hmacSecret,
|
|
13
13
|
hmacKeySignatureSecret: await deriveHmacKeySecret(hmacSecret)
|
|
14
14
|
});
|
|
15
|
-
|
|
15
|
+
const body = {
|
|
16
|
+
configuration: setCookie ? { setCookie } : void 0,
|
|
17
|
+
...challenge
|
|
18
|
+
};
|
|
19
|
+
return Response.json(body, { headers: { "Cache-Control": "no-store" } });
|
|
16
20
|
});
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
//#endregion
|
|
20
23
|
export { createChallengeHandler };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { Algorithm, ResolvedAstroAltchaConfig } from "../types/index.js";
|
|
1
2
|
import { DeriveKeyFunction } from "altcha-lib";
|
|
2
3
|
import { z } from "astro/zod";
|
|
3
4
|
import { MiddlewareHandler } from "astro";
|
|
4
|
-
//#region src/api/altcha-middleware.d.ts
|
|
5
5
|
declare const altchaPayloadSchema: z.ZodObject<{
|
|
6
6
|
challenge: z.ZodObject<{
|
|
7
7
|
parameters: z.ZodObject<{
|
|
@@ -23,6 +23,5 @@ declare const altchaPayloadSchema: z.ZodObject<{
|
|
|
23
23
|
}, z.core.$loose>;
|
|
24
24
|
}, z.core.$loose>;
|
|
25
25
|
export type AltchaPayload = z.infer<typeof altchaPayloadSchema>;
|
|
26
|
-
export declare function createAltchaMiddlewareHandler(hmacSecret: string, deriveKeyFunc: DeriveKeyFunction): MiddlewareHandler;
|
|
27
|
-
//#endregion
|
|
26
|
+
export declare function createAltchaMiddlewareHandler(hmacSecret: string, deriveKeyFunc: DeriveKeyFunction, { setCookie }: ResolvedAstroAltchaConfig<Algorithm>): MiddlewareHandler;
|
|
28
27
|
export {};
|
|
@@ -3,7 +3,7 @@ import { deriveHmacKeySecret, verify } from "altcha-lib/frameworks/shared";
|
|
|
3
3
|
import { z } from "astro/zod";
|
|
4
4
|
import { base64ToString } from "uint8array-extras";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// src/api/altcha-middleware.ts
|
|
7
7
|
const altchaPayloadSchema = z.looseObject({
|
|
8
8
|
challenge: z.looseObject({
|
|
9
9
|
parameters: z.looseObject({
|
|
@@ -26,15 +26,18 @@ const altchaPayloadSchema = z.looseObject({
|
|
|
26
26
|
});
|
|
27
27
|
const store = new CappedMap({ maxSize: 499 });
|
|
28
28
|
const keyRegexp = /.*astro-altcha.*/;
|
|
29
|
-
function createAltchaMiddlewareHandler(hmacSecret, deriveKeyFunc) {
|
|
29
|
+
function createAltchaMiddlewareHandler(hmacSecret, deriveKeyFunc, { setCookie }) {
|
|
30
30
|
return async (context, next) => {
|
|
31
31
|
if (context.request.method !== "POST") return next();
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
let altchaBase64 = setCookie?.name ? context.cookies.get(setCookie.name)?.value : void 0;
|
|
33
|
+
if (!altchaBase64) {
|
|
34
|
+
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();
|
|
35
|
+
const body = Object.fromEntries((await context.request.clone().formData()).entries());
|
|
36
|
+
const key = Object.keys(body).find((key) => keyRegexp.test(key));
|
|
37
|
+
if (!key) return next();
|
|
38
|
+
altchaBase64 = body?.[key]?.toString();
|
|
39
|
+
if (!altchaBase64) return next();
|
|
40
|
+
}
|
|
38
41
|
let parsed;
|
|
39
42
|
try {
|
|
40
43
|
parsed = JSON.parse(base64ToString(altchaBase64));
|
|
@@ -48,6 +51,7 @@ function createAltchaMiddlewareHandler(hmacSecret, deriveKeyFunc) {
|
|
|
48
51
|
return next();
|
|
49
52
|
}
|
|
50
53
|
const result = await verify(altchaPayload, deriveKeyFunc, hmacSecret, await deriveHmacKeySecret(hmacSecret), store);
|
|
54
|
+
if (setCookie) context.cookies.delete(setCookie.name, { ...setCookie });
|
|
51
55
|
if (result.error && !result.verification) {
|
|
52
56
|
context.logger?.error(`Error verifying altcha payload: ${result.error}`);
|
|
53
57
|
return next();
|
|
@@ -57,5 +61,4 @@ function createAltchaMiddlewareHandler(hmacSecret, deriveKeyFunc) {
|
|
|
57
61
|
};
|
|
58
62
|
}
|
|
59
63
|
|
|
60
|
-
//#endregion
|
|
61
64
|
export { createAltchaMiddlewareHandler };
|
package/dist/api/challenge.d.ts
CHANGED
package/dist/api/challenge.js
CHANGED
|
@@ -3,10 +3,9 @@ import { pickDeriveKeyFunc } from "./pickDeriveKey.js";
|
|
|
3
3
|
import { getSecret } from "astro:env/server";
|
|
4
4
|
import { config } from "astro-altcha:config";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// src/api/challenge.ts
|
|
7
7
|
const ALTCHA_HMAC_SIGNATURE = getSecret("ALTCHA_HMAC_SIGNATURE");
|
|
8
8
|
if (!ALTCHA_HMAC_SIGNATURE) throw new Error("ALTCHA_HMAC_SIGNATURE not set");
|
|
9
9
|
const GET = createChallengeHandler(ALTCHA_HMAC_SIGNATURE, config, await pickDeriveKeyFunc(config.challengeOptions.algorithm));
|
|
10
10
|
|
|
11
|
-
//#endregion
|
|
12
11
|
export { GET };
|
package/dist/api/middleware.d.ts
CHANGED
package/dist/api/middleware.js
CHANGED
|
@@ -4,11 +4,10 @@ import { getSecret } from "astro:env/server";
|
|
|
4
4
|
import { config } from "astro-altcha:config";
|
|
5
5
|
import { defineMiddleware } from "astro:middleware";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// src/api/middleware.ts
|
|
8
8
|
const ALTCHA_HMAC_SIGNATURE = getSecret("ALTCHA_HMAC_SIGNATURE");
|
|
9
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)));
|
|
10
|
+
const altchaMiddleware = defineMiddleware(createAltchaMiddlewareHandler(ALTCHA_HMAC_SIGNATURE, await pickDeriveKeyFunc(config.challengeOptions.algorithm), config));
|
|
11
11
|
const onRequest = altchaMiddleware;
|
|
12
12
|
|
|
13
|
-
//#endregion
|
|
14
13
|
export { onRequest };
|
|
@@ -1,4 +1,2 @@
|
|
|
1
1
|
import { Algorithm } from "../types/index.js";
|
|
2
|
-
|
|
3
|
-
export declare function pickDeriveKeyFunc(algorithm: Algorithm): Promise<typeof import("altcha-lib/algorithms/pbkdf2").deriveKey>;
|
|
4
|
-
//#endregion
|
|
2
|
+
export declare function pickDeriveKeyFunc(algorithm: Algorithm): Promise<typeof import("altcha-lib/algorithms/pbkdf2").deriveKey>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/api/pickDeriveKey.ts
|
|
2
2
|
async function pickDeriveKeyFunc(algorithm) {
|
|
3
3
|
switch (algorithm) {
|
|
4
4
|
case "PBKDF2/SHA-256":
|
|
@@ -12,5 +12,4 @@ async function pickDeriveKeyFunc(algorithm) {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
//#endregion
|
|
16
15
|
export { pickDeriveKeyFunc };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { AltchaWidgetTheme, AstroAltchaHydrationStrategy } from "../types/index.js";
|
|
2
|
+
export declare function mountWithStrategy(strategy: AstroAltchaHydrationStrategy, element: HTMLElement, mount: () => void): void;
|
|
3
|
+
export declare function importTheme(theme?: AltchaWidgetTheme | null): Promise<void>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/components/common.ts
|
|
2
|
+
function mountWithStrategy(strategy, element, mount) {
|
|
3
|
+
switch (strategy) {
|
|
4
|
+
case "onload":
|
|
5
|
+
mount();
|
|
6
|
+
return;
|
|
7
|
+
case "onfocus":
|
|
8
|
+
element.addEventListener("focusin", mount, { once: true });
|
|
9
|
+
return;
|
|
10
|
+
case "onvisible":
|
|
11
|
+
new IntersectionObserver((entries, observer) => {
|
|
12
|
+
if (!entries.some((entry) => entry.isIntersecting)) return;
|
|
13
|
+
observer.disconnect();
|
|
14
|
+
mount();
|
|
15
|
+
}, { rootMargin: "200px 0px" }).observe(element);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function importTheme(theme) {
|
|
20
|
+
if (!theme) return;
|
|
21
|
+
switch (theme) {
|
|
22
|
+
case "aqua":
|
|
23
|
+
await import("altcha/themes/aqua.min.css");
|
|
24
|
+
return;
|
|
25
|
+
case "business":
|
|
26
|
+
await import("altcha/themes/business.min.css");
|
|
27
|
+
return;
|
|
28
|
+
case "caramel":
|
|
29
|
+
await import("altcha/themes/caramel.min.css");
|
|
30
|
+
return;
|
|
31
|
+
case "cupcake":
|
|
32
|
+
await import("altcha/themes/cupcake.min.css");
|
|
33
|
+
return;
|
|
34
|
+
case "cyberpunk":
|
|
35
|
+
await import("altcha/themes/cyberpunk.min.css");
|
|
36
|
+
return;
|
|
37
|
+
case "lime":
|
|
38
|
+
await import("altcha/themes/lime.min.css");
|
|
39
|
+
return;
|
|
40
|
+
case "wireframe":
|
|
41
|
+
await import("altcha/themes/wireframe.min.css");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { importTheme, mountWithStrategy };
|
package/dist/index.js
CHANGED
package/dist/integration.d.ts
CHANGED
|
@@ -1,39 +1,45 @@
|
|
|
1
|
-
import { Algorithm,
|
|
1
|
+
import { Algorithm, AstroAltchaConfig } from "./types/index.js";
|
|
2
2
|
import { AstroIntegration } from "astro";
|
|
3
|
-
//#region src/integration.d.ts
|
|
4
3
|
export declare function integration(options?: AstroAltchaConfig): AstroIntegration;
|
|
5
|
-
export declare function mergeDefaultConfig(options?: AstroAltchaConfig): {
|
|
4
|
+
export declare function mergeDefaultConfig(command: "dev" | "build" | "preview" | "sync", options?: AstroAltchaConfig): {
|
|
5
|
+
readonly setCookie?: {
|
|
6
|
+
domain?: string;
|
|
7
|
+
name: string;
|
|
8
|
+
maxAge: number;
|
|
9
|
+
path: string;
|
|
10
|
+
sameSite: "none" | "strict" | "lax";
|
|
11
|
+
secure: boolean;
|
|
12
|
+
};
|
|
13
|
+
readonly defaultName: `${string}astro-altcha${string}`;
|
|
14
|
+
readonly challengePath: string;
|
|
15
|
+
readonly challengeExpiresAfterMinutes: number;
|
|
6
16
|
readonly challengeOptions: {
|
|
7
17
|
readonly algorithm: "PBKDF2/SHA-256";
|
|
8
18
|
readonly cost: number;
|
|
9
19
|
readonly counter: number;
|
|
10
20
|
} | {
|
|
11
21
|
readonly algorithm: Exclude<Algorithm, "Argon2id" | "Scrypt">;
|
|
12
|
-
readonly keyLength?: number;
|
|
13
|
-
readonly keyPrefix?: string;
|
|
14
22
|
counter: number;
|
|
15
23
|
readonly counterMode?: "uint32" | "string";
|
|
16
24
|
readonly data?: Record<string, string | number | boolean | null>;
|
|
17
25
|
readonly hmacAlgorithm?: import("altcha-lib").HmacAlgorithm;
|
|
26
|
+
readonly keyLength?: number;
|
|
27
|
+
readonly keyPrefix?: string;
|
|
18
28
|
readonly keyPrefixLength?: number;
|
|
19
29
|
readonly memoryCost?: number;
|
|
20
30
|
readonly parallelism?: number;
|
|
21
31
|
cost: number;
|
|
22
32
|
} | {
|
|
23
33
|
readonly algorithm: "Argon2id" | "Scrypt";
|
|
24
|
-
readonly keyLength?: number;
|
|
25
|
-
readonly keyPrefix?: string;
|
|
26
34
|
counter: number;
|
|
27
35
|
readonly counterMode?: "uint32" | "string";
|
|
28
36
|
readonly data?: Record<string, string | number | boolean | null>;
|
|
29
37
|
readonly hmacAlgorithm?: import("altcha-lib").HmacAlgorithm;
|
|
38
|
+
readonly keyLength?: number;
|
|
39
|
+
readonly keyPrefix?: string;
|
|
30
40
|
readonly keyPrefixLength?: number;
|
|
31
41
|
readonly memoryCost: number;
|
|
32
42
|
readonly parallelism?: number;
|
|
33
43
|
cost: number;
|
|
34
44
|
};
|
|
35
|
-
|
|
36
|
-
readonly challengePath: string;
|
|
37
|
-
readonly challengeExpiresAfterMinutes: number;
|
|
38
|
-
};
|
|
39
|
-
//#endregion
|
|
45
|
+
};
|
package/dist/integration.js
CHANGED
|
@@ -2,13 +2,13 @@ import plugin from "./vite-plugin.js";
|
|
|
2
2
|
import { randomInt } from "altcha-lib";
|
|
3
3
|
import { envField } from "astro/config";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// src/integration.ts
|
|
6
6
|
function integration(options = {}) {
|
|
7
|
-
const config = mergeDefaultConfig(options);
|
|
8
7
|
return {
|
|
9
8
|
name: "astro-altcha",
|
|
10
9
|
hooks: {
|
|
11
|
-
"astro:config:setup": ({ injectRoute, updateConfig, addMiddleware }) => {
|
|
10
|
+
"astro:config:setup": ({ injectRoute, updateConfig, addMiddleware, command }) => {
|
|
11
|
+
const config = mergeDefaultConfig(command, options);
|
|
12
12
|
updateConfig({
|
|
13
13
|
vite: { plugins: [plugin(config)] },
|
|
14
14
|
env: { schema: { ALTCHA_HMAC_SIGNATURE: envField.string({
|
|
@@ -30,11 +30,9 @@ function integration(options = {}) {
|
|
|
30
30
|
injectTypes({
|
|
31
31
|
filename: "types.d.ts",
|
|
32
32
|
content: `declare module "astro-altcha:config" {
|
|
33
|
-
export const config:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
>
|
|
37
|
-
>;
|
|
33
|
+
export const config: import("astro-altcha/types").ResolvedAstroAltchaConfig<
|
|
34
|
+
import("astro-altcha/types").Algorithm
|
|
35
|
+
>
|
|
38
36
|
}
|
|
39
37
|
|
|
40
38
|
declare namespace App {
|
|
@@ -74,20 +72,29 @@ export {};
|
|
|
74
72
|
}
|
|
75
73
|
};
|
|
76
74
|
}
|
|
77
|
-
function mergeDefaultConfig(options = {}) {
|
|
75
|
+
function mergeDefaultConfig(command, options = {}) {
|
|
76
|
+
const defaultName = options.defaultName ?? "astro-altcha";
|
|
77
|
+
const challengeExpiresAfterMinutes = options.challengeExpiresAfterMinutes ?? 20;
|
|
78
|
+
const setCookie = !options.setCookie ? void 0 : {
|
|
79
|
+
name: defaultName,
|
|
80
|
+
maxAge: challengeExpiresAfterMinutes * 60,
|
|
81
|
+
path: "/",
|
|
82
|
+
sameSite: "lax",
|
|
83
|
+
secure: command !== "dev",
|
|
84
|
+
...options.setCookie === true ? {} : options.setCookie
|
|
85
|
+
};
|
|
78
86
|
return {
|
|
79
|
-
defaultName
|
|
80
|
-
challengePath: "/api/altcha/challenge",
|
|
81
|
-
challengeExpiresAfterMinutes
|
|
82
|
-
...options,
|
|
87
|
+
defaultName,
|
|
88
|
+
challengePath: options.challengePath ?? "/api/altcha/challenge",
|
|
89
|
+
challengeExpiresAfterMinutes,
|
|
83
90
|
challengeOptions: {
|
|
84
91
|
algorithm: "PBKDF2/SHA-256",
|
|
85
92
|
cost: 5e3,
|
|
86
93
|
counter: randomInt(5e3, 1e4),
|
|
87
94
|
...options.challengeOptions
|
|
88
|
-
}
|
|
95
|
+
},
|
|
96
|
+
...setCookie ? { setCookie } : {}
|
|
89
97
|
};
|
|
90
98
|
}
|
|
91
99
|
|
|
92
|
-
//#endregion
|
|
93
100
|
export { integration, mergeDefaultConfig };
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CreateChallengeOptions } from "altcha-lib/types";
|
|
2
2
|
import { WidgetAttributes } from "altcha/types";
|
|
3
|
-
|
|
3
|
+
import { SetCookieOptions } from "altcha/lib";
|
|
4
4
|
export type AltchaWidgetName = `${string}astro-altcha${string}`;
|
|
5
5
|
export type Algorithm = `PBKDF2/${Digest}` | "Argon2id" | "Scrypt" | Digest;
|
|
6
6
|
type Digest = `SHA-${"256" | "384" | "512"}`;
|
|
@@ -8,35 +8,43 @@ export type AstroAltchaConfig = {
|
|
|
8
8
|
/**
|
|
9
9
|
* Default altcha-widget name. Must contain the string `astro-altcha`.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* @default astro-altcha
|
|
12
12
|
*/
|
|
13
13
|
defaultName?: AltchaWidgetName;
|
|
14
14
|
/**
|
|
15
15
|
* Path of the challenge API endpoint.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
17
|
+
* @default /api/altcha/challenge
|
|
18
18
|
*/
|
|
19
19
|
challengePath?: string;
|
|
20
20
|
/**
|
|
21
21
|
* Challenge options for [Challange creation](https://altcha.org/docs/integration/server/#challenge-generation)
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* algorithm: "PBKDF2/SHA-256"
|
|
26
|
-
* cost: 5_000
|
|
27
|
-
* counter: randomInt(5_000, 10_000)
|
|
28
|
-
* ```
|
|
23
|
+
* @default { algorithm: "PBKDF2/SHA-256", cost: 5_000, counter: randomInt(5_000, 10_000) }
|
|
24
|
+
*
|
|
29
25
|
*/
|
|
30
26
|
challengeOptions?: WithOptional<AltchaChallengeOptions<Algorithm>, "cost">;
|
|
31
27
|
/**
|
|
32
28
|
* Challenge expiration in minutes
|
|
33
29
|
*
|
|
34
|
-
*
|
|
30
|
+
* @default 20
|
|
35
31
|
*/
|
|
36
32
|
challengeExpiresAfterMinutes?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Use cookies instead of an invisible input field
|
|
35
|
+
*
|
|
36
|
+
* @default { name: "astro-altcha", maxAge: 1200, path: "/", sameSite: "lax", secure: command !== "dev" }
|
|
37
|
+
*/
|
|
38
|
+
setCookie?: true | (SetCookieOptions & {
|
|
39
|
+
sameSite?: "lax" | "strict" | "none";
|
|
40
|
+
});
|
|
37
41
|
};
|
|
38
|
-
export type
|
|
42
|
+
export type ResolvedSetCookieOptions = WithRequired<SetCookieOptions & {
|
|
43
|
+
sameSite?: "lax" | "strict" | "none";
|
|
44
|
+
}, "name" | "maxAge" | "path" | "sameSite" | "secure">;
|
|
45
|
+
export type ResolvedAstroAltchaConfig<Algo extends Algorithm> = Required<Omit<AstroAltchaConfig, "setCookie">> & {
|
|
39
46
|
challengeOptions: AltchaChallengeOptions<Algo>;
|
|
47
|
+
setCookie?: ResolvedSetCookieOptions;
|
|
40
48
|
};
|
|
41
49
|
type WithOptional<T, K extends keyof T> = T extends unknown ? Omit<T, K> & Partial<Pick<T, K>> : never;
|
|
42
50
|
type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P]; };
|
|
@@ -46,23 +54,48 @@ export type AltchaChallengeOptions<Algo extends Algorithm> = Algo extends "Argon
|
|
|
46
54
|
} : OmitCreateChallengeOptions & {
|
|
47
55
|
algorithm: Exclude<Algorithm, "Argon2id" | "Scrypt">;
|
|
48
56
|
};
|
|
49
|
-
export type AstroAltchaHydrationStrategy = "onfocus" | "onload" | "
|
|
50
|
-
export type
|
|
51
|
-
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* `
|
|
57
|
+
export type AstroAltchaHydrationStrategy = "onfocus" | "onload" | "onvisible";
|
|
58
|
+
export type AltchaWidgetProps = {
|
|
59
|
+
/**
|
|
60
|
+
* When the altcha-widget should be hydrated.
|
|
61
|
+
* - onfocus` - when the form is first focused
|
|
62
|
+
* - `onload` - when the page first loads
|
|
63
|
+
* - `onvisible` - when the form becomes visible in the viewport (uses IntersectionObserver)
|
|
56
64
|
*
|
|
57
|
-
*
|
|
65
|
+
* @default onload
|
|
58
66
|
*/
|
|
59
67
|
hydrationStrategy?: AstroAltchaHydrationStrategy;
|
|
68
|
+
/**
|
|
69
|
+
* Select a theme from https://altcha.org/docs/integration/widget-customization/.
|
|
70
|
+
*/
|
|
71
|
+
theme?: AltchaWidgetTheme;
|
|
72
|
+
/**
|
|
73
|
+
* Enable debug mode for altcha widget.
|
|
74
|
+
*/
|
|
60
75
|
debug?: boolean;
|
|
61
|
-
} & Omit<WidgetAttributes, "name">;
|
|
76
|
+
} & Omit<WidgetAttributes, "name" | "theme">;
|
|
77
|
+
export type AltchaWidgetTheme = "aqua" | "business" | "caramel" | "cupcake" | "cyberpunk" | "lime" | "wireframe";
|
|
62
78
|
export type AltchaFormProps = {
|
|
63
|
-
widget?:
|
|
79
|
+
widget?: AltchaWidgetProps & {
|
|
64
80
|
name?: AltchaWidgetName;
|
|
65
81
|
};
|
|
66
82
|
} & astroHTML.JSX.FormHTMLAttributes;
|
|
67
|
-
|
|
83
|
+
export type AltchaObfuscationProps = {
|
|
84
|
+
/**
|
|
85
|
+
* Plain data to be obfuscated.
|
|
86
|
+
*
|
|
87
|
+
* It follows the same rules as a plain altcha-widget, i.e. it recognizes links and automatically
|
|
88
|
+
* inserts the appropriate element on deobfuscation. You do not need to include markup.
|
|
89
|
+
*
|
|
90
|
+
*/
|
|
91
|
+
data: string;
|
|
92
|
+
/**
|
|
93
|
+
* When the altcha-widget should be hydrated.
|
|
94
|
+
* - `onload` - when the page first loads
|
|
95
|
+
* - `onvisible` - when the widget becomes visible in the viewport (uses IntersectionObserver)
|
|
96
|
+
*
|
|
97
|
+
* @default onvisible
|
|
98
|
+
*/
|
|
99
|
+
hydrationStrategy?: Exclude<AstroAltchaHydrationStrategy, "onfocus">;
|
|
100
|
+
} & Omit<AltchaWidgetProps, "hydrationStrategy">;
|
|
68
101
|
export {};
|
package/dist/vite-plugin.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { Algorithm, ResolvedAstroAltchaConfig } from "./types/index.js";
|
|
2
2
|
import { AstroConfig } from "astro";
|
|
3
|
-
//#region src/vite-plugin.d.ts
|
|
4
3
|
declare const plugin: (config: ResolvedAstroAltchaConfig<Algorithm>) => NonNullable<AstroConfig["vite"]["plugins"]>[number];
|
|
5
|
-
//#endregion
|
|
6
4
|
export { plugin as default };
|
package/dist/vite-plugin.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/vite-plugin.ts
|
|
2
2
|
const virtualModuleId = "astro-altcha:config";
|
|
3
3
|
const resolvedVirtualModuleId = `\0${virtualModuleId}`;
|
|
4
4
|
const plugin = (config) => {
|
|
@@ -14,5 +14,4 @@ ${JSON.stringify(config)}`;
|
|
|
14
14
|
};
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
//#endregion
|
|
18
17
|
export { plugin as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-altcha",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Altcha integration for Astro",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -49,8 +49,9 @@
|
|
|
49
49
|
"astro": "^6.0.0 || ^7.0.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@astrojs/check": "0.9.
|
|
53
|
-
"tsdown": "0.23.
|
|
52
|
+
"@astrojs/check": "0.9.12",
|
|
53
|
+
"tsdown": "0.23.5",
|
|
54
|
+
"vite": "^8.3.4"
|
|
54
55
|
},
|
|
55
56
|
"dependencies": {
|
|
56
57
|
"altcha": "^3.2.3",
|
|
@@ -60,7 +61,12 @@
|
|
|
60
61
|
"scripts": {
|
|
61
62
|
"dev": "tsdown --watch",
|
|
62
63
|
"build": "tsdown",
|
|
64
|
+
"prets:check": "pnpm build",
|
|
63
65
|
"ts:check": "pnpm astro check",
|
|
66
|
+
"pretest": "pnpm build",
|
|
67
|
+
"pretest:ci": "pnpm build",
|
|
68
|
+
"pretest:watch": "pnpm build:",
|
|
69
|
+
"pretest:update-snapshots": "pnpm build",
|
|
64
70
|
"test": "node --experimental-test-snapshots --experimental-test-module-mocks --test 'test/**/*.test.ts'",
|
|
65
71
|
"test:ci": "node --experimental-test-snapshots --experimental-test-module-mocks --experimental-test-coverage --test 'test/**/*.test.ts'",
|
|
66
72
|
"test:watch": "node --experimental-test-snapshots --experimental-test-module-mocks --watch --test 'test/**/*.test.ts'",
|
|
@@ -29,7 +29,11 @@ const {
|
|
|
29
29
|
|
|
30
30
|
<script>
|
|
31
31
|
import type { WidgetAttributes, WidgetMethods } from "altcha/types";
|
|
32
|
-
import type {
|
|
32
|
+
import type {
|
|
33
|
+
AltchaWidgetTheme,
|
|
34
|
+
AstroAltchaHydrationStrategy,
|
|
35
|
+
} from "astro-altcha/types";
|
|
36
|
+
import { importTheme, mountWithStrategy } from "./common.ts";
|
|
33
37
|
|
|
34
38
|
const templates = document.querySelectorAll("template");
|
|
35
39
|
|
|
@@ -49,19 +53,13 @@ for (const template of templates) {
|
|
|
49
53
|
continue;
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
const
|
|
56
|
+
const theme = widget.getAttribute("theme") as AltchaWidgetTheme | null;
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
form.addEventListener("focusin", mount, { once: true });
|
|
60
|
-
break;
|
|
61
|
-
case "onhover":
|
|
62
|
-
form.addEventListener("mouseenter", mount, { once: true });
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
58
|
+
mountWithStrategy(
|
|
59
|
+
hydrationStrategy as AstroAltchaHydrationStrategy,
|
|
60
|
+
form,
|
|
61
|
+
() => mountWidget(form, template, widget, debug === "true", theme),
|
|
62
|
+
);
|
|
65
63
|
}
|
|
66
64
|
|
|
67
65
|
async function mountWidget(
|
|
@@ -69,13 +67,15 @@ async function mountWidget(
|
|
|
69
67
|
template: HTMLTemplateElement,
|
|
70
68
|
widget: HTMLElement & WidgetAttributes & WidgetMethods,
|
|
71
69
|
debug: boolean,
|
|
70
|
+
theme?: AltchaWidgetTheme | null,
|
|
72
71
|
) {
|
|
73
|
-
await import("altcha");
|
|
74
|
-
|
|
75
72
|
const widgetReady = new Promise<void>((resolve) =>
|
|
76
73
|
widget.addEventListener("load", () => resolve(), { once: true }),
|
|
77
74
|
);
|
|
78
75
|
|
|
76
|
+
await importTheme(theme);
|
|
77
|
+
await import("altcha");
|
|
78
|
+
|
|
79
79
|
template.replaceWith(widget);
|
|
80
80
|
await widgetReady;
|
|
81
81
|
|
|
@@ -1,21 +1,94 @@
|
|
|
1
1
|
---
|
|
2
|
-
import type { WidgetAttributes } from "altcha/types";
|
|
3
2
|
import { obfuscate } from "altcha-lib/obfuscation";
|
|
3
|
+
import type { AltchaObfuscationProps } from "astro-altcha/types";
|
|
4
4
|
|
|
5
|
-
type Props =
|
|
6
|
-
data: string;
|
|
7
|
-
} & WidgetAttributes;
|
|
5
|
+
type Props = AltchaObfuscationProps;
|
|
8
6
|
|
|
9
|
-
const {
|
|
7
|
+
const {
|
|
8
|
+
data,
|
|
9
|
+
hydrationStrategy = "onvisible",
|
|
10
|
+
debug,
|
|
11
|
+
display = "floating",
|
|
12
|
+
...widgetProps
|
|
13
|
+
} = Astro.props;
|
|
10
14
|
const dataObfuscated = await obfuscate(data);
|
|
11
15
|
---
|
|
12
16
|
|
|
13
|
-
<
|
|
14
|
-
|
|
17
|
+
<template
|
|
18
|
+
data-astro-altcha="obfuscation-widget"
|
|
19
|
+
data-hydration-strategy={hydrationStrategy}
|
|
20
|
+
data-debug={debug}
|
|
21
|
+
>
|
|
22
|
+
<altcha-widget
|
|
23
|
+
data-obfuscated={dataObfuscated}
|
|
24
|
+
{display}
|
|
25
|
+
{...widgetProps}
|
|
26
|
+
></altcha-widget>
|
|
27
|
+
</template>
|
|
28
|
+
<span>
|
|
29
|
+
<slot>
|
|
15
30
|
<button type="button">Click to reveal</button>
|
|
16
31
|
</slot>
|
|
17
|
-
</
|
|
32
|
+
</span>
|
|
18
33
|
|
|
19
34
|
<script>
|
|
20
|
-
import "altcha/
|
|
35
|
+
import type { WidgetAttributes, WidgetMethods } from "altcha/lib";
|
|
36
|
+
import type {
|
|
37
|
+
AltchaWidgetTheme,
|
|
38
|
+
AstroAltchaHydrationStrategy,
|
|
39
|
+
} from "astro-altcha/types";
|
|
40
|
+
import { importTheme, mountWithStrategy } from "./common.ts";
|
|
41
|
+
|
|
42
|
+
const templates = document.querySelectorAll("template");
|
|
43
|
+
|
|
44
|
+
for (const template of templates) {
|
|
45
|
+
if (template.dataset.astroAltcha !== "obfuscation-widget") {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const trigger = template.nextElementSibling;
|
|
50
|
+
const widget = template.content.querySelector("altcha-widget");
|
|
51
|
+
if (!(trigger instanceof HTMLElement) || !widget) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { hydrationStrategy, debug } = template.dataset;
|
|
56
|
+
const theme = widget.getAttribute("theme") as AltchaWidgetTheme | null;
|
|
57
|
+
|
|
58
|
+
mountWithStrategy(
|
|
59
|
+
hydrationStrategy as AstroAltchaHydrationStrategy,
|
|
60
|
+
trigger,
|
|
61
|
+
() => mountWidget(template, trigger, widget, debug === "true", theme),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function mountWidget(
|
|
66
|
+
template: HTMLTemplateElement,
|
|
67
|
+
trigger: Element,
|
|
68
|
+
widget: HTMLElement & WidgetAttributes & WidgetMethods,
|
|
69
|
+
debug: boolean,
|
|
70
|
+
theme?: AltchaWidgetTheme | null,
|
|
71
|
+
) {
|
|
72
|
+
const widgetReady = new Promise<void>((resolve) =>
|
|
73
|
+
widget.addEventListener("load", () => resolve(), { once: true }),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
await importTheme(theme);
|
|
77
|
+
await import("altcha/plugins/obfuscation");
|
|
78
|
+
await import("altcha");
|
|
79
|
+
|
|
80
|
+
// move the trigger into the widget
|
|
81
|
+
widget.append(trigger);
|
|
82
|
+
template.replaceWith(widget);
|
|
83
|
+
|
|
84
|
+
await widgetReady;
|
|
85
|
+
|
|
86
|
+
widget.configure({ debug });
|
|
87
|
+
widget.dispatchEvent(
|
|
88
|
+
new CustomEvent("astro-altcha:init", {
|
|
89
|
+
detail: { widget },
|
|
90
|
+
bubbles: true,
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
21
94
|
</script>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AltchaWidgetTheme,
|
|
3
|
+
AstroAltchaHydrationStrategy,
|
|
4
|
+
} from "astro-altcha/types";
|
|
5
|
+
|
|
6
|
+
export function mountWithStrategy(
|
|
7
|
+
strategy: AstroAltchaHydrationStrategy,
|
|
8
|
+
element: HTMLElement,
|
|
9
|
+
mount: () => void,
|
|
10
|
+
) {
|
|
11
|
+
switch (strategy) {
|
|
12
|
+
case "onload":
|
|
13
|
+
void mount();
|
|
14
|
+
return;
|
|
15
|
+
case "onfocus":
|
|
16
|
+
element.addEventListener("focusin", mount, { once: true });
|
|
17
|
+
return;
|
|
18
|
+
case "onvisible": {
|
|
19
|
+
const observer = new IntersectionObserver(
|
|
20
|
+
(entries, observer) => {
|
|
21
|
+
if (!entries.some((entry) => entry.isIntersecting)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
observer.disconnect();
|
|
25
|
+
void mount();
|
|
26
|
+
},
|
|
27
|
+
{ rootMargin: "200px 0px" },
|
|
28
|
+
);
|
|
29
|
+
observer.observe(element);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function importTheme(theme?: AltchaWidgetTheme | null) {
|
|
36
|
+
if (!theme) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
switch (theme) {
|
|
41
|
+
case "aqua":
|
|
42
|
+
await import("altcha/themes/aqua.min.css");
|
|
43
|
+
return;
|
|
44
|
+
case "business":
|
|
45
|
+
await import("altcha/themes/business.min.css");
|
|
46
|
+
return;
|
|
47
|
+
case "caramel":
|
|
48
|
+
await import("altcha/themes/caramel.min.css");
|
|
49
|
+
return;
|
|
50
|
+
case "cupcake":
|
|
51
|
+
await import("altcha/themes/cupcake.min.css");
|
|
52
|
+
return;
|
|
53
|
+
case "cyberpunk":
|
|
54
|
+
await import("altcha/themes/cyberpunk.min.css");
|
|
55
|
+
return;
|
|
56
|
+
case "lime":
|
|
57
|
+
await import("altcha/themes/lime.min.css");
|
|
58
|
+
return;
|
|
59
|
+
case "wireframe":
|
|
60
|
+
await import("altcha/themes/wireframe.min.css");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|