astro-altcha 0.2.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 +18 -0
- package/README.md +133 -7
- package/dist/api/altcha-challenge.d.ts +1 -3
- package/dist/api/altcha-challenge.js +9 -6
- package/dist/api/altcha-middleware.d.ts +3 -3
- package/dist/api/altcha-middleware.js +12 -11
- 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 +3 -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.d.ts +2 -0
- package/dist/index.js +5 -3
- package/dist/integration.d.ts +17 -10
- package/dist/integration.js +26 -17
- package/dist/types/index.d.ts +62 -22
- package/dist/{vite-plugin.d.ts → vite-plugin-config.d.ts} +0 -2
- package/dist/{vite-plugin.js → vite-plugin-config.js} +1 -2
- package/dist/vite-plugin-storage.d.ts +3 -0
- package/dist/vite-plugin-storage.js +34 -0
- 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/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
|

|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
> **Warning** Early development. Expect bugs and
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -70,7 +197,7 @@ This package is structured as a monorepo:
|
|
|
70
197
|
- `playground` contains code for testing the package
|
|
71
198
|
- `package` contains the actual package
|
|
72
199
|
|
|
73
|
-
Install dependencies using pnpm:
|
|
200
|
+
Install dependencies using pnpm:
|
|
74
201
|
|
|
75
202
|
```bash
|
|
76
203
|
pnpm i --frozen-lockfile
|
|
@@ -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://
|
|
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:
|
|
@@ -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,8 @@
|
|
|
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
|
-
|
|
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,6 +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): MiddlewareHandler;
|
|
27
|
-
//#endregion
|
|
27
|
+
export declare function createAltchaMiddlewareHandler(hmacSecret: string, deriveKeyFunc: DeriveKeyFunction, { setCookie }: ResolvedAstroAltchaConfig<Algorithm>, store: Store): MiddlewareHandler;
|
|
28
28
|
export {};
|
|
@@ -1,9 +1,8 @@
|
|
|
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";
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
// src/api/altcha-middleware.ts
|
|
7
6
|
const altchaPayloadSchema = z.looseObject({
|
|
8
7
|
challenge: z.looseObject({
|
|
9
8
|
parameters: z.looseObject({
|
|
@@ -24,17 +23,19 @@ 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) {
|
|
27
|
+
function createAltchaMiddlewareHandler(hmacSecret, deriveKeyFunc, { setCookie }, store) {
|
|
30
28
|
return async (context, next) => {
|
|
31
29
|
if (context.request.method !== "POST") return next();
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
let altchaBase64 = setCookie?.name ? context.cookies.get(setCookie.name)?.value : void 0;
|
|
31
|
+
if (!altchaBase64) {
|
|
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
|
+
altchaBase64 = body?.[key]?.toString();
|
|
37
|
+
if (!altchaBase64) return next();
|
|
38
|
+
}
|
|
38
39
|
let parsed;
|
|
39
40
|
try {
|
|
40
41
|
parsed = JSON.parse(base64ToString(altchaBase64));
|
|
@@ -48,6 +49,7 @@ function createAltchaMiddlewareHandler(hmacSecret, deriveKeyFunc) {
|
|
|
48
49
|
return next();
|
|
49
50
|
}
|
|
50
51
|
const result = await verify(altchaPayload, deriveKeyFunc, hmacSecret, await deriveHmacKeySecret(hmacSecret), store);
|
|
52
|
+
if (setCookie) context.cookies.delete(setCookie.name, { ...setCookie });
|
|
51
53
|
if (result.error && !result.verification) {
|
|
52
54
|
context.logger?.error(`Error verifying altcha payload: ${result.error}`);
|
|
53
55
|
return next();
|
|
@@ -57,5 +59,4 @@ function createAltchaMiddlewareHandler(hmacSecret, deriveKeyFunc) {
|
|
|
57
59
|
};
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
//#endregion
|
|
61
62
|
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
|
@@ -3,12 +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)));
|
|
11
|
+
const altchaMiddleware = defineMiddleware(createAltchaMiddlewareHandler(ALTCHA_HMAC_SIGNATURE, await pickDeriveKeyFunc(config.challengeOptions.algorithm), config, store));
|
|
11
12
|
const onRequest = altchaMiddleware;
|
|
12
13
|
|
|
13
|
-
//#endregion
|
|
14
14
|
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.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
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
|
-
|
|
7
|
-
export { src_default as default };
|
|
9
|
+
export { src_default as default, defineStorage };
|
package/dist/integration.d.ts
CHANGED
|
@@ -1,39 +1,46 @@
|
|
|
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: "lax" | "strict" | "none";
|
|
11
|
+
secure: boolean;
|
|
12
|
+
};
|
|
6
13
|
readonly challengeOptions: {
|
|
7
14
|
readonly algorithm: "PBKDF2/SHA-256";
|
|
8
15
|
readonly cost: number;
|
|
9
16
|
readonly counter: number;
|
|
10
17
|
} | {
|
|
11
18
|
readonly algorithm: Exclude<Algorithm, "Argon2id" | "Scrypt">;
|
|
12
|
-
readonly keyLength?: number;
|
|
13
|
-
readonly keyPrefix?: string;
|
|
14
19
|
counter: number;
|
|
15
20
|
readonly counterMode?: "uint32" | "string";
|
|
16
21
|
readonly data?: Record<string, string | number | boolean | null>;
|
|
17
22
|
readonly hmacAlgorithm?: import("altcha-lib").HmacAlgorithm;
|
|
23
|
+
readonly keyLength?: number;
|
|
24
|
+
readonly keyPrefix?: string;
|
|
18
25
|
readonly keyPrefixLength?: number;
|
|
19
26
|
readonly memoryCost?: number;
|
|
20
27
|
readonly parallelism?: number;
|
|
21
28
|
cost: number;
|
|
22
29
|
} | {
|
|
23
30
|
readonly algorithm: "Argon2id" | "Scrypt";
|
|
24
|
-
readonly keyLength?: number;
|
|
25
|
-
readonly keyPrefix?: string;
|
|
26
31
|
counter: number;
|
|
27
32
|
readonly counterMode?: "uint32" | "string";
|
|
28
33
|
readonly data?: Record<string, string | number | boolean | null>;
|
|
29
34
|
readonly hmacAlgorithm?: import("altcha-lib").HmacAlgorithm;
|
|
35
|
+
readonly keyLength?: number;
|
|
36
|
+
readonly keyPrefix?: string;
|
|
30
37
|
readonly keyPrefixLength?: number;
|
|
31
38
|
readonly memoryCost: number;
|
|
32
39
|
readonly parallelism?: number;
|
|
33
40
|
cost: number;
|
|
34
41
|
};
|
|
35
|
-
readonly
|
|
42
|
+
readonly storage?: string;
|
|
43
|
+
readonly defaultName: `${string}astro-altcha${string}`;
|
|
36
44
|
readonly challengePath: string;
|
|
37
45
|
readonly challengeExpiresAfterMinutes: number;
|
|
38
|
-
};
|
|
39
|
-
//#endregion
|
|
46
|
+
};
|
package/dist/integration.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
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
|
|
|
5
|
-
|
|
6
|
+
// src/integration.ts
|
|
6
7
|
function integration(options = {}) {
|
|
7
|
-
const config = mergeDefaultConfig(options);
|
|
8
8
|
return {
|
|
9
9
|
name: "astro-altcha",
|
|
10
10
|
hooks: {
|
|
11
|
-
"astro:config:setup": ({ injectRoute, updateConfig, addMiddleware }) => {
|
|
11
|
+
"astro:config:setup": ({ injectRoute, updateConfig, addMiddleware, command }) => {
|
|
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"
|
|
@@ -30,11 +31,9 @@ function integration(options = {}) {
|
|
|
30
31
|
injectTypes({
|
|
31
32
|
filename: "types.d.ts",
|
|
32
33
|
content: `declare module "astro-altcha:config" {
|
|
33
|
-
export const config:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
>
|
|
37
|
-
>;
|
|
34
|
+
export const config: import("astro-altcha/types").ResolvedAstroAltchaConfig<
|
|
35
|
+
import("astro-altcha/types").Algorithm
|
|
36
|
+
>
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
declare namespace App {
|
|
@@ -74,20 +73,30 @@ export {};
|
|
|
74
73
|
}
|
|
75
74
|
};
|
|
76
75
|
}
|
|
77
|
-
function mergeDefaultConfig(options = {}) {
|
|
76
|
+
function mergeDefaultConfig(command, options = {}) {
|
|
77
|
+
const defaultName = options.defaultName ?? "astro-altcha";
|
|
78
|
+
const challengeExpiresAfterMinutes = options.challengeExpiresAfterMinutes ?? 20;
|
|
79
|
+
const setCookie = !options.setCookie ? void 0 : {
|
|
80
|
+
name: defaultName,
|
|
81
|
+
maxAge: challengeExpiresAfterMinutes * 60,
|
|
82
|
+
path: "/",
|
|
83
|
+
sameSite: "lax",
|
|
84
|
+
secure: command !== "dev",
|
|
85
|
+
...options.setCookie === true ? {} : options.setCookie
|
|
86
|
+
};
|
|
78
87
|
return {
|
|
79
|
-
defaultName
|
|
80
|
-
challengePath: "/api/altcha/challenge",
|
|
81
|
-
challengeExpiresAfterMinutes
|
|
82
|
-
|
|
88
|
+
defaultName,
|
|
89
|
+
challengePath: options.challengePath ?? "/api/altcha/challenge",
|
|
90
|
+
challengeExpiresAfterMinutes,
|
|
91
|
+
...!options.storage ? { storage: options.storage } : {},
|
|
83
92
|
challengeOptions: {
|
|
84
93
|
algorithm: "PBKDF2/SHA-256",
|
|
85
94
|
cost: 5e3,
|
|
86
95
|
counter: randomInt(5e3, 1e4),
|
|
87
96
|
...options.challengeOptions
|
|
88
|
-
}
|
|
97
|
+
},
|
|
98
|
+
...setCookie ? { setCookie } : {}
|
|
89
99
|
};
|
|
90
100
|
}
|
|
91
101
|
|
|
92
|
-
//#endregion
|
|
93
102
|
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,50 @@ 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
|
+
});
|
|
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;
|
|
37
47
|
};
|
|
38
|
-
export type
|
|
48
|
+
export type ResolvedSetCookieOptions = WithRequired<SetCookieOptions & {
|
|
49
|
+
sameSite?: "lax" | "strict" | "none";
|
|
50
|
+
}, "name" | "maxAge" | "path" | "sameSite" | "secure">;
|
|
51
|
+
export type ResolvedAstroAltchaConfig<Algo extends Algorithm> = Omit<Required<AstroAltchaConfig>, "setCookie" | "storage" | "challengeOptions"> & {
|
|
39
52
|
challengeOptions: AltchaChallengeOptions<Algo>;
|
|
53
|
+
setCookie?: ResolvedSetCookieOptions;
|
|
54
|
+
storage?: string | URL;
|
|
40
55
|
};
|
|
41
56
|
type WithOptional<T, K extends keyof T> = T extends unknown ? Omit<T, K> & Partial<Pick<T, K>> : never;
|
|
42
57
|
type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P]; };
|
|
@@ -46,23 +61,48 @@ export type AltchaChallengeOptions<Algo extends Algorithm> = Algo extends "Argon
|
|
|
46
61
|
} : OmitCreateChallengeOptions & {
|
|
47
62
|
algorithm: Exclude<Algorithm, "Argon2id" | "Scrypt">;
|
|
48
63
|
};
|
|
49
|
-
export type AstroAltchaHydrationStrategy = "onfocus" | "onload" | "
|
|
50
|
-
export type
|
|
51
|
-
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* `
|
|
64
|
+
export type AstroAltchaHydrationStrategy = "onfocus" | "onload" | "onvisible";
|
|
65
|
+
export type AltchaWidgetProps = {
|
|
66
|
+
/**
|
|
67
|
+
* When the altcha-widget should be hydrated.
|
|
68
|
+
* - onfocus` - when the form is first focused
|
|
69
|
+
* - `onload` - when the page first loads
|
|
70
|
+
* - `onvisible` - when the form becomes visible in the viewport (uses IntersectionObserver)
|
|
56
71
|
*
|
|
57
|
-
*
|
|
72
|
+
* @default onload
|
|
58
73
|
*/
|
|
59
74
|
hydrationStrategy?: AstroAltchaHydrationStrategy;
|
|
75
|
+
/**
|
|
76
|
+
* Select a theme from https://altcha.org/docs/integration/widget-customization/.
|
|
77
|
+
*/
|
|
78
|
+
theme?: AltchaWidgetTheme;
|
|
79
|
+
/**
|
|
80
|
+
* Enable debug mode for altcha widget.
|
|
81
|
+
*/
|
|
60
82
|
debug?: boolean;
|
|
61
|
-
} & Omit<WidgetAttributes, "name">;
|
|
83
|
+
} & Omit<WidgetAttributes, "name" | "theme">;
|
|
84
|
+
export type AltchaWidgetTheme = "aqua" | "business" | "caramel" | "cupcake" | "cyberpunk" | "lime" | "wireframe";
|
|
62
85
|
export type AltchaFormProps = {
|
|
63
|
-
widget?:
|
|
86
|
+
widget?: AltchaWidgetProps & {
|
|
64
87
|
name?: AltchaWidgetName;
|
|
65
88
|
};
|
|
66
89
|
} & astroHTML.JSX.FormHTMLAttributes;
|
|
67
|
-
|
|
90
|
+
export type AltchaObfuscationProps = {
|
|
91
|
+
/**
|
|
92
|
+
* Plain data to be obfuscated.
|
|
93
|
+
*
|
|
94
|
+
* It follows the same rules as a plain altcha-widget, i.e. it recognizes links and automatically
|
|
95
|
+
* inserts the appropriate element on deobfuscation. You do not need to include markup.
|
|
96
|
+
*
|
|
97
|
+
*/
|
|
98
|
+
data: string;
|
|
99
|
+
/**
|
|
100
|
+
* When the altcha-widget should be hydrated.
|
|
101
|
+
* - `onload` - when the page first loads
|
|
102
|
+
* - `onvisible` - when the widget becomes visible in the viewport (uses IntersectionObserver)
|
|
103
|
+
*
|
|
104
|
+
* @default onvisible
|
|
105
|
+
*/
|
|
106
|
+
hydrationStrategy?: Exclude<AstroAltchaHydrationStrategy, "onfocus">;
|
|
107
|
+
} & Omit<AltchaWidgetProps, "hydrationStrategy">;
|
|
68
108
|
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
|
-
|
|
1
|
+
// src/vite-plugin-config.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 };
|
|
@@ -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
|
+
"version": "0.4.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
|
+
}
|