astro-altcha 0.1.5 → 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 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
- //#region src/api/altcha-challenge.d.ts
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
- //#region src/api/altcha-challenge.ts
5
- function createChallengeHandler(hmacSecret, config, deriveKeyFunc) {
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
- ...config.challengeOptions,
10
- expiresAt: Math.floor(Date.now() / 1e3 + config.challengeExpiresAfterMinutes * 60),
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
- return Response.json(challenge);
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
- //#region src/api/altcha-middleware.ts
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
- 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();
33
- const body = Object.fromEntries((await context.request.clone().formData()).entries());
34
- const key = Object.keys(body).find((key) => keyRegexp.test(key));
35
- if (!key) return next();
36
- const altchaBase64 = body?.[key]?.toString();
37
- if (!altchaBase64) return next();
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 };
@@ -1,3 +1 @@
1
- //#region src/api/challenge.d.ts
2
- export declare const GET: import("astro").APIRoute;
3
- //#endregion
1
+ export declare const GET: import("astro").APIRoute;
@@ -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
- //#region src/api/challenge.ts
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 };
@@ -1,3 +1 @@
1
- //#region src/api/middleware.d.ts
2
- export declare const onRequest: any;
3
- //#endregion
1
+ export declare const onRequest: any;
@@ -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
- //#region src/api/middleware.ts
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
- //#region src/api/pickDeriveKey.d.ts
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
- //#region src/api/pickDeriveKey.ts
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
@@ -1,7 +1,6 @@
1
1
  import { integration } from "./integration.js";
2
2
 
3
- //#region src/index.ts
3
+ // src/index.ts
4
4
  var src_default = integration;
5
5
 
6
- //#endregion
7
6
  export { src_default as default };
@@ -1,39 +1,45 @@
1
- import { Algorithm, AltchaWidgetName, AstroAltchaConfig } from "./types/index.js";
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
- readonly defaultName: AltchaWidgetName;
36
- readonly challengePath: string;
37
- readonly challengeExpiresAfterMinutes: number;
38
- };
39
- //#endregion
45
+ };
@@ -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
- //#region src/integration.ts
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: Required<
34
- import("astro-altcha/types").ResolvedAstroAltchaConfig<
35
- import("astro-altcha/types").Algorithm
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: "astro-altcha",
80
- challengePath: "/api/altcha/challenge",
81
- challengeExpiresAfterMinutes: 20,
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 };
@@ -1,6 +1,6 @@
1
1
  import { CreateChallengeOptions } from "altcha-lib/types";
2
2
  import { WidgetAttributes } from "altcha/types";
3
- //#region src/types/index.d.ts
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
- * Default: `astro-altcha`
11
+ * @default astro-altcha
12
12
  */
13
13
  defaultName?: AltchaWidgetName;
14
14
  /**
15
15
  * Path of the challenge API endpoint.
16
16
  *
17
- * Default: `/api/altcha/challenge`
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
- * Default:
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
- * Default: 20 min
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 ResolvedAstroAltchaConfig<Algo extends Algorithm> = Required<AstroAltchaConfig> & {
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,21 +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 AltchaFormWidgetProps = {
51
- /** When the altcha-widget should be hydrated.
52
- *
53
- * `onfocus` - when the form is first focused
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)
54
64
  *
55
- * `onload` - when the page first loads
65
+ * @default onload
66
+ */
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.
56
74
  */
57
- hydrationStrategy?: "onfocus" | "onload";
58
75
  debug?: boolean;
