enigmatic 0.27.0 → 0.29.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/bun-server.js ADDED
@@ -0,0 +1,130 @@
1
+ import { S3Client } from "bun";
2
+
3
+ const dbPath = "db.json";
4
+ const db = await Bun.file(dbPath).json().catch(() => ({}));
5
+ const s3 = new S3Client({
6
+ accessKeyId: Bun.env.CLOUDFLARE_ACCESS_KEY_ID,
7
+ secretAccessKey: Bun.env.CLOUDFLARE_SECRET_ACCESS_KEY,
8
+ bucket: Bun.env.CLOUDFLARE_BUCKET_NAME,
9
+ endpoint: Bun.env.CLOUDFLARE_PUBLIC_URL
10
+ });
11
+ const json = (data, status = 200, extraHeaders = {}) => new Response(JSON.stringify(data), {
12
+ status,
13
+ headers: {
14
+ "Access-Control-Allow-Origin": "*",
15
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PURGE, PROPFIND, DOWNLOAD, OPTIONS",
16
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, Cookie, X-HTTP-Method-Override",
17
+ "Access-Control-Allow-Credentials": "true",
18
+ "Content-Type": "application/json",
19
+ ...extraHeaders
20
+ }
21
+ });
22
+ const writeDb = () => Bun.write(dbPath, JSON.stringify(db, null, 2));
23
+ const redirect = (url, cookie = null) => new Response(null, {
24
+ status: 302,
25
+ headers: { Location: url, ...(cookie && { "Set-Cookie": cookie }) }
26
+ });
27
+
28
+ export default {
29
+ async fetch(req) {
30
+ console.log('req.method', req.method);
31
+ const url = new URL(req.url);
32
+ const key = url.pathname.slice(1);
33
+ const cb = `${url.origin}/callback`;
34
+ const token = req.headers.get("Cookie")?.match(/token=([^;]+)/)?.[1];
35
+ const user = token ? db[`session:${token}`] : null;
36
+
37
+ if (req.method === "OPTIONS") return json(null, 204);
38
+
39
+ if (url.pathname === '/login') {
40
+ return Response.redirect(`https://${Bun.env.AUTH0_DOMAIN}/authorize?${new URLSearchParams({
41
+ response_type: "code",
42
+ client_id: Bun.env.AUTH0_CLIENT_ID,
43
+ redirect_uri: cb,
44
+ scope: "openid email profile"
45
+ })}`);
46
+ }
47
+
48
+ if (url.pathname === '/callback') {
49
+ const code = url.searchParams.get("code");
50
+ if (!code) return json({ error: 'No code' }, 400);
51
+ const tRes = await fetch(`https://${Bun.env.AUTH0_DOMAIN}/oauth/token`, {
52
+ method: "POST",
53
+ headers: { "content-type": "application/json" },
54
+ body: JSON.stringify({
55
+ grant_type: "authorization_code",
56
+ client_id: Bun.env.AUTH0_CLIENT_ID,
57
+ client_secret: Bun.env.AUTH0_CLIENT_SECRET,
58
+ code,
59
+ redirect_uri: cb
60
+ })
61
+ });
62
+ if (!tRes.ok) return json({ error: 'Auth error' }, 401);
63
+ const tokens = await tRes.json();
64
+ const userInfo = await (await fetch(`https://${Bun.env.AUTH0_DOMAIN}/userinfo`, {
65
+ headers: { Authorization: `Bearer ${tokens.access_token}` }
66
+ })).json();
67
+ const session = crypto.randomUUID();
68
+ db[`session:${session}`] = {
69
+ ...userInfo,
70
+ login_time: new Date().toISOString(),
71
+ access_token_expires_at: tokens.expires_in ? new Date(Date.now() + tokens.expires_in * 1000).toISOString() : null
72
+ };
73
+ await writeDb();
74
+ return redirect(url.origin, `token=${session}; HttpOnly; Path=/; Secure; SameSite=Lax; Max-Age=86400`);
75
+ }
76
+
77
+ if (!token || !user) return json({ error: 'Unauthorized' }, 401);
78
+
79
+ if (url.pathname === '/logout') {
80
+ delete db[`session:${token}`];
81
+ await writeDb();
82
+ return redirect(url.origin, "token=; Max-Age=0; Path=/");
83
+ }
84
+
85
+ const files = { '/': 'index.html', '/client.js': 'client.js', '/custom.js': 'custom.js' };
86
+ if (req.method === 'GET' && files[url.pathname]) return new Response(Bun.file(`./public/${files[url.pathname]}`));
87
+
88
+ console.log(req.method);
89
+ switch (req.method) {
90
+ case 'GET': return json(db[key]);
91
+ case 'POST':
92
+ const value = await req.text();
93
+ db[key] = (() => { try { return JSON.parse(value); } catch { return value; } })();
94
+ await writeDb();
95
+ return json({ key, value });
96
+ case 'DELETE':
97
+ delete db[key];
98
+ await writeDb();
99
+ return json({ status: "Deleted" });
100
+ case 'PUT':
101
+ await s3.write(`${user.sub}/${key}`, req.body);
102
+ return json({ status: "Saved to R2" });
103
+ case 'PURGE':
104
+ await s3.delete(`${user.sub}/${key}`);
105
+ return json({ status: "Deleted from R2" });
106
+ case 'PROPFIND':
107
+ const list = await s3.list({ prefix: `${user.sub}/` });
108
+ const items = Array.isArray(list) ? list : (list?.contents || []);
109
+ return json(items.map(item => ({
110
+ name: item.key?.split('/').pop() || item.name || item.Key,
111
+ lastModified: item.lastModified || item.LastModified,
112
+ size: item.size || item.Size || 0
113
+ })));
114
+ case 'PATCH':
115
+ try {
116
+ const exists = await s3.exists(`${user.sub}/${key}`);
117
+ if (!exists) return json({ error: 'File not found' }, 404);
118
+ const file = await s3.file(`${user.sub}/${key}`);
119
+ if (!file) return json({ error: 'File not found' }, 404);
120
+ return new Response(file.stream(), { headers: file.headers });
121
+ } catch (err) {
122
+ console.error('Download error:', err);
123
+ return json({ error: 'File not found', details: err.message }, 404);
124
+ }
125
+ default: return json({ error: 'Method not allowed' }, 405);
126
+ }
127
+ },
128
+ port: 3000,
129
+ tls: { cert: Bun.file("cert.pem"), key: Bun.file("key.pem") }
130
+ };
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
- "name": "enigmatic",
3
- "main": "enigmatic.js",
4
- "version": "0.27.0",
5
- "scripts": {
6
- "test": "jest",
7
- "test:watch": "jest --watch"
8
- },
9
- "devDependencies": {
10
- "jest": "^29.7.0",
11
- "jest-environment-jsdom": "^29.7.0",
12
- "whatwg-fetch": "^3.6.20"
13
- }
2
+ "name": "enigmatic",
3
+ "version": "0.29.1",
4
+ "unpkg": "./public/client.js",
5
+ "scripts": {
6
+ "start": "bun --hot ./bun-server.js",
7
+ "test": "jest --config __tests__/jest.config.js"
8
+ },
9
+ "devDependencies": {
10
+ "jest": "^29.0.0",
11
+ "jest-environment-jsdom": "^29.0.0",
12
+ "whatwg-fetch": "^3.6.20"
13
+ }
14
14
  }
