@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 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-square&labelColor=1e293b&borderRadius=6px">
7
- <img src="https://img.shields.io/badge/decryption-offline%20only-f59e0b?style=flat-square&labelColor=1e293b&borderRadius=6px">
8
- <img src="https://img.shields.io/badge/backend-blind-6366f1?style=flat-square&labelColor=1e293b&borderRadius=6px">
9
- <img src="https://img.shields.io/npm/v/@formseal/embed?style=flat-square&label=npm&labelColor=fff&color=cb0000&borderRadius=6px">
10
- <img src="https://img.shields.io/badge/license-MIT-fc8181?style=flat-square&labelColor=1e293b&borderRadius=6px">
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
- Scaffolds `./formseal-embed/` into your project, then:
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: function(response) {},
104
- onError: function(error) {},
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 _load_config() -> dict:
64
- if not CONFIG_PATH.exists():
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
- if line.strip().startswith("fields:") and "{" in line:
81
- out.append(line)
82
- break
83
- out.append(line)
84
- else:
85
- out.append(f" fields: {json.dumps(cfg.get('fields', {}), indent=4)},\n")
86
- CONFIG_PATH.write_text("".join(out), encoding="utf-8")
87
- return
88
-
89
- indent = " "
90
- fields_json = json.dumps(cfg.get("fields", {}), indent=4)
91
- fields_json = "\n".join(indent + line for line in fields_json.splitlines())
92
- out.append(fields_json + ",\n")
93
-
94
- while lines and not lines[-1].strip().startswith("}"):
95
- lines.pop()
96
- out.extend(lines)
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
- cfg = _load_config()
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
- field = {}
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
- cfg["fields"] = fields
199
- _save_config(cfg)
184
+ _save_fields_jsonl(fields)
200
185
 
201
186
  br()
202
- print(f" {G}Added field:{R} {name}")
203
- if field:
204
- for k, v in field.items():
205
- row("", k, str(v))
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
- cfg = _load_config()
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
- cfg["fields"] = fields
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
- cfg = _load_config()
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
- cfg["fields"] = fields
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
- cfg = _load_config()
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
- cfg["fields"] = fields
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
- cfg = _load_config()
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
- cfg["fields"] = fields
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
@@ -26,5 +26,5 @@
26
26
  "type": "git",
27
27
  "url": "https://github.com/grayguava/formseal-embed.git"
28
28
  },
29
- "version": "3.1.0"
29
+ "version": "3.1.1"
30
30
  }
@@ -0,0 +1,3 @@
1
+ {"name": {"required": true, "maxLength": 100, "enabled": true}}
2
+ {"email": {"type": "email", "required": true, "maxLength": 200, "enabled": true}}
3
+ {"message": {"type": "text", "required": true, "maxLength": 1000, "enabled": true}}
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // Edit this file or use the CLI:
5
5
  // fse configure endpoint <url>
6
- // fse configure key <base64url>
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: { ciphertext: "<base64url>" }
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. Useful when you have multiple forms.
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
- // Key = field name (matches [name] attribute in HTML)
52
- // Value = validation rules
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
- var s = document.createElement("script");
60
- s.textContent = code;
61
- document.head.appendChild(s);
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) {