59
- } & Omit<WidgetAttributes, "name">;
76
+ } & Omit<WidgetAttributes, "name" | "theme">;
77
+ export type AltchaWidgetTheme = "aqua" | "business" | "caramel" | "cupcake" | "cyberpunk" | "lime" | "wireframe";
60
78
  export type AltchaFormProps = {
61
- widget?: AltchaFormWidgetProps & {
79
+ widget?: AltchaWidgetProps & {
62
80
  name?: AltchaWidgetName;
63
81
  };
64
82
  } & astroHTML.JSX.FormHTMLAttributes;
65
- //#endregion
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">;
66
101
  export {};
@@ -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 };
@@ -1,4 +1,4 @@
1
- //#region src/vite-plugin.ts
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.1.5",
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.10",
53
- "tsdown": "0.23.1"
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'",
@@ -18,66 +18,70 @@ const {
18
18
 
19
19
  <form {...formProps}>
20
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
- />
21
+ <template
22
+ data-astro-altcha="form-widget"
23
+ data-hydration-strategy={hydrationStrategy}
24
+ data-debug={debug}
25
+ >
26
+ <altcha-widget {name} {challenge} {...widgetProps}></altcha-widget>
27
+ </template>
33
28
  </form>
34
29
 
35
30
  <script>
36
31
  import type { WidgetAttributes, WidgetMethods } from "altcha/types";
37
- import type { AstroAltchaHydrationStrategy } from "astro-altcha/types";
32
+ import type {
33
+ AltchaWidgetTheme,
34
+ AstroAltchaHydrationStrategy,
35
+ } from "astro-altcha/types";
36
+ import { importTheme, mountWithStrategy } from "./common.ts";
38
37
 
39
- const altchaWidgets = document.querySelectorAll("altcha-widget");
38
+ const templates = document.querySelectorAll("template");
40
39
 
41
- for (const widget of altchaWidgets) {
42
- if (widget.getAttribute("data-astro-altcha") !== "astro-altcha-form-widget") {
40
+ for (const template of templates) {
41
+ if (template.dataset.astroAltcha !== "form-widget") {
43
42
  continue;
44
43
  }
45
44
 
46
- const form = widget.closest("form");
45
+ const form = template.closest("form");
47
46
  if (!form) {
48
- throw new Error("Widget must be placed inside a form element");
47
+ continue;
49
48
  }
50
49
 
51
- const config = form.querySelector("script");
52
- if (config?.getAttribute("data-astro-altcha") !== "config") continue;
53
-
54
- const {
55
- hydrationStrategy,
56
- debug,
57
- }: {
58
- hydrationStrategy: AstroAltchaHydrationStrategy;
59
- debug: boolean;
60
- } = JSON.parse(config.innerText);
61
- if (hydrationStrategy === "onload") {
62
- await loadAltcha(form, widget, debug);
63
- } else {
64
- form.addEventListener("focusin", () => loadAltcha(form, widget, debug), {
65
- once: true,
66
- });
50
+ const { hydrationStrategy, debug } = template.dataset;
51
+ const widget = template.content.querySelector("altcha-widget");
52
+ if (!widget) {
53
+ continue;
67
54
  }
55
+
56
+ const theme = widget.getAttribute("theme") as AltchaWidgetTheme | null;
57
+
58
+ mountWithStrategy(
59
+ hydrationStrategy as AstroAltchaHydrationStrategy,
60
+ form,
61
+ () => mountWidget(form, template, widget, debug === "true", theme),
62
+ );
68
63
  }
69
64
 
70
- async function loadAltcha(
65
+ async function mountWidget(
71
66
  form: HTMLFormElement,
72
- widget: WidgetAttributes & WidgetMethods & HTMLElement,
67
+ template: HTMLTemplateElement,
68
+ widget: HTMLElement & WidgetAttributes & WidgetMethods,
73
69
  debug: boolean,
70
+ theme?: AltchaWidgetTheme | null,
74
71
  ) {
72
+ const widgetReady = new Promise<void>((resolve) =>
73
+ widget.addEventListener("load", () => resolve(), { once: true }),
74
+ );
75
+
76
+ await importTheme(theme);
75
77
  await import("altcha");
76
- widget.configure({ debug });
77
78
 
78
- const configureEvent = new CustomEvent("astro-altcha:init", {
79
- detail: { widget },
80
- });
81
- form.dispatchEvent(configureEvent);
79
+ template.replaceWith(widget);
80
+ await widgetReady;
81
+
82
+ widget.configure({ debug });
83
+ form.dispatchEvent(
84
+ new CustomEvent("astro-altcha:init", { detail: { widget } }),
85
+ );
82
86
  }
83
87
  </script>
@@ -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 { data, display = "floating" } = Astro.props;
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
- <altcha-widget data-obfuscated={dataObfuscated} {display}>
14
- <slot name="reveal">
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
- </altcha-widget>
32
+ </span>
18
33
 
19
34
  <script>
20
- import "altcha/plugins/obfuscation";
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
+ }