@@ -0,0 +1,127 @@
1
+ window.api_url = "https://localhost:3000"
2
+ window.$ = document.querySelector.bind(document)
3
+ window.$$ = document.querySelectorAll.bind(document)
4
+ window.$c = (selector) => $0.closest(selector);
5
+
6
+ // Initialize custom elements
7
+ window.initCustomElements = function() {
8
+ Object.keys(window.custom || {}).forEach(tagName => {
9
+ $$(tagName).forEach(async el => {
10
+ const f = window.custom[tagName];
11
+ if (typeof f === 'function') {
12
+ el.innerHTML = await f();
13
+ } else if (f && typeof f.render === 'function') {
14
+ el.innerHTML = f.render();
15
+ }
16
+ });
17
+ });
18
+ }
19
+
20
+ // Make window.custom a Proxy that auto-initializes when properties are added
21
+ if (!window.custom) window.custom = {};
22
+ const customProxy = new Proxy(window.custom, {
23
+ set(target, prop, value) {
24
+ target[prop] = value;
25
+ if (typeof value === 'function' || (value && typeof value.render === 'function')) {
26
+ setTimeout(() => {
27
+ $$(prop).forEach(async el => {
28
+ if (typeof value === 'function') {
29
+ el.innerHTML = await value();
30
+ } else {
31
+ el.innerHTML = value.render();
32
+ }
33
+ });
34
+ }, 0);
35
+ }
36
+ return true;
37
+ }
38
+ });
39
+ Object.defineProperty(window, 'custom', {
40
+ get: () => customProxy,
41
+ set: (val) => {
42
+ Object.assign(customProxy, val || {});
43
+ },
44
+ configurable: true
45
+ });
46
+
47
+ window.state = new Proxy({}, {
48
+ set(obj, prop, value) {
49
+ obj[prop] = value
50
+ $$(`[data="${prop}"]`).forEach(el => {
51
+ console.log('setting', el.tagName);
52
+ const f = window.custom?.[el.tagName.toLowerCase()];
53
+ if (!f) return;
54
+ if(typeof f === 'function') {
55
+ el.innerHTML = f(value);
56
+ } else if (f && typeof f.render === 'function') {
57
+ el.innerHTML = f.render(value);
58
+ }
59
+ });
60
+ return true
61
+ }
62
+ })
63
+ window.get = async function(key) {
64
+ const res = await fetch(`${window.api_url}/${encodeURIComponent(key)}`)
65
+ return await res.json()
66
+ }
67
+ window.set = async function(key, value) {
68
+ const res = await fetch(`${window.api_url}/${encodeURIComponent(key)}`, {
69
+ method: 'POST', body: typeof value === 'string' ? value : JSON.stringify(value)
70
+ })
71
+ return await res.json()
72
+ }
73
+ window.delete = async function(key) {
74
+ const res = await fetch(`${window.api_url}/${encodeURIComponent(key)}`, {
75
+ method: 'DELETE'
76
+ })
77
+ return await res.json()
78
+ }
79
+ window.put = async function(key, body) {
80
+ const res = await fetch(`${window.api_url}/${encodeURIComponent(key)}`, {
81
+ method: 'PUT', body: body instanceof Blob ? body : typeof body === 'string' ? body : JSON.stringify(body)
82
+ })
83
+ return await res.json()
84
+ }
85
+ window.purge = async function(key) {
86
+ const res = await fetch(`${window.api_url}/${encodeURIComponent(key)}`, {
87
+ method: 'PURGE'
88
+ })
89
+ return await res.json()
90
+ }
91
+ window.list = async function() {
92
+ const res = await fetch(`${window.api_url}`, {
93
+ method: 'PROPFIND'
94
+ })
95
+ return await res.json()
96
+ }
97
+ window.download = async function(key) {
98
+ try {
99
+ console.log('Downloading with method DOWNLOAD:', key);
100
+ const res = await fetch(`${window.api_url}/${encodeURIComponent(key)}`, { method: 'PATCH' });
101
+ console.log('Response:', key, res.status, res.statusText);
102
+ if (!res.ok) throw new Error('Download failed');
103
+ const blob = await res.blob();
104
+ const url = URL.createObjectURL(blob);
105
+ const a = document.createElement('a');
106
+ a.href = url;
107
+ a.download = key;
108
+ a.click();
109
+ URL.revokeObjectURL(url);
110
+ } catch (err) {
111
+ console.error('Download error:', err);
112
+ throw err;
113
+ }
114
+ }
115
+ window.login = function() {
116
+ window.location.href = `${window.api_url}/login`
117
+ }
118
+ window.logout = function() {
119
+ window.location.href = `${window.api_url}/logout`
120
+ }
121
+
122
+ // Auto-initialize on load
123
+ if (document.readyState === 'loading') {
124
+ document.addEventListener('DOMContentLoaded', window.initCustomElements);
125
+ } else {
126
+ window.initCustomElements();
127
+ }
@@ -0,0 +1,29 @@
1
+ window.custom = {
2
+ "hello-world": (data) => `Hello ${data}`,
3
+ "hello-world-2": {
4
+ prop: (data) => `${data} World`,
5
+ render: function(data) {
6
+ return this.prop(data);
7
+ }
8
+ },
9
+ "file-widget": async () => {
10
+ const list = await window.list();
11
+ const style = `<style>.w-c{font:13px sans-serif;border:1px solid #ddd;border-radius:6px;overflow:hidden;max-width:320px}.w-i{display:flex;justify-content:space-between;padding:8px 12px;border-bottom:1px solid #f0f0f0;align-items:center}.w-i:hover{background:#f9f9f9}.w-d{border:none;background:none;cursor:pointer;opacity:.5;transition:.2s}.w-d:hover{opacity:1}.w-u{display:block;padding:10px;background:#f5f5f5;text-align:center;cursor:pointer;color:#555;font-weight:600;transition:.2s}.w-u:hover{background:#eee}</style>`;
12
+
13
+ const items = list.map(item => `
14
+ <div class="w-i">
15
+ <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:10px">${item.name}</span>
16
+ <button class="w-d" onclick="window.download('${item.name}')" title="Download">⬇️</button>
17
+ <button class="w-d" onclick="(async()=>{await window.purge('${item.name}');location.reload()})()" title="Delete">🗑️</button>
18
+ </div>`
19
+ ).join('');
20
+
21
+ const upload = `
22
+ <label class="w-u">
23
+ 📂 Upload
24
+ <input type="file" style="display:none" onchange="(async()=>{const f=this.files[0];if(f){await window.put(f.name,f);location.reload()}})()">
25
+ </label>`;
26
+
27
+ return style + `<div class="w-c">${items}${upload}</div>`;
28
+ }
29
+ }
@@ -0,0 +1,45 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>API Test</title>
7
+ <script src="custom.js"></script>
8
+ <script src="client.js"></script>
9
+ <style>
10
+ body { font-family: sans-serif; max-width: 800px; margin: 20px auto; padding: 20px; }
11
+ .section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
12
+ input, button { margin: 5px; padding: 8px; }
13
+ input { width: 200px; }
14
+ button { cursor: pointer; }
15
+ #result { margin-top: 10px; padding: 10px; background: #f5f5f5; border-radius: 3px; white-space: pre-wrap; }
16
+ .error { color: red; }
17
+ .success { color: green; }
18
+ </style>
19
+ </head>
20
+ <body>
21
+ <h1>Server API Test</h1>
22
+
23
+ <div class="section">
24
+ <h2>KV Storage</h2>
25
+ <input type="text" id="kv-key" placeholder="Key" value="test-key">
26
+ <input type="text" id="kv-value" placeholder="Value" value="test-value">
27
+ <button onclick="window.set(document.querySelector('#kv-key').value, document.querySelector('#kv-value').value).then(r => window.$('#result').textContent = JSON.stringify(r)).catch(err => window.$('#result').textContent = 'Error: ' + err.message)">POST</button>
28
+ <button onclick="window.get(document.querySelector('#kv-key').value).then(r => window.$('#result').textContent = JSON.stringify(r)).catch(err => window.$('#result').textContent = 'Error: ' + err.message)">GET</button>
29
+ <button onclick="window.delete(document.querySelector('#kv-key').value).then(r => window.$('#result').textContent = JSON.stringify(r)).catch(err => window.$('#result').textContent = 'Error: ' + err.message)">DELETE</button>
30
+ </div>
31
+
32
+ <div class="section">
33
+ <h2>R2 Storage</h2>
34
+ <file-widget></file-widget>
35
+ </div>
36
+
37
+ <div class="section">
38
+ <h2>Auth</h2>
39
+ <button onclick="window.login()">Login</button>
40
+ <button onclick="window.logout()">Logout</button>
41
+ </div>
42
+
43
+ <pre id="result"></pre>
44
+ </body>
45
+ </html>
package/README.md DELETED
@@ -1,218 +0,0 @@
1
- **Enigmatic.js - Documentation**
2
-
3
- *Note: This repository contains a single JavaScript file that exposes a small set of utility functions under the global `w` object.*
4
-
5
- **1. Introduction**
6
-
7
- Enigmatic.js provides helpers for loading resources, creating custom elements and managing a small reactive state. The library automatically activates once the script is loaded and offers the following features:
8
-
9
- - Resource Loading: load JavaScript files dynamically
10
- - Custom Elements: quickly define web components with built-in event wiring
11
- - State Management: update DOM elements automatically when state values change
12
- - Data Handling: fetch JSON on elements with a `fetch` attribute and receive server-sent events via streams
13
- - Error Handling: automatic error display for JavaScript errors and promise rejections
14
- - Template Engine: powerful templating with support for nested properties and special variables
15
-
16
- **2. Helpers**
17
-
18
- Several helper functions simplify common tasks:
19
-
20
- - `w.$(selector)`: return the first element matching the selector
21
- - `w.$$(selector)`: return all elements matching the selector
22
- - `w.loadJS(src)`: dynamically load a JavaScript file (returns Promise)
23
- - `w.wait(ms)`: resolve a Promise after `ms` milliseconds
24
- - `w.ready()`: resolve when the DOM is ready
25
- - `w.get(url, opts, transform, key)`: fetch JSON from URL, optionally transform and store in state
26
-
27
- **3. Template Flattening**
28
-
29
- `w.flatten(obj, text)` replaces placeholders like `{key}` within a template string using data from `obj`.
30
-
31
- **Basic usage:**
32
- ```javascript
33
- w.flatten({ name: 'John', age: 30 }, 'Hello {name}, age {age}')
34
- // Returns: "Hello John, age 30"
35
- ```
36
-
37
- **Nested properties:**
38
- ```javascript
39
- w.flatten({ user: { name: 'John' } }, 'Hello {user.name}')
40
- // Returns: "Hello John"
41
- ```
42
-
43
- **Arrays:**
44
- ```javascript
45
- w.flatten([{ name: 'John' }, { name: 'Jane' }], 'Name: {name}')
46
- // Returns: "Name: JohnName: Jane"
47
- ```
48
-
49
- **Special variables for iteration:**
50
- - `{$key}` - current key/index
51
- - `{$val}` - current value
52
- - `{$index}` - array index (for arrays)
53
-
54
- ```javascript
55
- // For objects
56
- w.flatten({ k1: 'val1', k2: 'val2' }, '{$key}: {$val}')
57
- // Returns: "k1: val1k2: val2"
58
-
59
- // For arrays
60
- w.flatten(['a', 'b'], '{$index}: {$val}')
61
- // Returns: "0: a1: b"
62
- ```
63
-
64
- **4. Custom Elements**
65
-
66
- Use `w.e(name, fn, style)` to register a custom element.
67
-
68
- **With object configuration:**
69
- ```javascript
70
- w.e('my-element', {
71
- init: (e) => e.innerText = 'ready',
72
- click: (ev) => console.log('clicked'),
73
- set: (data) => { /* handle data updates */ }
74
- }, {
75
- color: 'red',
76
- padding: '10px'
77
- })
78
- ```
79
-
80
- **With function (simplified):**
81
- ```javascript
82
- w.e('my-element', (data) => `<div>${data.name}</div>`)
83
- // Automatically creates a set() method that updates innerHTML
84
- ```
85
-
86
- The `fn` parameter can be:
87
- - An object with methods (`init`, `set`, event handlers like `click`, `mouseover`)
88
- - A function that receives data and returns HTML string
89
-
90
- Event handlers matching `/click|mouseover/` are automatically bound as event listeners.
91
-
92
- **5. State and Reactivity**
93
-
94
- - `w.state`: reactive storage backed by a `Proxy`. Updating a key automatically calls `set()` on any element with a matching `data` attribute.
95
-
96
- ```javascript
97
- w.state.users = [{ name: 'John' }]
98
- // All elements with data="users" will have their set() method called
99
- ```
100
-
101
- - `w.state._all`: returns the entire state object
102
- - `w.stream(url, key)`: subscribe to an EventSource and populate `w.state[key]` with incoming data
103
-
104
- **6. Data Binding on Divs**
105
-
106
- The library automatically enhances all `<div>` elements with data binding capabilities:
107
-
108
- **Basic usage:**
109
- ```html
110
- <div data="users" fetch="https://api.example.com/users">
111
- <div>{name} - {email}</div>
112
- </div>
113
- ```
114
-
115
- **Attributes:**
116
- - `data="key"` - binds to `w.state[key]`, updates when state changes
117
- - `fetch="url"` - fetch JSON from URL and render with template
118
- - `fetch='{"inline": "json"}'` - use inline JSON instead of URL
119
- - `defer` - skip automatic fetch on init (call `element.fetch()` manually)
120
- - `t="transform"` - transform function as string (e.g., `t="d=>d.results"`)
121
-
122
- **IGNORE blocks:**
123
- ```html
124
- <div>
125
- Hello {name}
126
- <!--IGNORE-->
127
- This won't be in the template
128
- <!--ENDIGNORE-->
129
- </div>
130
- ```
131
-
132
- **7. Error Handling**
133
-
134
- Enigmatic.js automatically displays JavaScript errors and unhandled promise rejections as a red banner at the top of the page, showing the error message and location.
135
-
136
- **8. Component Registration via window.components**
137
-
138
- You can also register components via `window.components`:
139
-
140
- ```javascript
141
- window.components = {
142
- "my-component": {
143
- init: async () => {
144
- // initialization
145
- },
146
- set: (data) => {
147
- // handle data
148
- },
149
- click: (ev) => {
150
- // click handler
151
- },
152
- style: { color: 'blue' } // optional styles
153
- }
154
- }
155
- ```
156
-
157
- See `components.js` for examples.
158
-
159
- **9. Usage Examples**
160
-
161
- **Basic example:**
162
- ```html
163
- <script src="enigmatic.js"></script>
164
- <script>
165
- e('custom-element', {
166
- init: e => e.innerHTML = 'ready',
167
- click: ev => console.log('clicked')
168
- });
169
-
170
- (async () => {
171
- await ready();
172
- w.state.example = { key1: 'value1', key2: 'value2' };
173
- })();
174
- </script>
175
- ```
176
-
177
- **Data binding example:**
178
- ```html
179
- <div data="users" fetch="https://api.example.com/users">
180
- <div>{name} - {email}</div>
181
- </div>
182
-
183
- <script>
184
- // State updates automatically update the div
185
- w.state.users = [{ name: 'John', email: 'john@example.com' }];
186
- </script>
187
- ```
188
-
189
- **Component with function:**
190
- ```html
191
- <script>
192
- e('user-card', (data) => `
193
- <div class="card">
194
- <h3>${data.name}</h3>
195
- <p>${data.email}</p>
196
- </div>
197
- `);
198
- </script>
199
-
200
- <user-card data="currentUser"></user-card>
201
- ```
202
-
203
- **10. Testing**
204
-
205
- The library includes comprehensive headless tests using Jest:
206
-
207
- ```bash
208
- npm test # Run tests once
209
- npm run test:watch # Run tests in watch mode
210
- ```
211
-
212
- **11. Global Object**
213
-
214
- All helper functions are attached to the global object `w` as well as directly on `window` for convenience.
215
-
216
- **12. Support**
217
-
218
- For bug reports, feature requests, or general inquiries, please visit the GitHub repository of Enigmatic.js.