@formseal/embed 3.1.0 → 3.1.1
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 +39 -11
- package/cli/commands/configure.py +39 -62
- package/package.json +1 -1
- package/src/config/fields.jsonl +3 -0
- package/src/config/fse.config.js +5 -20
- package/src/globals.js +31 -3
package/README.md
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<img src="https://img.shields.io/badge/encryption-X25519-10b981?style=flat
|
|
7
|
-
<img src="https://img.shields.io/badge/decryption-offline%20only-f59e0b?style=flat
|
|
8
|
-
<img src="https://img.shields.io/badge/backend-blind-6366f1?style=flat
|
|
9
|
-
<img src="https://img.shields.io/npm/v/@formseal/embed?style=flat
|
|
10
|
-
<img src="https://img.shields.io/badge/license-MIT-fc8181?style=flat
|
|
6
|
+
<img src="https://img.shields.io/badge/encryption-X25519-10b981?style=flat&labelColor=1e293b">
|
|
7
|
+
<img src="https://img.shields.io/badge/decryption-offline%20only-f59e0b?style=flat&labelColor=1e293b">
|
|
8
|
+
<img src="https://img.shields.io/badge/backend-blind-6366f1?style=flat&labelColor=1e293b">
|
|
9
|
+
<img src="https://img.shields.io/npm/v/@formseal/embed?style=flat&label=npm&labelColor=fff&color=cb0000">
|
|
10
|
+
<img src="https://img.shields.io/badge/license-MIT-fc8181?style=flat&labelColor=1e293b">
|
|
11
|
+
</p>
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img src="https://badge.socket.dev/npm/package/@formseal/embed/3.1.0">
|
|
11
14
|
</p>
|
|
12
15
|
|
|
13
16
|
<p align="center">
|
|
@@ -24,12 +27,34 @@ formseal is not a hosted service, dashboard, or SaaS product. It is a drop-in cl
|
|
|
24
27
|
|
|
25
28
|
## Installation
|
|
26
29
|
|
|
30
|
+
**Via npm (recommended)**
|
|
31
|
+
|
|
27
32
|
```bash
|
|
28
33
|
npm install -g @formseal/embed
|
|
29
34
|
fse init
|
|
30
35
|
```
|
|
31
36
|
|
|
32
|
-
|
|
37
|
+
**Via GitHub release (zero toolchain)**
|
|
38
|
+
|
|
39
|
+
1. Download the latest [release artifact](https://github.com/grayguava/formseal-embed/releases)
|
|
40
|
+
2. Unzip → drop `formseal/` into your project
|
|
41
|
+
3. Edit `fse.config.js` manually
|
|
42
|
+
|
|
43
|
+
> Both paths are identical. The CLI is a scaffolding tool that lives globally — your project only gets `./formseal-embed/`, which is static files with zero dependencies. No `node_modules`, no `package.json`, no build step. Works fully offline after setup. The CLI can be uninstalled anytime.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 60-second demo
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install -g @formseal/embed && fse init && open sample/index.html
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or: download a release → unzip → open `sample/index.html`.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Configure
|
|
33
58
|
|
|
34
59
|
```bash
|
|
35
60
|
fse configure quick
|
|
@@ -37,8 +62,6 @@ fse configure quick
|
|
|
37
62
|
|
|
38
63
|
You'll be prompted for your POST endpoint and public key. See [Getting started](./docs/getting-started.md) for key generation.
|
|
39
64
|
|
|
40
|
-
> If you prefer not to install globally, `npx @formseal/embed init` works — but you'll need to edit `fse.config.js` manually instead of using the CLI.
|
|
41
|
-
|
|
42
65
|
---
|
|
43
66
|
|
|
44
67
|
## Security guarantee
|
|
@@ -77,6 +100,8 @@ Your endpoint stores the ciphertext. Only the holder of the private key can decr
|
|
|
77
100
|
|
|
78
101
|
## Wire up your HTML
|
|
79
102
|
|
|
103
|
+
> After `fse init`, files live in `./formseal-embed/`. Reference them via your server's static path (e.g. `/formseal-embed/globals.js`).
|
|
104
|
+
|
|
80
105
|
```html
|
|
81
106
|
<form id="contact-form">
|
|
82
107
|
|
|
@@ -100,14 +125,16 @@ Your endpoint stores the ciphertext. Only the holder of the private key can decr
|
|
|
100
125
|
|
|
101
126
|
<script>
|
|
102
127
|
window.fseCallbacks = {
|
|
103
|
-
onSuccess:
|
|
104
|
-
onError:
|
|
128
|
+
onSuccess: () => document.getElementById('contact-status').textContent = 'Sent securely.',
|
|
129
|
+
onError: (err) => console.error('formseal error:', err),
|
|
105
130
|
};
|
|
106
131
|
</script>
|
|
107
132
|
|
|
108
133
|
<script src="/formseal-embed/globals.js"></script>
|
|
109
134
|
```
|
|
110
135
|
|
|
136
|
+
> Works fully offline after setup. No CDN, no runtime fetches, no external dependencies.
|
|
137
|
+
|
|
111
138
|
---
|
|
112
139
|
|
|
113
140
|
## Payload format
|
|
@@ -118,7 +145,6 @@ Your endpoint stores the ciphertext. Only the holder of the private key can decr
|
|
|
118
145
|
"origin": "contact-form",
|
|
119
146
|
"id": "<uuid>",
|
|
120
147
|
"submitted_at": "<iso8601>",
|
|
121
|
-
"client_tz": "Europe/London",
|
|
122
148
|
"data": {
|
|
123
149
|
"name": "...",
|
|
124
150
|
"email": "...",
|
|
@@ -129,6 +155,8 @@ Your endpoint stores the ciphertext. Only the holder of the private key can decr
|
|
|
129
155
|
|
|
130
156
|
The entire object is sealed with `crypto_box_seal`. Your endpoint receives raw ciphertext as the request body.
|
|
131
157
|
|
|
158
|
+
> No IP, no timezone, no fingerprints — just the data you explicitly collect.
|
|
159
|
+
|
|
132
160
|
---
|
|
133
161
|
|
|
134
162
|
## CSS hooks
|
|
@@ -15,6 +15,7 @@ from pathlib import Path
|
|
|
15
15
|
from ui.output import br, rule, row, code, fail, C, G, Y, S, W, R, D
|
|
16
16
|
|
|
17
17
|
CONFIG_PATH = Path.cwd() / "formseal-embed" / "config" / "fse.config.js"
|
|
18
|
+
FIELDS_PATH = Path.cwd() / "formseal-embed" / "config" / "fields.jsonl"
|
|
18
19
|
|
|
19
20
|
MARKERS = {
|
|
20
21
|
"endpoint": "endpoint:",
|
|
@@ -60,42 +61,30 @@ def _normalize_endpoint(url: str) -> str:
|
|
|
60
61
|
return url
|
|
61
62
|
|
|
62
63
|
|
|
63
|
-
def
|
|
64
|
-
if not
|
|
65
|
-
return {}
|
|
66
|
-
content = CONFIG_PATH.read_text(encoding="utf-8")
|
|
67
|
-
match = re.search(r'var FSE = (\{[\s\S]*?\});', content)
|
|
68
|
-
if not match:
|
|
69
|
-
return {}
|
|
70
|
-
try:
|
|
71
|
-
return json.loads(match.group(1))
|
|
72
|
-
except json.JSONDecodeError:
|
|
64
|
+
def _load_fields_jsonl() -> dict:
|
|
65
|
+
if not FIELDS_PATH.exists():
|
|
73
66
|
return {}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def _save_config(cfg: dict):
|
|
77
|
-
lines = CONFIG_PATH.read_text(encoding="utf-8").splitlines(keepends=True)
|
|
78
|
-
out = []
|
|
67
|
+
lines = FIELDS_PATH.read_text(encoding="utf-8").strip().split('\n')
|
|
68
|
+
fields = {}
|
|
79
69
|
for line in lines:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
CONFIG_PATH.write_text("".join(out), encoding="utf-8")
|
|
70
|
+
line = line.strip()
|
|
71
|
+
if not line:
|
|
72
|
+
continue
|
|
73
|
+
try:
|
|
74
|
+
obj = json.loads(line)
|
|
75
|
+
key = list(obj.keys())[0]
|
|
76
|
+
fields[key] = obj[key]
|
|
77
|
+
except:
|
|
78
|
+
pass
|
|
79
|
+
return fields
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _save_fields_jsonl(fields: dict):
|
|
83
|
+
lines = []
|
|
84
|
+
for name, opts in fields.items():
|
|
85
|
+
line = json.dumps({name: opts})
|
|
86
|
+
lines.append(line)
|
|
87
|
+
FIELDS_PATH.write_text('\n'.join(lines) + '\n', encoding="utf-8")
|
|
99
88
|
|
|
100
89
|
|
|
101
90
|
def run(subcommand: str, args: list):
|
|
@@ -172,13 +161,10 @@ def _field_add(args: list):
|
|
|
172
161
|
fail("Usage: fse configure field add <name> [required:true] [maxLength:n] [type:email]")
|
|
173
162
|
|
|
174
163
|
name = args[0]
|
|
175
|
-
|
|
176
|
-
fields = cfg.get("fields", {})
|
|
177
|
-
|
|
178
|
-
if name in fields:
|
|
179
|
-
fail(f"Field {W}{name}{R} already exists.")
|
|
164
|
+
fields = _load_fields_jsonl()
|
|
180
165
|
|
|
181
|
-
|
|
166
|
+
is_update = name in fields
|
|
167
|
+
field = fields.get(name, {"enabled": True})
|
|
182
168
|
for opt in args[1:]:
|
|
183
169
|
if ":" in opt:
|
|
184
170
|
k, v = opt.split(":", 1)
|
|
@@ -195,14 +181,13 @@ def _field_add(args: list):
|
|
|
195
181
|
field["type"] = v
|
|
196
182
|
|
|
197
183
|
fields[name] = field
|
|
198
|
-
|
|
199
|
-
_save_config(cfg)
|
|
184
|
+
_save_fields_jsonl(fields)
|
|
200
185
|
|
|
201
186
|
br()
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
187
|
+
action = "Updated" if is_update else "Added"
|
|
188
|
+
print(f" {G}{action} field:{R} {name}")
|
|
189
|
+
for k, v in field.items():
|
|
190
|
+
row("", k, str(v))
|
|
206
191
|
|
|
207
192
|
|
|
208
193
|
def _field_remove(args: list):
|
|
@@ -210,15 +195,13 @@ def _field_remove(args: list):
|
|
|
210
195
|
fail("Usage: fse configure field remove <name>")
|
|
211
196
|
|
|
212
197
|
name = args[0]
|
|
213
|
-
|
|
214
|
-
fields = cfg.get("fields", {})
|
|
198
|
+
fields = _load_fields_jsonl()
|
|
215
199
|
|
|
216
200
|
if name not in fields:
|
|
217
201
|
fail(f"Field {W}{name}{R} not found.")
|
|
218
202
|
|
|
219
203
|
del fields[name]
|
|
220
|
-
|
|
221
|
-
_save_config(cfg)
|
|
204
|
+
_save_fields_jsonl(fields)
|
|
222
205
|
|
|
223
206
|
br()
|
|
224
207
|
print(f" {G}Removed field:{R} {name}")
|
|
@@ -232,15 +215,13 @@ def _field_required(args: list):
|
|
|
232
215
|
if value not in ("true", "false"):
|
|
233
216
|
fail("Use true or false")
|
|
234
217
|
|
|
235
|
-
|
|
236
|
-
fields = cfg.get("fields", {})
|
|
218
|
+
fields = _load_fields_jsonl()
|
|
237
219
|
|
|
238
220
|
if name not in fields:
|
|
239
221
|
fail(f"Field {W}{name}{R} not found.")
|
|
240
222
|
|
|
241
223
|
fields[name]["required"] = value == "true"
|
|
242
|
-
|
|
243
|
-
_save_config(cfg)
|
|
224
|
+
_save_fields_jsonl(fields)
|
|
244
225
|
|
|
245
226
|
br()
|
|
246
227
|
row(">", f"{name}.required", value)
|
|
@@ -256,15 +237,13 @@ def _field_maxlength(args: list):
|
|
|
256
237
|
except ValueError:
|
|
257
238
|
fail(f"Invalid number: {value}")
|
|
258
239
|
|
|
259
|
-
|
|
260
|
-
fields = cfg.get("fields", {})
|
|
240
|
+
fields = _load_fields_jsonl()
|
|
261
241
|
|
|
262
242
|
if name not in fields:
|
|
263
243
|
fail(f"Field {W}{name}{R} not found.")
|
|
264
244
|
|
|
265
245
|
fields[name]["maxLength"] = maxlen
|
|
266
|
-
|
|
267
|
-
_save_config(cfg)
|
|
246
|
+
_save_fields_jsonl(fields)
|
|
268
247
|
|
|
269
248
|
br()
|
|
270
249
|
row(">", f"{name}.maxLength", str(maxlen))
|
|
@@ -278,15 +257,13 @@ def _field_type(args: list):
|
|
|
278
257
|
if value not in ("email", "tel", "text"):
|
|
279
258
|
fail(f"Invalid type: {value}. Use email, tel, or text.")
|
|
280
259
|
|
|
281
|
-
|
|
282
|
-
fields = cfg.get("fields", {})
|
|
260
|
+
fields = _load_fields_jsonl()
|
|
283
261
|
|
|
284
262
|
if name not in fields:
|
|
285
263
|
fail(f"Field {W}{name}{R} not found.")
|
|
286
264
|
|
|
287
265
|
fields[name]["type"] = value
|
|
288
|
-
|
|
289
|
-
_save_config(cfg)
|
|
266
|
+
_save_fields_jsonl(fields)
|
|
290
267
|
|
|
291
268
|
br()
|
|
292
269
|
row(">", f"{name}.type", value)
|
package/package.json
CHANGED
package/src/config/fse.config.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Edit this file or use the CLI:
|
|
5
5
|
// fse configure endpoint <url>
|
|
6
|
-
// fse configure
|
|
6
|
+
// fse configure publicKey <base64url>
|
|
7
7
|
// fse configure field add <name> [options]
|
|
8
8
|
// fse configure field remove <name>
|
|
9
9
|
// fse configure field required <name> <true|false>
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
var FSE = {
|
|
13
13
|
|
|
14
14
|
// -- Endpoint --
|
|
15
|
-
// POST target. Receives
|
|
15
|
+
// POST target. Receives raw ciphertext.
|
|
16
16
|
endpoint: "https://your-api.example.com/submit",
|
|
17
17
|
|
|
18
18
|
// -- Origin --
|
|
19
|
-
// Identifier for this form deployment.
|
|
19
|
+
// Identifier for this form deployment.
|
|
20
20
|
origin: "contact-form",
|
|
21
21
|
|
|
22
22
|
// -- Encryption --
|
|
@@ -48,22 +48,7 @@ var FSE = {
|
|
|
48
48
|
},
|
|
49
49
|
|
|
50
50
|
// -- Fields --
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
fields: {
|
|
54
|
-
name: {
|
|
55
|
-
required: true,
|
|
56
|
-
maxLength: 100,
|
|
57
|
-
},
|
|
58
|
-
email: {
|
|
59
|
-
type: "email",
|
|
60
|
-
required: true,
|
|
61
|
-
maxLength: 200,
|
|
62
|
-
},
|
|
63
|
-
message: {
|
|
64
|
-
required: true,
|
|
65
|
-
maxLength: 1000,
|
|
66
|
-
},
|
|
67
|
-
},
|
|
51
|
+
// Loaded from fields.jsonl at runtime
|
|
52
|
+
fields: FSE_FIELDS,
|
|
68
53
|
|
|
69
54
|
};
|
package/src/globals.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
var FILES = [
|
|
21
21
|
"vendor/sodium.js",
|
|
22
|
+
"config/fields.jsonl",
|
|
22
23
|
"config/fse.config.js",
|
|
23
24
|
"runtime/fse.crypto.js",
|
|
24
25
|
"runtime/fse.payload.js",
|
|
@@ -26,6 +27,25 @@
|
|
|
26
27
|
"runtime/fse.form.js",
|
|
27
28
|
];
|
|
28
29
|
|
|
30
|
+
var FSE_FIELDS = {};
|
|
31
|
+
|
|
32
|
+
function parseFieldsJsonl(code) {
|
|
33
|
+
var lines = code.trim().split('\n');
|
|
34
|
+
var fields = {};
|
|
35
|
+
for (var i = 0; i < lines.length; i++) {
|
|
36
|
+
var line = lines[i].trim();
|
|
37
|
+
if (!line) continue;
|
|
38
|
+
try {
|
|
39
|
+
var obj = JSON.parse(line);
|
|
40
|
+
var key = Object.keys(obj)[0];
|
|
41
|
+
if (key) {
|
|
42
|
+
fields[key] = obj[key];
|
|
43
|
+
}
|
|
44
|
+
} catch (e) {}
|
|
45
|
+
}
|
|
46
|
+
return fields;
|
|
47
|
+
}
|
|
48
|
+
|
|
29
49
|
function loadNext(index) {
|
|
30
50
|
if (index >= FILES.length) {
|
|
31
51
|
try {
|
|
@@ -56,9 +76,17 @@
|
|
|
56
76
|
})
|
|
57
77
|
.then(function (code) {
|
|
58
78
|
if (code === null || code === undefined) return;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
79
|
+
|
|
80
|
+
if (FILES[index] === "config/fields.jsonl") {
|
|
81
|
+
FSE_FIELDS = parseFieldsJsonl(code);
|
|
82
|
+
} else {
|
|
83
|
+
var s = document.createElement("script");
|
|
84
|
+
if (FILES[index] === "config/fse.config.js") {
|
|
85
|
+
code = "var FSE_FIELDS = " + JSON.stringify(FSE_FIELDS) + ";\n" + code;
|
|
86
|
+
}
|
|
87
|
+
s.textContent = code;
|
|
88
|
+
document.head.appendChild(s);
|
|
89
|
+
}
|
|
62
90
|
loadNext(index + 1);
|
|
63
91
|
})
|
|
64
92
|
.catch(function (err) {
|