altcha 2.2.4 → 2.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 +16 -6
- package/package.json +3 -17
- package/dist_plugins/analytics.d.ts +0 -1
- package/dist_plugins/analytics.js +0 -276
- package/dist_plugins/analytics.umd.cjs +0 -1
- package/dist_plugins/obfuscation.d.ts +0 -1
- package/dist_plugins/obfuscation.js +0 -129
- package/dist_plugins/obfuscation.umd.cjs +0 -1
- package/dist_plugins/upload.d.ts +0 -1
- package/dist_plugins/upload.js +0 -503
- package/dist_plugins/upload.umd.cjs +0 -4
package/README.md
CHANGED
|
@@ -9,6 +9,16 @@ ALTCHA is fully compliant with:
|
|
|
9
9
|
|
|
10
10
|
For more details, visit [altcha.org](https://altcha.org).
|
|
11
11
|
|
|
12
|
+
> [!NOTE]
|
|
13
|
+
> **Breaking change (v2.3.0)**
|
|
14
|
+
>
|
|
15
|
+
> As of version **2.3.0**, the obfuscation and other plugins have been removed from the main package and moved to a separate package: `@altcha/plugins`.
|
|
16
|
+
>
|
|
17
|
+
> If you do not use any plugins, no changes are required.
|
|
18
|
+
>
|
|
19
|
+
> See the [migration guide for v2.3.0](https://github.com/altcha-org/altcha/blob/main/MIGRATION-v2.3.0.md).
|
|
20
|
+
|
|
21
|
+
|
|
12
22
|
## Features
|
|
13
23
|
|
|
14
24
|
- **Frictionless Experience**: Uses proof-of-work (PoW) instead of visual puzzles for a seamless user experience.
|
|
@@ -60,7 +70,7 @@ Explore starter templates for popular frameworks:
|
|
|
60
70
|
|
|
61
71
|
## Plugins & CMS
|
|
62
72
|
|
|
63
|
-
- [Libraries and plugins](https://altcha.org/docs/
|
|
73
|
+
- [Libraries and plugins](https://altcha.org/docs/v2/libraries/)
|
|
64
74
|
|
|
65
75
|
## Usage
|
|
66
76
|
|
|
@@ -103,11 +113,11 @@ Or load via `<script>` tag:
|
|
|
103
113
|
</form>
|
|
104
114
|
```
|
|
105
115
|
|
|
106
|
-
See [configuration options](#configuration) or the [website integration docs](https://altcha.org/docs/
|
|
116
|
+
See [configuration options](#configuration) or the [website integration docs](https://altcha.org/docs/v2/widget-integration).
|
|
107
117
|
|
|
108
118
|
### 3. Integrate with Your Server
|
|
109
119
|
|
|
110
|
-
Refer to the [server documentation](https://altcha.org/docs/server-integration) for implementation details.
|
|
120
|
+
Refer to the [server documentation](https://altcha.org/docs/v2/server-integration) for implementation details.
|
|
111
121
|
|
|
112
122
|
## Supported Browsers
|
|
113
123
|
|
|
@@ -141,7 +151,7 @@ When GZIPped, it totals about 30 kB, making ALTCHA’s widget about 90% smaller
|
|
|
141
151
|
|
|
142
152
|
## Content Security Policy (CSP)
|
|
143
153
|
|
|
144
|
-
The default bundle includes styles and workers in a single file. For strict CSP compliance, use scripts from `/dist_external`. Learn more in the [documentation](https://altcha.org/docs/
|
|
154
|
+
The default bundle includes styles and workers in a single file. For strict CSP compliance, use scripts from `/dist_external`. Learn more in the [documentation](https://altcha.org/docs/v2/widget-integration).
|
|
145
155
|
|
|
146
156
|
## Configuration
|
|
147
157
|
|
|
@@ -172,14 +182,14 @@ Additional options:
|
|
|
172
182
|
- **name**: Name of the hidden field containing the payload (defaults to "altcha").
|
|
173
183
|
- **overlay**: Enables overlay UI mode (automatically sets `auto="onsubmit"`).
|
|
174
184
|
- **overlaycontent**: CSS selector of the HTML element to display in the overlay modal before the widget.
|
|
175
|
-
- **strings**: JSON-encoded translation strings. Refer to [customization](https://altcha.org/docs/widget-customization).
|
|
185
|
+
- **strings**: JSON-encoded translation strings. Refer to [customization](https://altcha.org/docs/v2/widget-customization).
|
|
176
186
|
- **verifyurl**: URL for server-side verification requests. This option is automatically configured with Sentinel. Override this setting only if using a custom server implementation. Supports `fn:function_name` format to call a global JS function instead.
|
|
177
187
|
- **workers**: Number of workers to utilize for PoW (defaults to `navigator.hardwareConcurrency || 8`, max value `16`).
|
|
178
188
|
- **workerurl**: URL of the Worker script (defaults to `./worker.js`, only works with `external` build).
|
|
179
189
|
|
|
180
190
|
Data Obfuscation options:
|
|
181
191
|
|
|
182
|
-
- **obfuscated**: The [obfuscated data](https://altcha.org/docs/obfuscation) provided as a base64-encoded string (requires `altcha/obfuscation` plugin). Use only without `challengeurl`/`challengejson`.
|
|
192
|
+
- **obfuscated**: The [obfuscated data](https://altcha.org/docs/v2/obfuscation) provided as a base64-encoded string (requires `altcha/obfuscation` plugin). Use only without `challengeurl`/`challengejson`.
|
|
183
193
|
|
|
184
194
|
Development / Testing options:
|
|
185
195
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "altcha",
|
|
3
3
|
"description": "Privacy-first CAPTCHA widget, compliant with global regulations (GDPR/HIPAA/CCPA/LGDP/DPDPA/PIPL) and WCAG accessible. No tracking, self-verifying.",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.3.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Daniel Regeci",
|
|
@@ -37,7 +37,6 @@
|
|
|
37
37
|
"files": [
|
|
38
38
|
"dist",
|
|
39
39
|
"dist_external",
|
|
40
|
-
"dist_plugins",
|
|
41
40
|
"dist_i18n"
|
|
42
41
|
],
|
|
43
42
|
"main": "./dist/altcha.umd.cjs",
|
|
@@ -52,18 +51,6 @@
|
|
|
52
51
|
"import": "./dist_external/altcha.css",
|
|
53
52
|
"require": "./dist_external/altcha.css"
|
|
54
53
|
},
|
|
55
|
-
"./analytics": {
|
|
56
|
-
"import": "./dist_plugins/analytics.js",
|
|
57
|
-
"require": "./dist_plugins/analytics.umd.cjs"
|
|
58
|
-
},
|
|
59
|
-
"./obfuscation": {
|
|
60
|
-
"import": "./dist_plugins/obfuscation.js",
|
|
61
|
-
"require": "./dist_plugins/obfuscation.umd.cjs"
|
|
62
|
-
},
|
|
63
|
-
"./upload": {
|
|
64
|
-
"import": "./dist_plugins/upload.js",
|
|
65
|
-
"require": "./dist_plugins/upload.umd.cjs"
|
|
66
|
-
},
|
|
67
54
|
"./external": {
|
|
68
55
|
"import": "./dist_external/altcha.js",
|
|
69
56
|
"require": "./dist_external/altcha.umd.cjs"
|
|
@@ -83,16 +70,15 @@
|
|
|
83
70
|
},
|
|
84
71
|
"scripts": {
|
|
85
72
|
"dev": "vite",
|
|
86
|
-
"build": "npm run build:bundle && npm run build:external && npm run build:
|
|
73
|
+
"build": "npm run build:bundle && npm run build:external && npm run build:i18n",
|
|
87
74
|
"build:bundle": "rimraf dist && vite build && vite build -c vite.bundle-i18n.config.ts && cp src/declarations.d.ts dist/altcha.d.ts && cp src/declarations.d.ts dist/altcha.i18n.d.ts",
|
|
88
75
|
"build:external": "rimraf dist_external && vite build -c vite.external.config.ts && cp src/declarations.d.ts dist_external/altcha.d.ts",
|
|
89
|
-
"build:plugins": "rimraf dist_plugins && find src/plugins -type f -name '*.ts' | xargs -I {} vite build -c vite.plugins.config.ts -- {}",
|
|
90
76
|
"build:i18n": "rimraf dist_i18n && find src/i18n -type f -name '*.ts' | xargs -I {} vite build -c vite.i18n.config.ts -- {}",
|
|
91
77
|
"preview": "vite preview",
|
|
92
78
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
93
79
|
"format": "prettier --write ./src/**/*",
|
|
94
80
|
"test": "vitest run tests/helpers.test.ts",
|
|
95
|
-
"test:e2e": "testcafe chrome
|
|
81
|
+
"test:e2e": "testcafe chrome e2e/altcha.fixture.ts --hostname localhost",
|
|
96
82
|
"prepare": "husky"
|
|
97
83
|
},
|
|
98
84
|
"devDependencies": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
declare module 'altcha/analytics';
|
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
new TextEncoder();
|
|
2
|
-
function r() {
|
|
3
|
-
try {
|
|
4
|
-
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
5
|
-
} catch {
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
|
-
class n {
|
|
9
|
-
/**
|
|
10
|
-
* Constructs a new instance of the Plugin.
|
|
11
|
-
*
|
|
12
|
-
* @param {PluginContext} context - The context provided to the plugin, containing necessary configurations and dependencies.
|
|
13
|
-
*/
|
|
14
|
-
constructor(t) {
|
|
15
|
-
this.context = t;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* A distinct name of the plugin. Every plugin must have it's own name.
|
|
19
|
-
*/
|
|
20
|
-
static pluginName;
|
|
21
|
-
/**
|
|
22
|
-
* Registers a plugin class in the global `altchaPlugins` array.
|
|
23
|
-
* Ensures the plugin is added only once.
|
|
24
|
-
*
|
|
25
|
-
* @param {new(context: PluginContext) => Plugin} cls - The plugin class to register.
|
|
26
|
-
*/
|
|
27
|
-
static register(t) {
|
|
28
|
-
typeof globalThis.altchaPlugins != "object" && (globalThis.altchaPlugins = []), globalThis.altchaPlugins.includes(t) || globalThis.altchaPlugins.push(t);
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Clean up resources when the plugin is destroyed.
|
|
32
|
-
* Override this method in subclasses to implement custom destruction logic.
|
|
33
|
-
*/
|
|
34
|
-
destroy() {
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Callback triggered when an error changes.
|
|
38
|
-
* Override this method in subclasses to handle error state changes.
|
|
39
|
-
*
|
|
40
|
-
* @param {string | null} err - The error message or `null` if there's no error.
|
|
41
|
-
*/
|
|
42
|
-
onErrorChange(t) {
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Callback triggered when the plugin state changes.
|
|
46
|
-
* Override this method in subclasses to handle state changes.
|
|
47
|
-
*
|
|
48
|
-
* @param {State} state - The new state of the plugin.
|
|
49
|
-
*/
|
|
50
|
-
onStateChange(t) {
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
class a extends n {
|
|
54
|
-
static pluginName = "analytics";
|
|
55
|
-
// HTML form element associated with the plugin
|
|
56
|
-
#t;
|
|
57
|
-
// Session instance for tracking analytics data
|
|
58
|
-
#e;
|
|
59
|
-
// Bound method for form submission handling
|
|
60
|
-
#i = this.#s.bind(this);
|
|
61
|
-
/**
|
|
62
|
-
* Creates an instance of PluginAnalytics.
|
|
63
|
-
*
|
|
64
|
-
* @param {PluginContext} context - The context object containing plugin configurations.
|
|
65
|
-
*/
|
|
66
|
-
constructor(t) {
|
|
67
|
-
if (super(t), this.#t = this.context.el.closest("form"), this.#t) {
|
|
68
|
-
let e = this.#t.getAttribute("data-beacon-url");
|
|
69
|
-
const i = this.#t.getAttribute("action");
|
|
70
|
-
!e && i && (e = i + "/beacon"), this.#t.addEventListener("submit", this.#i), this.#e = new h(this.#t, e);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Destroys the plugin instance, removing event listeners and cleaning up the session.
|
|
75
|
-
*/
|
|
76
|
-
destroy() {
|
|
77
|
-
this.#t?.removeEventListener("submit", this.#i), this.#e?.destroy();
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Tracks errors by forwarding them to the session instance.
|
|
81
|
-
*
|
|
82
|
-
* @param {string | null} err - The error message, or `null` if no error exists.
|
|
83
|
-
*/
|
|
84
|
-
onErrorChange(t) {
|
|
85
|
-
this.#e?.trackError(t);
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Handles form submission events, appending session data to the form if applicable.
|
|
89
|
-
*/
|
|
90
|
-
#s() {
|
|
91
|
-
if (this.#e && !this.#e.submitTime) {
|
|
92
|
-
this.#e.end();
|
|
93
|
-
const t = this.#e.dataAsBase64();
|
|
94
|
-
this.context.dispatch("session", t);
|
|
95
|
-
const e = document.createElement("input");
|
|
96
|
-
e.type = "hidden", e.name = "__session", e.value = t, this.#t?.appendChild(e);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
class h {
|
|
101
|
-
/**
|
|
102
|
-
* Creates a new Session instance.
|
|
103
|
-
*
|
|
104
|
-
* @param {HTMLFormElement} elForm - The form element being tracked.
|
|
105
|
-
* @param {string | null} [beaconUrl=null] - The URL to send analytics data to.
|
|
106
|
-
*/
|
|
107
|
-
constructor(t, e = null) {
|
|
108
|
-
this.elForm = t, this.beaconUrl = e, window.addEventListener("unload", this.#n), this.elForm.addEventListener("change", this.#i), this.elForm.addEventListener("focusin", this.#s);
|
|
109
|
-
}
|
|
110
|
-
// Error message associated with the session
|
|
111
|
-
error = null;
|
|
112
|
-
// Timestamp when the form was loaded
|
|
113
|
-
loadTime = Date.now();
|
|
114
|
-
// Timestamp when the form was submitted
|
|
115
|
-
submitTime = null;
|
|
116
|
-
// Timestamp when the user started interacting with the form
|
|
117
|
-
startTime = null;
|
|
118
|
-
// Minimum time in milliseconds required to consider the form "viewed"
|
|
119
|
-
viewTimeThresholdMs = 1500;
|
|
120
|
-
// Tracks the number of changes made to each form field
|
|
121
|
-
#t = {};
|
|
122
|
-
// Name of the last input field focused by the user
|
|
123
|
-
#e = null;
|
|
124
|
-
// Bound method for handling form change events
|
|
125
|
-
#i = this.onFormChange.bind(this);
|
|
126
|
-
// Bound method for handling form focus events
|
|
127
|
-
#s = this.onFormFocus.bind(this);
|
|
128
|
-
// Bound method for handling the unload event
|
|
129
|
-
#n = this.onUnload.bind(this);
|
|
130
|
-
/**
|
|
131
|
-
* Collects and returns analytics data about the form interaction.
|
|
132
|
-
*
|
|
133
|
-
* @returns {Record<string, unknown>} - An object containing analytics data.
|
|
134
|
-
*/
|
|
135
|
-
data() {
|
|
136
|
-
const t = Object.entries(this.#t);
|
|
137
|
-
return {
|
|
138
|
-
correction: t.length && t.filter(([e, i]) => i > 1).length / t.length || 0,
|
|
139
|
-
dropoff: !this.submitTime && !this.error && this.#e ? this.#e : null,
|
|
140
|
-
error: this.error,
|
|
141
|
-
mobile: this.isMobile(),
|
|
142
|
-
start: this.startTime,
|
|
143
|
-
submit: this.submitTime,
|
|
144
|
-
tz: r()
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Encodes the session data as a base64 string.
|
|
149
|
-
*
|
|
150
|
-
* @returns {string} - The base64-encoded session data.
|
|
151
|
-
*/
|
|
152
|
-
dataAsBase64() {
|
|
153
|
-
try {
|
|
154
|
-
return btoa(JSON.stringify(this.data()));
|
|
155
|
-
} catch (t) {
|
|
156
|
-
console.error("failed to encode ALTCHA session data to base64", t);
|
|
157
|
-
}
|
|
158
|
-
return "";
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Destroys the session, removing event listeners.
|
|
162
|
-
*/
|
|
163
|
-
destroy() {
|
|
164
|
-
window.removeEventListener("unload", this.#n), this.elForm.removeEventListener("change", this.#i), this.elForm.removeEventListener("focusin", this.#s);
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Marks the session as ended by recording the submission time.
|
|
168
|
-
*/
|
|
169
|
-
end() {
|
|
170
|
-
this.submitTime || (this.submitTime = Date.now());
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Retrieves the name of a form field, including a group label if available.
|
|
174
|
-
*
|
|
175
|
-
* @param {HTMLInputElement} el - The input element.
|
|
176
|
-
* @param {number} [maxLength=40] - The maximum length of the field name.
|
|
177
|
-
* @returns {string} - The field name, truncated to `maxLength` if necessary.
|
|
178
|
-
*/
|
|
179
|
-
getFieldName(t, e = 40) {
|
|
180
|
-
const i = t.getAttribute("data-group-label"), o = t.getAttribute("name") || t.getAttribute("aria-label");
|
|
181
|
-
return ((i ? i + ": " : "") + o).slice(0, e);
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Determines if the current device is a mobile device.
|
|
185
|
-
*
|
|
186
|
-
* @returns {boolean} - `true` if the device is mobile, otherwise `false`.
|
|
187
|
-
*/
|
|
188
|
-
isMobile() {
|
|
189
|
-
const t = "userAgentData" in navigator && navigator.userAgentData ? navigator.userAgentData : {};
|
|
190
|
-
return "mobile" in t ? t.mobile === !0 : /Mobi/i.test(window.navigator.userAgent);
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Checks if a given element is an input element (input, select, or textarea).
|
|
194
|
-
*
|
|
195
|
-
* @param {HTMLElement} el - The element to check.
|
|
196
|
-
* @returns {boolean} - `true` if the element is an input, otherwise `false`.
|
|
197
|
-
*/
|
|
198
|
-
isInput(t) {
|
|
199
|
-
return ["INPUT", "SELECT", "TEXTAREA"].includes(t.tagName);
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Tracks changes to a specific form field.
|
|
203
|
-
*
|
|
204
|
-
* @param {HTMLInputElement} el - The input element that changed.
|
|
205
|
-
*/
|
|
206
|
-
onFormFieldChange(t) {
|
|
207
|
-
const e = this.getFieldName(t);
|
|
208
|
-
e && this.trackFieldChange(e);
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Handles form change events, tracking changes to input fields.
|
|
212
|
-
*
|
|
213
|
-
* @param {Event} ev - The change event.
|
|
214
|
-
*/
|
|
215
|
-
onFormChange(t) {
|
|
216
|
-
const e = t.target;
|
|
217
|
-
e && this.isInput(e) && this.onFormFieldChange(e);
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Handles form focus events, marking the session start time and tracking the last focused field.
|
|
221
|
-
*
|
|
222
|
-
* @param {FocusEvent} ev - The focus event.
|
|
223
|
-
*/
|
|
224
|
-
onFormFocus(t) {
|
|
225
|
-
const e = t.target;
|
|
226
|
-
if (this.startTime || this.start(), e && this.isInput(e)) {
|
|
227
|
-
const i = this.getFieldName(e);
|
|
228
|
-
i && (this.#e = i);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
232
|
-
* Handles the window unload event, sending a beacon with session data if the form was viewed but not submitted.
|
|
233
|
-
*/
|
|
234
|
-
onUnload() {
|
|
235
|
-
this.loadTime <= Date.now() - this.viewTimeThresholdMs && !this.submitTime && this.sendBeacon();
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Sends a beacon with session data to the specified beacon URL.
|
|
239
|
-
*/
|
|
240
|
-
async sendBeacon() {
|
|
241
|
-
if (this.beaconUrl && "sendBeacon" in navigator)
|
|
242
|
-
try {
|
|
243
|
-
navigator.sendBeacon(
|
|
244
|
-
new URL(this.beaconUrl, location.origin),
|
|
245
|
-
JSON.stringify(this.data())
|
|
246
|
-
);
|
|
247
|
-
} catch {
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Marks the session as started by recording the start time.
|
|
252
|
-
*/
|
|
253
|
-
start() {
|
|
254
|
-
this.startTime = Date.now();
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* Tracks an error associated with the session.
|
|
258
|
-
*
|
|
259
|
-
* @param {string | null} err - The error message, or `null` if no error exists.
|
|
260
|
-
*/
|
|
261
|
-
trackError(t) {
|
|
262
|
-
this.error = t === null ? null : String(t);
|
|
263
|
-
}
|
|
264
|
-
/**
|
|
265
|
-
* Tracks a change to a specific form field.
|
|
266
|
-
*
|
|
267
|
-
* @param {string} name - The name of the form field.
|
|
268
|
-
*/
|
|
269
|
-
trackFieldChange(t) {
|
|
270
|
-
this.#t[t] = (this.#t[t] || 0) + 1;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
n.register(a);
|
|
274
|
-
export {
|
|
275
|
-
a as PluginAnalytics
|
|
276
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
(function(s,n){typeof exports=="object"&&typeof module<"u"?n(exports):typeof define=="function"&&define.amd?define(["exports"],n):(s=typeof globalThis<"u"?globalThis:s||self,n(s["[name]"]={}))})(this,function(s){"use strict";new TextEncoder;function n(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone}catch{}}class o{constructor(t){this.context=t}static pluginName;static register(t){typeof globalThis.altchaPlugins!="object"&&(globalThis.altchaPlugins=[]),globalThis.altchaPlugins.includes(t)||globalThis.altchaPlugins.push(t)}destroy(){}onErrorChange(t){}onStateChange(t){}}class r extends o{static pluginName="analytics";#t;#e;#i=this.#s.bind(this);constructor(t){if(super(t),this.#t=this.context.el.closest("form"),this.#t){let e=this.#t.getAttribute("data-beacon-url");const i=this.#t.getAttribute("action");!e&&i&&(e=i+"/beacon"),this.#t.addEventListener("submit",this.#i),this.#e=new h(this.#t,e)}}destroy(){this.#t?.removeEventListener("submit",this.#i),this.#e?.destroy()}onErrorChange(t){this.#e?.trackError(t)}#s(){if(this.#e&&!this.#e.submitTime){this.#e.end();const t=this.#e.dataAsBase64();this.context.dispatch("session",t);const e=document.createElement("input");e.type="hidden",e.name="__session",e.value=t,this.#t?.appendChild(e)}}}class h{constructor(t,e=null){this.elForm=t,this.beaconUrl=e,window.addEventListener("unload",this.#n),this.elForm.addEventListener("change",this.#i),this.elForm.addEventListener("focusin",this.#s)}error=null;loadTime=Date.now();submitTime=null;startTime=null;viewTimeThresholdMs=1500;#t={};#e=null;#i=this.onFormChange.bind(this);#s=this.onFormFocus.bind(this);#n=this.onUnload.bind(this);data(){const t=Object.entries(this.#t);return{correction:t.length&&t.filter(([e,i])=>i>1).length/t.length||0,dropoff:!this.submitTime&&!this.error&&this.#e?this.#e:null,error:this.error,mobile:this.isMobile(),start:this.startTime,submit:this.submitTime,tz:n()}}dataAsBase64(){try{return btoa(JSON.stringify(this.data()))}catch(t){console.error("failed to encode ALTCHA session data to base64",t)}return""}destroy(){window.removeEventListener("unload",this.#n),this.elForm.removeEventListener("change",this.#i),this.elForm.removeEventListener("focusin",this.#s)}end(){this.submitTime||(this.submitTime=Date.now())}getFieldName(t,e=40){const i=t.getAttribute("data-group-label"),l=t.getAttribute("name")||t.getAttribute("aria-label");return((i?i+": ":"")+l).slice(0,e)}isMobile(){const t="userAgentData"in navigator&&navigator.userAgentData?navigator.userAgentData:{};return"mobile"in t?t.mobile===!0:/Mobi/i.test(window.navigator.userAgent)}isInput(t){return["INPUT","SELECT","TEXTAREA"].includes(t.tagName)}onFormFieldChange(t){const e=this.getFieldName(t);e&&this.trackFieldChange(e)}onFormChange(t){const e=t.target;e&&this.isInput(e)&&this.onFormFieldChange(e)}onFormFocus(t){const e=t.target;if(this.startTime||this.start(),e&&this.isInput(e)){const i=this.getFieldName(e);i&&(this.#e=i)}}onUnload(){this.loadTime<=Date.now()-this.viewTimeThresholdMs&&!this.submitTime&&this.sendBeacon()}async sendBeacon(){if(this.beaconUrl&&"sendBeacon"in navigator)try{navigator.sendBeacon(new URL(this.beaconUrl,location.origin),JSON.stringify(this.data()))}catch{}}start(){this.startTime=Date.now()}trackError(t){this.error=t===null?null:String(t)}trackFieldChange(t){this.#t[t]=(this.#t[t]||0)+1}}o.register(r),s.PluginAnalytics=r,Object.defineProperty(s,Symbol.toStringTag,{value:"Module"})});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
declare module 'altcha/obfuscation';
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
class h {
|
|
2
|
-
/**
|
|
3
|
-
* Constructs a new instance of the Plugin.
|
|
4
|
-
*
|
|
5
|
-
* @param {PluginContext} context - The context provided to the plugin, containing necessary configurations and dependencies.
|
|
6
|
-
*/
|
|
7
|
-
constructor(t) {
|
|
8
|
-
this.context = t;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* A distinct name of the plugin. Every plugin must have it's own name.
|
|
12
|
-
*/
|
|
13
|
-
static pluginName;
|
|
14
|
-
/**
|
|
15
|
-
* Registers a plugin class in the global `altchaPlugins` array.
|
|
16
|
-
* Ensures the plugin is added only once.
|
|
17
|
-
*
|
|
18
|
-
* @param {new(context: PluginContext) => Plugin} cls - The plugin class to register.
|
|
19
|
-
*/
|
|
20
|
-
static register(t) {
|
|
21
|
-
typeof globalThis.altchaPlugins != "object" && (globalThis.altchaPlugins = []), globalThis.altchaPlugins.includes(t) || globalThis.altchaPlugins.push(t);
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Clean up resources when the plugin is destroyed.
|
|
25
|
-
* Override this method in subclasses to implement custom destruction logic.
|
|
26
|
-
*/
|
|
27
|
-
destroy() {
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Callback triggered when an error changes.
|
|
31
|
-
* Override this method in subclasses to handle error state changes.
|
|
32
|
-
*
|
|
33
|
-
* @param {string | null} err - The error message or `null` if there's no error.
|
|
34
|
-
*/
|
|
35
|
-
onErrorChange(t) {
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Callback triggered when the plugin state changes.
|
|
39
|
-
* Override this method in subclasses to handle state changes.
|
|
40
|
-
*
|
|
41
|
-
* @param {State} state - The new state of the plugin.
|
|
42
|
-
*/
|
|
43
|
-
onStateChange(t) {
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
var s = /* @__PURE__ */ ((e) => (e.CODE = "code", e.ERROR = "error", e.VERIFIED = "verified", e.VERIFYING = "verifying", e.UNVERIFIED = "unverified", e.EXPIRED = "expired", e))(s || {});
|
|
47
|
-
class R extends h {
|
|
48
|
-
static pluginName = "obfuscation";
|
|
49
|
-
// The button element associated with revealing the obfuscated data
|
|
50
|
-
elButton;
|
|
51
|
-
// Bound method for handling button click events
|
|
52
|
-
#t = this.#e.bind(this);
|
|
53
|
-
/**
|
|
54
|
-
* Creates an instance of PluginObfuscation.
|
|
55
|
-
*
|
|
56
|
-
* @param {PluginContext} context - The context object containing plugin configurations.
|
|
57
|
-
*/
|
|
58
|
-
constructor(t) {
|
|
59
|
-
super(t);
|
|
60
|
-
const i = t.el;
|
|
61
|
-
this.elButton = i.parentElement?.querySelector("[data-clarify-button]") || i.parentElement?.querySelector("button, a"), this.elButton && this.elButton.addEventListener("click", this.#t);
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Destroys the plugin instance, removing event listeners.
|
|
65
|
-
*/
|
|
66
|
-
destroy() {
|
|
67
|
-
this.elButton && this.elButton.removeEventListener("click", this.#t);
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Handles the clarification process by decrypting the obfuscated data and rendering the clear text.
|
|
71
|
-
*/
|
|
72
|
-
async clarify() {
|
|
73
|
-
const {
|
|
74
|
-
el: t,
|
|
75
|
-
getConfiguration: i,
|
|
76
|
-
getFloatingAnchor: n,
|
|
77
|
-
setFloatingAnchor: r,
|
|
78
|
-
reset: f,
|
|
79
|
-
solve: d,
|
|
80
|
-
setState: c
|
|
81
|
-
} = this.context, { delay: g, floating: m, maxnumber: p, obfuscated: u } = i();
|
|
82
|
-
if (this.elButton && !n() && r(this.elButton), !u) {
|
|
83
|
-
c(s.ERROR);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
f(s.VERIFYING), await new Promise((l) => setTimeout(l, g || 0));
|
|
87
|
-
const [E, y] = u.split("?");
|
|
88
|
-
let o = new URLSearchParams(y || "").get("key") || void 0;
|
|
89
|
-
if (o) {
|
|
90
|
-
const l = o.match(/^\(prompt:?(.*)\)$/);
|
|
91
|
-
l && (o = prompt(l[1] || "Enter Key:") || void 0);
|
|
92
|
-
}
|
|
93
|
-
const { solution: a } = await d({
|
|
94
|
-
obfuscated: E,
|
|
95
|
-
key: o,
|
|
96
|
-
maxnumber: p
|
|
97
|
-
});
|
|
98
|
-
a && "clearText" in a ? (this.#i(a.clearText), c(s.VERIFIED), this.context.dispatch("cleartext", a.clearText), m && t && (t.style.display = "none")) : c(s.ERROR, "Unable to decrypt data.");
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Handles the button click event, triggering the clarification process.
|
|
102
|
-
*
|
|
103
|
-
* @param {Event} ev - The click event.
|
|
104
|
-
*/
|
|
105
|
-
#e(t) {
|
|
106
|
-
t.preventDefault();
|
|
107
|
-
const { auto: i } = this.context.getConfiguration();
|
|
108
|
-
i === "off" || this.clarify();
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Renders the clear text data by creating an appropriate element (e.g., a link or text node).
|
|
112
|
-
*
|
|
113
|
-
* @param {string} clearText - The decrypted clear text data to render.
|
|
114
|
-
*/
|
|
115
|
-
#i(t) {
|
|
116
|
-
const i = t.match(/^(mailto|tel|sms|https?):/);
|
|
117
|
-
let n;
|
|
118
|
-
if (i) {
|
|
119
|
-
const [r] = t.slice(t.indexOf(":") + 1).replace(/^\/\//, "").split("?");
|
|
120
|
-
n = document.createElement("a"), n.href = t, n.innerHTML = r;
|
|
121
|
-
} else
|
|
122
|
-
n = document.createTextNode(t);
|
|
123
|
-
this.elButton && n && (this.elButton.after(n), this.elButton.parentElement?.removeChild(this.elButton));
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
h.register(R);
|
|
127
|
-
export {
|
|
128
|
-
R as PluginObfuscation
|
|
129
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
(function(o,s){typeof exports=="object"&&typeof module<"u"?s(exports):typeof define=="function"&&define.amd?define(["exports"],s):(o=typeof globalThis<"u"?globalThis:o||self,s(o["[name]"]={}))})(this,function(o){"use strict";class s{constructor(t){this.context=t}static pluginName;static register(t){typeof globalThis.altchaPlugins!="object"&&(globalThis.altchaPlugins=[]),globalThis.altchaPlugins.includes(t)||globalThis.altchaPlugins.push(t)}destroy(){}onErrorChange(t){}onStateChange(t){}}var a=(e=>(e.CODE="code",e.ERROR="error",e.VERIFIED="verified",e.VERIFYING="verifying",e.UNVERIFIED="unverified",e.EXPIRED="expired",e))(a||{});class h extends s{static pluginName="obfuscation";elButton;#t=this.#e.bind(this);constructor(t){super(t);const i=t.el;this.elButton=i.parentElement?.querySelector("[data-clarify-button]")||i.parentElement?.querySelector("button, a"),this.elButton&&this.elButton.addEventListener("click",this.#t)}destroy(){this.elButton&&this.elButton.removeEventListener("click",this.#t)}async clarify(){const{el:t,getConfiguration:i,getFloatingAnchor:n,setFloatingAnchor:u,reset:p,solve:m,setState:f}=this.context,{delay:g,floating:E,maxnumber:y,obfuscated:d}=i();if(this.elButton&&!n()&&u(this.elButton),!d){f(a.ERROR);return}p(a.VERIFYING),await new Promise(c=>setTimeout(c,g||0));const[b,R]=d.split("?");let l=new URLSearchParams(R||"").get("key")||void 0;if(l){const c=l.match(/^\(prompt:?(.*)\)$/);c&&(l=prompt(c[1]||"Enter Key:")||void 0)}const{solution:r}=await m({obfuscated:b,key:l,maxnumber:y});r&&"clearText"in r?(this.#i(r.clearText),f(a.VERIFIED),this.context.dispatch("cleartext",r.clearText),E&&t&&(t.style.display="none")):f(a.ERROR,"Unable to decrypt data.")}#e(t){t.preventDefault();const{auto:i}=this.context.getConfiguration();i==="off"||this.clarify()}#i(t){const i=t.match(/^(mailto|tel|sms|https?):/);let n;if(i){const[u]=t.slice(t.indexOf(":")+1).replace(/^\/\//,"").split("?");n=document.createElement("a"),n.href=t,n.innerHTML=u}else n=document.createTextNode(t);this.elButton&&n&&(this.elButton.after(n),this.elButton.parentElement?.removeChild(this.elButton))}}s.register(h),o.PluginObfuscation=h,Object.defineProperty(o,Symbol.toStringTag,{value:"Module"})});
|
package/dist_plugins/upload.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
declare module 'altcha/upload';
|
package/dist_plugins/upload.js
DELETED
|
@@ -1,503 +0,0 @@
|
|
|
1
|
-
const u = {
|
|
2
|
-
generateKey: v,
|
|
3
|
-
exportKey: S,
|
|
4
|
-
importKey: x,
|
|
5
|
-
decrypt: C,
|
|
6
|
-
encrypt: L
|
|
7
|
-
};
|
|
8
|
-
async function v(n = 256) {
|
|
9
|
-
return crypto.subtle.generateKey({
|
|
10
|
-
name: "AES-GCM",
|
|
11
|
-
length: n
|
|
12
|
-
}, !0, ["encrypt", "decrypt"]);
|
|
13
|
-
}
|
|
14
|
-
async function S(n) {
|
|
15
|
-
return new Uint8Array(await crypto.subtle.exportKey("raw", n));
|
|
16
|
-
}
|
|
17
|
-
async function x(n) {
|
|
18
|
-
return crypto.subtle.importKey("raw", n, {
|
|
19
|
-
name: "AES-GCM"
|
|
20
|
-
}, !0, ["encrypt", "decrypt"]);
|
|
21
|
-
}
|
|
22
|
-
async function L(n, e, t = 16) {
|
|
23
|
-
const r = crypto.getRandomValues(new Uint8Array(t));
|
|
24
|
-
return {
|
|
25
|
-
encrypted: new Uint8Array(await crypto.subtle.encrypt({
|
|
26
|
-
name: "AES-GCM",
|
|
27
|
-
iv: r
|
|
28
|
-
}, n, e)),
|
|
29
|
-
iv: r
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
async function C(n, e, t) {
|
|
33
|
-
return new Uint8Array(await crypto.subtle.decrypt({
|
|
34
|
-
name: "AES-GCM",
|
|
35
|
-
iv: t
|
|
36
|
-
}, n, e));
|
|
37
|
-
}
|
|
38
|
-
function I(n, e = !1) {
|
|
39
|
-
return e && (n = n.replace(/_/g, "/").replace(/-/g, "+") + "=".repeat(3 - (3 + n.length) % 4)), Uint8Array.from(atob(n), (t) => t.charCodeAt(0));
|
|
40
|
-
}
|
|
41
|
-
function h(n, e = !1) {
|
|
42
|
-
const t = btoa(String.fromCharCode(...n));
|
|
43
|
-
return e ? t.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "") : t;
|
|
44
|
-
}
|
|
45
|
-
function f(n, e = 80) {
|
|
46
|
-
let t = "";
|
|
47
|
-
for (; n.length > 0; )
|
|
48
|
-
t += n.slice(0, e) + `
|
|
49
|
-
`, n = n.slice(e);
|
|
50
|
-
return t;
|
|
51
|
-
}
|
|
52
|
-
function g(n) {
|
|
53
|
-
return I(n.split(/\r?\n/).filter((e) => !e.startsWith("-----")).join(""));
|
|
54
|
-
}
|
|
55
|
-
const l = "RSA-OAEP", p = "SHA-256", N = 2048, z = new Uint8Array([1, 0, 1]), T = {
|
|
56
|
-
generateKeyPair: _,
|
|
57
|
-
encrypt: R,
|
|
58
|
-
decrypt: j,
|
|
59
|
-
exportPrivateKey: m,
|
|
60
|
-
exportPrivateKeyPem: H,
|
|
61
|
-
exportPublicKey: d,
|
|
62
|
-
exportPublicKeyPem: k,
|
|
63
|
-
exportPublicKeyFromPrivateKey: O,
|
|
64
|
-
importPrivateKey: E,
|
|
65
|
-
importPrivateKeyPem: q,
|
|
66
|
-
importPublicKey: w,
|
|
67
|
-
importPublicKeyPem: b
|
|
68
|
-
};
|
|
69
|
-
async function _() {
|
|
70
|
-
return crypto.subtle.generateKey({
|
|
71
|
-
name: l,
|
|
72
|
-
modulusLength: N,
|
|
73
|
-
publicExponent: z,
|
|
74
|
-
hash: p
|
|
75
|
-
}, !0, ["encrypt", "decrypt"]);
|
|
76
|
-
}
|
|
77
|
-
async function R(n, e) {
|
|
78
|
-
return new Uint8Array(await crypto.subtle.encrypt({
|
|
79
|
-
name: l
|
|
80
|
-
}, n, e));
|
|
81
|
-
}
|
|
82
|
-
async function j(n, e) {
|
|
83
|
-
return new Uint8Array(await crypto.subtle.decrypt({
|
|
84
|
-
name: l
|
|
85
|
-
}, n, e));
|
|
86
|
-
}
|
|
87
|
-
async function d(n) {
|
|
88
|
-
return new Uint8Array(await crypto.subtle.exportKey("spki", n));
|
|
89
|
-
}
|
|
90
|
-
async function m(n) {
|
|
91
|
-
return new Uint8Array(await crypto.subtle.exportKey("pkcs8", n));
|
|
92
|
-
}
|
|
93
|
-
async function k(n) {
|
|
94
|
-
return `-----BEGIN PUBLIC KEY-----
|
|
95
|
-
` + f(h(await d(n)), 64) + "-----END PUBLIC KEY-----";
|
|
96
|
-
}
|
|
97
|
-
async function H(n) {
|
|
98
|
-
return `-----BEGIN PRIVATE KEY-----
|
|
99
|
-
` + f(h(await m(n)), 64) + "-----END PRIVATE KEY-----";
|
|
100
|
-
}
|
|
101
|
-
async function w(n) {
|
|
102
|
-
return crypto.subtle.importKey("spki", n, {
|
|
103
|
-
name: l,
|
|
104
|
-
hash: p
|
|
105
|
-
}, !0, ["encrypt"]);
|
|
106
|
-
}
|
|
107
|
-
async function b(n) {
|
|
108
|
-
return w(g(n));
|
|
109
|
-
}
|
|
110
|
-
async function E(n) {
|
|
111
|
-
return crypto.subtle.importKey("pkcs8", n, {
|
|
112
|
-
name: l,
|
|
113
|
-
hash: p
|
|
114
|
-
}, !0, ["decrypt"]);
|
|
115
|
-
}
|
|
116
|
-
async function q(n) {
|
|
117
|
-
return E(g(n));
|
|
118
|
-
}
|
|
119
|
-
async function O(n) {
|
|
120
|
-
const e = await crypto.subtle.exportKey("jwk", n);
|
|
121
|
-
delete e.d, delete e.dp, delete e.dq, delete e.q, delete e.qi, e.key_ops = ["encrypt"];
|
|
122
|
-
const t = await crypto.subtle.importKey("jwk", e, {
|
|
123
|
-
name: l,
|
|
124
|
-
hash: p
|
|
125
|
-
}, !0, ["encrypt"]);
|
|
126
|
-
return d(t);
|
|
127
|
-
}
|
|
128
|
-
const $ = new Uint8Array([1, 0, 1]), B = 256, G = 16;
|
|
129
|
-
async function M(n, e, t = {}) {
|
|
130
|
-
const { aesIVLength: r = G, aesKeyLength: i = B } = t, s = await u.generateKey(i), { encrypted: o, iv: a } = await u.encrypt(s, e, r), c = await T.encrypt(n, await u.exportKey(s));
|
|
131
|
-
return new Uint8Array([
|
|
132
|
-
...$,
|
|
133
|
-
...new Uint8Array([c.length]),
|
|
134
|
-
...new Uint8Array([a.length]),
|
|
135
|
-
...c,
|
|
136
|
-
...a,
|
|
137
|
-
...o
|
|
138
|
-
]);
|
|
139
|
-
}
|
|
140
|
-
class U {
|
|
141
|
-
/**
|
|
142
|
-
* Constructs a new instance of the Plugin.
|
|
143
|
-
*
|
|
144
|
-
* @param {PluginContext} context - The context provided to the plugin, containing necessary configurations and dependencies.
|
|
145
|
-
*/
|
|
146
|
-
constructor(e) {
|
|
147
|
-
this.context = e;
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* A distinct name of the plugin. Every plugin must have it's own name.
|
|
151
|
-
*/
|
|
152
|
-
static pluginName;
|
|
153
|
-
/**
|
|
154
|
-
* Registers a plugin class in the global `altchaPlugins` array.
|
|
155
|
-
* Ensures the plugin is added only once.
|
|
156
|
-
*
|
|
157
|
-
* @param {new(context: PluginContext) => Plugin} cls - The plugin class to register.
|
|
158
|
-
*/
|
|
159
|
-
static register(e) {
|
|
160
|
-
typeof globalThis.altchaPlugins != "object" && (globalThis.altchaPlugins = []), globalThis.altchaPlugins.includes(e) || globalThis.altchaPlugins.push(e);
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Clean up resources when the plugin is destroyed.
|
|
164
|
-
* Override this method in subclasses to implement custom destruction logic.
|
|
165
|
-
*/
|
|
166
|
-
destroy() {
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Callback triggered when an error changes.
|
|
170
|
-
* Override this method in subclasses to handle error state changes.
|
|
171
|
-
*
|
|
172
|
-
* @param {string | null} err - The error message or `null` if there's no error.
|
|
173
|
-
*/
|
|
174
|
-
onErrorChange(e) {
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Callback triggered when the plugin state changes.
|
|
178
|
-
* Override this method in subclasses to handle state changes.
|
|
179
|
-
*
|
|
180
|
-
* @param {State} state - The new state of the plugin.
|
|
181
|
-
*/
|
|
182
|
-
onStateChange(e) {
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
class Y extends U {
|
|
186
|
-
static pluginName = "upload";
|
|
187
|
-
pendingFiles = [];
|
|
188
|
-
uploadHandles = [];
|
|
189
|
-
elForm;
|
|
190
|
-
#e = this.#c.bind(this);
|
|
191
|
-
#t = this.#l.bind(this);
|
|
192
|
-
/**
|
|
193
|
-
* Constructor initializes the plugin, setting up event listeners on the form.
|
|
194
|
-
*
|
|
195
|
-
* @param {PluginContext} context - Plugin context providing access to the element, configuration, and utility methods.
|
|
196
|
-
*/
|
|
197
|
-
constructor(e) {
|
|
198
|
-
super(e), this.elForm = this.context.el.closest("form"), this.elForm && (this.elForm.addEventListener("change", this.#e), this.elForm.addEventListener("submit", this.#t, {
|
|
199
|
-
capture: !0
|
|
200
|
-
}));
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Adds a file to the pending files list for upload.
|
|
204
|
-
*
|
|
205
|
-
* @param {string} fieldName - The field name associated with the file input.
|
|
206
|
-
* @param {File} file - The file to be uploaded.
|
|
207
|
-
*/
|
|
208
|
-
addFile(e, t) {
|
|
209
|
-
this.pendingFiles.find(([r, i]) => r === e && i === t) || this.pendingFiles.push([e, t]);
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Cleans up event listeners and other resources when the plugin is destroyed.
|
|
213
|
-
*/
|
|
214
|
-
destroy() {
|
|
215
|
-
this.elForm && (this.elForm.removeEventListener("change", this.#e), this.elForm.removeEventListener("submit", this.#t));
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* Uploads all pending files in the list.
|
|
219
|
-
*/
|
|
220
|
-
async uploadPendingFiles() {
|
|
221
|
-
const e = async () => {
|
|
222
|
-
const t = this.pendingFiles[0];
|
|
223
|
-
if (t && await this.#r(this.#s(t)), this.pendingFiles.length)
|
|
224
|
-
return e();
|
|
225
|
-
};
|
|
226
|
-
try {
|
|
227
|
-
await e();
|
|
228
|
-
} catch (t) {
|
|
229
|
-
return this.context.log("upload failed", t), this.context.dispatch("uploaderror", {
|
|
230
|
-
error: t
|
|
231
|
-
}), !1;
|
|
232
|
-
}
|
|
233
|
-
this.pendingFiles.length === 0 && (this.#i(), this.elForm?.requestSubmit());
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* Adds hidden input fields to the form containing the file IDs of uploaded files.
|
|
237
|
-
*/
|
|
238
|
-
#i() {
|
|
239
|
-
const e = this.uploadHandles.reduce(
|
|
240
|
-
(t, r) => (t[r.fieldName] || (t[r.fieldName] = []), r.fileId && t[r.fieldName].push(r.fileId), t),
|
|
241
|
-
{}
|
|
242
|
-
);
|
|
243
|
-
for (const t in e) {
|
|
244
|
-
const r = document.createElement("input");
|
|
245
|
-
r.name = t, r.type = "hidden", r.value = e[t].join(","), this.elForm?.querySelector(`[name="${t}"]`)?.setAttribute("disabled", "disabled"), this.elForm?.appendChild(r);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* Creates an upload handle for the specified pending file.
|
|
250
|
-
*
|
|
251
|
-
* @param {[string, File]} pendingFile - The field name and file to be uploaded.
|
|
252
|
-
* @returns {UploadHandle} The created upload handle.
|
|
253
|
-
* @throws Will throw an error if the upload handle cannot be created.
|
|
254
|
-
*/
|
|
255
|
-
#s(e) {
|
|
256
|
-
const t = this.pendingFiles.findIndex(
|
|
257
|
-
([i, s]) => i === e[0] && s === e[1]
|
|
258
|
-
);
|
|
259
|
-
if (t < 0)
|
|
260
|
-
throw new Error("Cannot create upload handle.");
|
|
261
|
-
const r = new D(e[0], e[1]);
|
|
262
|
-
return this.uploadHandles.push(r), this.pendingFiles.splice(t, 1), this.#o(r), this.#n(), r;
|
|
263
|
-
}
|
|
264
|
-
/**
|
|
265
|
-
* Dispatches a custom event when a file upload starts.
|
|
266
|
-
*
|
|
267
|
-
* @param {UploadHandle} handle - The upload handle associated with the file upload.
|
|
268
|
-
*/
|
|
269
|
-
#o(e) {
|
|
270
|
-
this.context.dispatch("upload", { handle: e });
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Dispatches a custom event to track the progress of ongoing file uploads.
|
|
274
|
-
*/
|
|
275
|
-
#n() {
|
|
276
|
-
const e = this.pendingFiles.reduce((r, [i, s]) => r + s.size, 0) + this.uploadHandles.reduce((r, { uploadSize: i }) => r + i, 0), t = this.uploadHandles.reduce(
|
|
277
|
-
(r, { loaded: i }) => r + i,
|
|
278
|
-
0
|
|
279
|
-
);
|
|
280
|
-
this.context.dispatch("uploadprogress", {
|
|
281
|
-
bytesLoaded: t,
|
|
282
|
-
bytesTotal: e,
|
|
283
|
-
pendingFiles: this.pendingFiles,
|
|
284
|
-
uploadHandles: this.uploadHandles
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Retrieves the upload URL from the form's attributes.
|
|
289
|
-
*
|
|
290
|
-
* @returns {string | null} The upload URL, or null if not found.
|
|
291
|
-
*/
|
|
292
|
-
#a() {
|
|
293
|
-
if (this.elForm) {
|
|
294
|
-
const e = this.elForm.getAttribute("action"), t = this.elForm.getAttribute("data-upload-url");
|
|
295
|
-
if (t)
|
|
296
|
-
return t;
|
|
297
|
-
const r = new URL(e || location.origin);
|
|
298
|
-
return r.pathname = r.pathname + "/file", r.toString();
|
|
299
|
-
}
|
|
300
|
-
return null;
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* Handles the form's change event, adding files to the pending files list.
|
|
304
|
-
*
|
|
305
|
-
* @param {Event} ev - The change event.
|
|
306
|
-
*/
|
|
307
|
-
#c(e) {
|
|
308
|
-
const t = e.target;
|
|
309
|
-
if (t && t.type === "file") {
|
|
310
|
-
const r = t.files;
|
|
311
|
-
if (r?.length)
|
|
312
|
-
for (const i of r)
|
|
313
|
-
this.addFile(t.name, i);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* Handles the form's submit event, preventing submission until all pending files are uploaded.
|
|
318
|
-
*
|
|
319
|
-
* @param {SubmitEvent} ev - The submit event.
|
|
320
|
-
*/
|
|
321
|
-
#l(e) {
|
|
322
|
-
e.target?.hasAttribute(
|
|
323
|
-
"data-code-challenge-form"
|
|
324
|
-
) || this.pendingFiles.length && (e.preventDefault(), e.stopPropagation(), this.uploadPendingFiles());
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* Uploads a single file, handling encryption and ALTCHA challenges if necessary.
|
|
328
|
-
*
|
|
329
|
-
* @param {UploadHandle} handle - The upload handle associated with the file.
|
|
330
|
-
* @param {string} [altchaPayload] - The ALTCHA payload, if available.
|
|
331
|
-
* @returns {Promise<unknown>} A promise that resolves when the upload is complete.
|
|
332
|
-
* @throws Will throw an error if the upload fails or if an ALTCHA challenge cannot be solved.
|
|
333
|
-
*/
|
|
334
|
-
async #r(e, t) {
|
|
335
|
-
const r = this.#a();
|
|
336
|
-
if (!r)
|
|
337
|
-
throw new Error("Upload url not specified.");
|
|
338
|
-
const i = {
|
|
339
|
-
"content-type": "application/json"
|
|
340
|
-
};
|
|
341
|
-
t && (i.authorization = "Altcha payload=" + t);
|
|
342
|
-
const s = await fetch(r, {
|
|
343
|
-
body: JSON.stringify({
|
|
344
|
-
name: e.file.name || "file",
|
|
345
|
-
size: e.file.size,
|
|
346
|
-
type: e.file.type || "application/octet-stream"
|
|
347
|
-
}),
|
|
348
|
-
credentials: "include",
|
|
349
|
-
headers: i,
|
|
350
|
-
method: "POST"
|
|
351
|
-
});
|
|
352
|
-
if (s.status === 401)
|
|
353
|
-
return this.#p(s, e);
|
|
354
|
-
if (s.status !== 200)
|
|
355
|
-
throw new Error(`Unexpected server response ${s.status}.`);
|
|
356
|
-
const o = await s.json();
|
|
357
|
-
let a = e.file;
|
|
358
|
-
if (o.encrypted && o.encryptionPublicKey) {
|
|
359
|
-
const c = await b(o.encryptionPublicKey), P = await new Response(
|
|
360
|
-
new ReadableStream({
|
|
361
|
-
async start(y) {
|
|
362
|
-
const A = e.file.stream().getReader();
|
|
363
|
-
for (; ; ) {
|
|
364
|
-
const { done: F, value: K } = await A.read();
|
|
365
|
-
if (F)
|
|
366
|
-
break;
|
|
367
|
-
y.enqueue(K);
|
|
368
|
-
}
|
|
369
|
-
y.close();
|
|
370
|
-
}
|
|
371
|
-
})
|
|
372
|
-
).arrayBuffer();
|
|
373
|
-
a = await M(c, new Uint8Array(P));
|
|
374
|
-
}
|
|
375
|
-
return e.uploadSize = a instanceof Uint8Array ? a.byteLength : e.file.size, await this.#d(o.uploadUrl, e, a, {
|
|
376
|
-
"content-type": e.file.type || "application/octet-stream"
|
|
377
|
-
}), o.finalizeUrl && await this.#u(o.finalizeUrl, e.uploadSize), e.fileId = o.fileId, e.resolve({
|
|
378
|
-
encrypted: o.encrypted,
|
|
379
|
-
fileId: o.fileId
|
|
380
|
-
}), e.promise;
|
|
381
|
-
}
|
|
382
|
-
/**
|
|
383
|
-
* Handles ALTCHA challenges during file upload, solving the challenge and retrying the upload.
|
|
384
|
-
*
|
|
385
|
-
* @param {Response} resp - The response from the server containing the ALTCHA challenge.
|
|
386
|
-
* @param {UploadHandle} handle - The upload handle associated with the file.
|
|
387
|
-
* @returns {Promise<unknown>} A promise that resolves when the challenge is solved and the upload is complete.
|
|
388
|
-
* @throws Will throw an error if the challenge cannot be solved.
|
|
389
|
-
*/
|
|
390
|
-
async #p(e, t) {
|
|
391
|
-
try {
|
|
392
|
-
const i = e.headers.get("www-authenticate")?.match(/challenge=(.*),/)?.[1];
|
|
393
|
-
if (!i)
|
|
394
|
-
throw new Error(
|
|
395
|
-
"Unable to retrieve altcha challenge from www-authenticate header."
|
|
396
|
-
);
|
|
397
|
-
const s = JSON.parse(i);
|
|
398
|
-
if (s && "challenge" in s) {
|
|
399
|
-
const { solution: o } = await this.context.solve(s);
|
|
400
|
-
if (o && "number" in o)
|
|
401
|
-
return this.#r(
|
|
402
|
-
t,
|
|
403
|
-
btoa(
|
|
404
|
-
JSON.stringify({
|
|
405
|
-
...s,
|
|
406
|
-
number: o.number
|
|
407
|
-
})
|
|
408
|
-
)
|
|
409
|
-
);
|
|
410
|
-
throw new Error("Invalid challenge solution.");
|
|
411
|
-
}
|
|
412
|
-
} catch (r) {
|
|
413
|
-
throw this.context.log(r), new Error("Unable to solve altcha challenge for upload.");
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
/**
|
|
417
|
-
* Finalizes the file upload by sending a request to the server.
|
|
418
|
-
*
|
|
419
|
-
* @param {string} finalizeUrl - The URL to finalize the upload.
|
|
420
|
-
* @param {number} uploadSize - The size of the uploaded file.
|
|
421
|
-
*/
|
|
422
|
-
async #u(e, t) {
|
|
423
|
-
const r = await fetch(e, {
|
|
424
|
-
body: JSON.stringify({
|
|
425
|
-
uploadedBytes: t
|
|
426
|
-
}),
|
|
427
|
-
headers: {
|
|
428
|
-
"content-type": "application/json"
|
|
429
|
-
},
|
|
430
|
-
method: "POST"
|
|
431
|
-
});
|
|
432
|
-
if (r.status > 204)
|
|
433
|
-
throw new Error(`Unexpected server response ${r.status}.`);
|
|
434
|
-
return !0;
|
|
435
|
-
}
|
|
436
|
-
/**
|
|
437
|
-
* Uploads the file's contents to the server using the PUT method.
|
|
438
|
-
*
|
|
439
|
-
* @param {string} url - The URL to upload the file to.
|
|
440
|
-
* @param {UploadHandle} handle - The upload handle associated with the file.
|
|
441
|
-
* @param {Uint8Array | File} body - The file's contents.
|
|
442
|
-
* @param {Record<string, string>} headers - Additional headers for the upload request.
|
|
443
|
-
*/
|
|
444
|
-
async #d(e, t, r, i = {}) {
|
|
445
|
-
return e = new URL(
|
|
446
|
-
e,
|
|
447
|
-
this.elForm?.getAttribute("action") || location.origin
|
|
448
|
-
).toString(), new Promise((s, o) => {
|
|
449
|
-
const a = new XMLHttpRequest();
|
|
450
|
-
t.controller.signal.addEventListener("abort", () => {
|
|
451
|
-
a.abort();
|
|
452
|
-
}), a.upload.addEventListener("progress", (c) => {
|
|
453
|
-
t.setProgress(c.loaded), this.#n();
|
|
454
|
-
}), a.addEventListener("error", (c) => {
|
|
455
|
-
o(new Error("Upload failed."));
|
|
456
|
-
}), a.addEventListener("load", () => {
|
|
457
|
-
a.status >= 400 ? o(new Error(`Server responded with ${a.status}`)) : s(void 0);
|
|
458
|
-
}), a.open("PUT", e);
|
|
459
|
-
for (const c in i)
|
|
460
|
-
a.setRequestHeader(c, i[c]);
|
|
461
|
-
a.send(r);
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
class D {
|
|
466
|
-
/**
|
|
467
|
-
* Creates an instance of UploadHandle.
|
|
468
|
-
*
|
|
469
|
-
* @param {string} fieldName - The name of the field associated with the file upload.
|
|
470
|
-
* @param {File} file - The file to be uploaded.
|
|
471
|
-
*/
|
|
472
|
-
constructor(e, t) {
|
|
473
|
-
this.fieldName = e, this.file = t, this.uploadSize = this.file.size, this.promise = new Promise((r, i) => {
|
|
474
|
-
this.resolve = r, this.reject = i;
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
controller = new AbortController();
|
|
478
|
-
promise;
|
|
479
|
-
fileId;
|
|
480
|
-
loaded = 0;
|
|
481
|
-
progress = 0;
|
|
482
|
-
uploadSize = 0;
|
|
483
|
-
resolve;
|
|
484
|
-
reject;
|
|
485
|
-
/**
|
|
486
|
-
* Aborts the file upload by invoking the AbortController's abort method.
|
|
487
|
-
*/
|
|
488
|
-
abort() {
|
|
489
|
-
this.controller.abort();
|
|
490
|
-
}
|
|
491
|
-
/**
|
|
492
|
-
* Updates the progress of the file upload.
|
|
493
|
-
*
|
|
494
|
-
* @param {number} loaded - The number of bytes that have been uploaded.
|
|
495
|
-
*/
|
|
496
|
-
setProgress(e) {
|
|
497
|
-
this.loaded = e, this.progress = this.file.size && e ? Math.min(1, e / this.file.size) : 0;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
U.register(Y);
|
|
501
|
-
export {
|
|
502
|
-
Y as PluginUpload
|
|
503
|
-
};
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
(function(p,l){typeof exports=="object"&&typeof module<"u"?l(exports):typeof define=="function"&&define.amd?define(["exports"],l):(p=typeof globalThis<"u"?globalThis:p||self,l(p["[name]"]={}))})(this,function(p){"use strict";const l={generateKey:F,exportKey:K,importKey:v,decrypt:x,encrypt:S};async function F(n=256){return crypto.subtle.generateKey({name:"AES-GCM",length:n},!0,["encrypt","decrypt"])}async function K(n){return new Uint8Array(await crypto.subtle.exportKey("raw",n))}async function v(n){return crypto.subtle.importKey("raw",n,{name:"AES-GCM"},!0,["encrypt","decrypt"])}async function S(n,e,t=16){const r=crypto.getRandomValues(new Uint8Array(t));return{encrypted:new Uint8Array(await crypto.subtle.encrypt({name:"AES-GCM",iv:r},n,e)),iv:r}}async function x(n,e,t){return new Uint8Array(await crypto.subtle.decrypt({name:"AES-GCM",iv:t},n,e))}function L(n,e=!1){return e&&(n=n.replace(/_/g,"/").replace(/-/g,"+")+"=".repeat(3-(3+n.length)%4)),Uint8Array.from(atob(n),t=>t.charCodeAt(0))}function h(n,e=!1){const t=btoa(String.fromCharCode(...n));return e?t.replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,""):t}function f(n,e=80){let t="";for(;n.length>0;)t+=n.slice(0,e)+`
|
|
2
|
-
`,n=n.slice(e);return t}function m(n){return L(n.split(/\r?\n/).filter(e=>!e.startsWith("-----")).join(""))}const u="RSA-OAEP",d="SHA-256",C=2048,I=new Uint8Array([1,0,1]),T={generateKeyPair:N,encrypt:z,decrypt:j,exportPrivateKey:g,exportPrivateKeyPem:R,exportPublicKey:y,exportPublicKeyPem:_,exportPublicKeyFromPrivateKey:H,importPrivateKey:E,importPrivateKeyPem:k,importPublicKey:w,importPublicKeyPem:b};async function N(){return crypto.subtle.generateKey({name:u,modulusLength:C,publicExponent:I,hash:d},!0,["encrypt","decrypt"])}async function z(n,e){return new Uint8Array(await crypto.subtle.encrypt({name:u},n,e))}async function j(n,e){return new Uint8Array(await crypto.subtle.decrypt({name:u},n,e))}async function y(n){return new Uint8Array(await crypto.subtle.exportKey("spki",n))}async function g(n){return new Uint8Array(await crypto.subtle.exportKey("pkcs8",n))}async function _(n){return`-----BEGIN PUBLIC KEY-----
|
|
3
|
-
`+f(h(await y(n)),64)+"-----END PUBLIC KEY-----"}async function R(n){return`-----BEGIN PRIVATE KEY-----
|
|
4
|
-
`+f(h(await g(n)),64)+"-----END PRIVATE KEY-----"}async function w(n){return crypto.subtle.importKey("spki",n,{name:u,hash:d},!0,["encrypt"])}async function b(n){return w(m(n))}async function E(n){return crypto.subtle.importKey("pkcs8",n,{name:u,hash:d},!0,["decrypt"])}async function k(n){return E(m(n))}async function H(n){const e=await crypto.subtle.exportKey("jwk",n);delete e.d,delete e.dp,delete e.dq,delete e.q,delete e.qi,e.key_ops=["encrypt"];const t=await crypto.subtle.importKey("jwk",e,{name:u,hash:d},!0,["encrypt"]);return y(t)}const O=new Uint8Array([1,0,1]),q=256,M=16;async function $(n,e,t={}){const{aesIVLength:r=M,aesKeyLength:i=q}=t,s=await l.generateKey(i),{encrypted:o,iv:a}=await l.encrypt(s,e,r),c=await T.encrypt(n,await l.exportKey(s));return new Uint8Array([...O,...new Uint8Array([c.length]),...new Uint8Array([a.length]),...c,...a,...o])}class P{constructor(e){this.context=e}static pluginName;static register(e){typeof globalThis.altchaPlugins!="object"&&(globalThis.altchaPlugins=[]),globalThis.altchaPlugins.includes(e)||globalThis.altchaPlugins.push(e)}destroy(){}onErrorChange(e){}onStateChange(e){}}class U extends P{static pluginName="upload";pendingFiles=[];uploadHandles=[];elForm;#e=this.#c.bind(this);#t=this.#l.bind(this);constructor(e){super(e),this.elForm=this.context.el.closest("form"),this.elForm&&(this.elForm.addEventListener("change",this.#e),this.elForm.addEventListener("submit",this.#t,{capture:!0}))}addFile(e,t){this.pendingFiles.find(([r,i])=>r===e&&i===t)||this.pendingFiles.push([e,t])}destroy(){this.elForm&&(this.elForm.removeEventListener("change",this.#e),this.elForm.removeEventListener("submit",this.#t))}async uploadPendingFiles(){const e=async()=>{const t=this.pendingFiles[0];if(t&&await this.#r(this.#s(t)),this.pendingFiles.length)return e()};try{await e()}catch(t){return this.context.log("upload failed",t),this.context.dispatch("uploaderror",{error:t}),!1}this.pendingFiles.length===0&&(this.#i(),this.elForm?.requestSubmit())}#i(){const e=this.uploadHandles.reduce((t,r)=>(t[r.fieldName]||(t[r.fieldName]=[]),r.fileId&&t[r.fieldName].push(r.fileId),t),{});for(const t in e){const r=document.createElement("input");r.name=t,r.type="hidden",r.value=e[t].join(","),this.elForm?.querySelector(`[name="${t}"]`)?.setAttribute("disabled","disabled"),this.elForm?.appendChild(r)}}#s(e){const t=this.pendingFiles.findIndex(([i,s])=>i===e[0]&&s===e[1]);if(t<0)throw new Error("Cannot create upload handle.");const r=new B(e[0],e[1]);return this.uploadHandles.push(r),this.pendingFiles.splice(t,1),this.#o(r),this.#n(),r}#o(e){this.context.dispatch("upload",{handle:e})}#n(){const e=this.pendingFiles.reduce((r,[i,s])=>r+s.size,0)+this.uploadHandles.reduce((r,{uploadSize:i})=>r+i,0),t=this.uploadHandles.reduce((r,{loaded:i})=>r+i,0);this.context.dispatch("uploadprogress",{bytesLoaded:t,bytesTotal:e,pendingFiles:this.pendingFiles,uploadHandles:this.uploadHandles})}#a(){if(this.elForm){const e=this.elForm.getAttribute("action"),t=this.elForm.getAttribute("data-upload-url");if(t)return t;const r=new URL(e||location.origin);return r.pathname=r.pathname+"/file",r.toString()}return null}#c(e){const t=e.target;if(t&&t.type==="file"){const r=t.files;if(r?.length)for(const i of r)this.addFile(t.name,i)}}#l(e){e.target?.hasAttribute("data-code-challenge-form")||this.pendingFiles.length&&(e.preventDefault(),e.stopPropagation(),this.uploadPendingFiles())}async#r(e,t){const r=this.#a();if(!r)throw new Error("Upload url not specified.");const i={"content-type":"application/json"};t&&(i.authorization="Altcha payload="+t);const s=await fetch(r,{body:JSON.stringify({name:e.file.name||"file",size:e.file.size,type:e.file.type||"application/octet-stream"}),credentials:"include",headers:i,method:"POST"});if(s.status===401)return this.#p(s,e);if(s.status!==200)throw new Error(`Unexpected server response ${s.status}.`);const o=await s.json();let a=e.file;if(o.encrypted&&o.encryptionPublicKey){const c=await b(o.encryptionPublicKey),G=await new Response(new ReadableStream({async start(A){const Y=e.file.stream().getReader();for(;;){const{done:D,value:V}=await Y.read();if(D)break;A.enqueue(V)}A.close()}})).arrayBuffer();a=await $(c,new Uint8Array(G))}return e.uploadSize=a instanceof Uint8Array?a.byteLength:e.file.size,await this.#d(o.uploadUrl,e,a,{"content-type":e.file.type||"application/octet-stream"}),o.finalizeUrl&&await this.#u(o.finalizeUrl,e.uploadSize),e.fileId=o.fileId,e.resolve({encrypted:o.encrypted,fileId:o.fileId}),e.promise}async#p(e,t){try{const i=e.headers.get("www-authenticate")?.match(/challenge=(.*),/)?.[1];if(!i)throw new Error("Unable to retrieve altcha challenge from www-authenticate header.");const s=JSON.parse(i);if(s&&"challenge"in s){const{solution:o}=await this.context.solve(s);if(o&&"number"in o)return this.#r(t,btoa(JSON.stringify({...s,number:o.number})));throw new Error("Invalid challenge solution.")}}catch(r){throw this.context.log(r),new Error("Unable to solve altcha challenge for upload.")}}async#u(e,t){const r=await fetch(e,{body:JSON.stringify({uploadedBytes:t}),headers:{"content-type":"application/json"},method:"POST"});if(r.status>204)throw new Error(`Unexpected server response ${r.status}.`);return!0}async#d(e,t,r,i={}){return e=new URL(e,this.elForm?.getAttribute("action")||location.origin).toString(),new Promise((s,o)=>{const a=new XMLHttpRequest;t.controller.signal.addEventListener("abort",()=>{a.abort()}),a.upload.addEventListener("progress",c=>{t.setProgress(c.loaded),this.#n()}),a.addEventListener("error",c=>{o(new Error("Upload failed."))}),a.addEventListener("load",()=>{a.status>=400?o(new Error(`Server responded with ${a.status}`)):s(void 0)}),a.open("PUT",e);for(const c in i)a.setRequestHeader(c,i[c]);a.send(r)})}}class B{constructor(e,t){this.fieldName=e,this.file=t,this.uploadSize=this.file.size,this.promise=new Promise((r,i)=>{this.resolve=r,this.reject=i})}controller=new AbortController;promise;fileId;loaded=0;progress=0;uploadSize=0;resolve;reject;abort(){this.controller.abort()}setProgress(e){this.loaded=e,this.progress=this.file.size&&e?Math.min(1,e/this.file.size):0}}P.register(U),p.PluginUpload=U,Object.defineProperty(p,Symbol.toStringTag,{value:"Module"})});
|