enigmatic 0.35.0 → 0.36.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 +1 -1
- package/client/public/api.html +202 -0
- package/client/public/client.js +47 -66
- package/client/public/index.html +359 -142
- package/package.json +1 -1
- package/server/bun-server.js +32 -19
package/README.md
CHANGED
|
@@ -127,7 +127,7 @@ const elements = window.$$('.my-class');
|
|
|
127
127
|
### API Base URL
|
|
128
128
|
|
|
129
129
|
```javascript
|
|
130
|
-
window.api_url = "https://localhost:
|
|
130
|
+
window.api_url = "https://localhost:3001"
|
|
131
131
|
```
|
|
132
132
|
|
|
133
133
|
Configures the base URL for all API requests. Modify this to point to your server.
|
|
@@ -0,0 +1,202 @@
|
|
|
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>Enigmatic Webapp</title>
|
|
7
|
+
|
|
8
|
+
<script>
|
|
9
|
+
window.api_url = "https://digplan.app"
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<style>
|
|
13
|
+
:root {
|
|
14
|
+
--bg: #f1f5f9;
|
|
15
|
+
--surface: #ffffff;
|
|
16
|
+
--text: #0f172a;
|
|
17
|
+
--text-muted: #64748b;
|
|
18
|
+
--accent: #6366f1;
|
|
19
|
+
--accent-hover: #4f46e5;
|
|
20
|
+
--border: #e2e8f0;
|
|
21
|
+
--input-bg: #f8fafc;
|
|
22
|
+
--radius: 12px;
|
|
23
|
+
--radius-sm: 8px;
|
|
24
|
+
--shadow: 0 1px 3px rgba(0,0,0,.06);
|
|
25
|
+
--shadow-md: 0 4px 12px rgba(0,0,0,.06);
|
|
26
|
+
}
|
|
27
|
+
* { box-sizing: border-box; }
|
|
28
|
+
body {
|
|
29
|
+
font-family: "Inter", system-ui, -apple-system, sans-serif;
|
|
30
|
+
font-size: 15px;
|
|
31
|
+
line-height: 1.6;
|
|
32
|
+
max-width: 600px;
|
|
33
|
+
margin: 0 auto;
|
|
34
|
+
padding: 32px 24px;
|
|
35
|
+
background: var(--bg);
|
|
36
|
+
color: var(--text);
|
|
37
|
+
-webkit-font-smoothing: antialiased;
|
|
38
|
+
}
|
|
39
|
+
h1 {
|
|
40
|
+
font-size: 1.75rem;
|
|
41
|
+
font-weight: 700;
|
|
42
|
+
letter-spacing: -0.02em;
|
|
43
|
+
margin: 0 0 28px;
|
|
44
|
+
color: var(--text);
|
|
45
|
+
}
|
|
46
|
+
section {
|
|
47
|
+
background: var(--surface);
|
|
48
|
+
border-radius: var(--radius);
|
|
49
|
+
padding: 20px;
|
|
50
|
+
margin-bottom: 20px;
|
|
51
|
+
box-shadow: var(--shadow);
|
|
52
|
+
border: 1px solid var(--border);
|
|
53
|
+
}
|
|
54
|
+
section h2 {
|
|
55
|
+
font-size: 0.8125rem;
|
|
56
|
+
font-weight: 600;
|
|
57
|
+
letter-spacing: 0.02em;
|
|
58
|
+
text-transform: uppercase;
|
|
59
|
+
color: var(--text-muted);
|
|
60
|
+
margin: 0 0 14px;
|
|
61
|
+
}
|
|
62
|
+
section p { margin: 0 0 12px; color: var(--text-muted); font-size: 0.9375rem; }
|
|
63
|
+
input {
|
|
64
|
+
font-size: 14px;
|
|
65
|
+
padding: 10px 14px;
|
|
66
|
+
border-radius: var(--radius-sm);
|
|
67
|
+
border: 1px solid var(--border);
|
|
68
|
+
background: var(--input-bg);
|
|
69
|
+
color: var(--text);
|
|
70
|
+
width: 100%;
|
|
71
|
+
max-width: 200px;
|
|
72
|
+
transition: border-color .15s, box-shadow .15s;
|
|
73
|
+
}
|
|
74
|
+
input:focus {
|
|
75
|
+
outline: none;
|
|
76
|
+
border-color: var(--accent);
|
|
77
|
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, .15);
|
|
78
|
+
}
|
|
79
|
+
input::placeholder { color: var(--text-muted); opacity: .8; }
|
|
80
|
+
button {
|
|
81
|
+
font-size: 14px;
|
|
82
|
+
font-weight: 500;
|
|
83
|
+
padding: 10px 16px;
|
|
84
|
+
border-radius: var(--radius-sm);
|
|
85
|
+
border: none;
|
|
86
|
+
cursor: pointer;
|
|
87
|
+
transition: background .15s, transform .05s;
|
|
88
|
+
}
|
|
89
|
+
button:active { transform: scale(0.98); }
|
|
90
|
+
button:not(.secondary) {
|
|
91
|
+
background: var(--accent);
|
|
92
|
+
color: #fff;
|
|
93
|
+
margin-right: 8px;
|
|
94
|
+
}
|
|
95
|
+
button:not(.secondary):hover { background: var(--accent-hover); }
|
|
96
|
+
button.secondary {
|
|
97
|
+
background: var(--input-bg);
|
|
98
|
+
color: var(--text);
|
|
99
|
+
border: 1px solid var(--border);
|
|
100
|
+
}
|
|
101
|
+
button.secondary:hover { background: var(--border); }
|
|
102
|
+
#auth-status { margin-right: 12px; color: var(--text-muted); font-size: 14px; }
|
|
103
|
+
.row { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 10px; }
|
|
104
|
+
.row:last-of-type { margin-bottom: 0; }
|
|
105
|
+
#kv-result {
|
|
106
|
+
margin-top: 12px;
|
|
107
|
+
padding: 12px 14px;
|
|
108
|
+
background: var(--input-bg);
|
|
109
|
+
border-radius: var(--radius-sm);
|
|
110
|
+
font-size: 13px;
|
|
111
|
+
font-family: ui-monospace, monospace;
|
|
112
|
+
white-space: pre-wrap;
|
|
113
|
+
word-break: break-all;
|
|
114
|
+
min-height: 24px;
|
|
115
|
+
border: 1px solid var(--border);
|
|
116
|
+
}
|
|
117
|
+
.error { color: #dc2626; }
|
|
118
|
+
.reactive-split { display: flex; gap: 20px; align-items: flex-start; flex-wrap: wrap; }
|
|
119
|
+
.reactive-demo { flex: 1; min-width: 180px; }
|
|
120
|
+
.reactive-snippet {
|
|
121
|
+
flex: 1;
|
|
122
|
+
min-width: 220px;
|
|
123
|
+
font-size: 12px;
|
|
124
|
+
background: #1e293b;
|
|
125
|
+
color: #e2e8f0;
|
|
126
|
+
border-radius: var(--radius-sm);
|
|
127
|
+
padding: 14px;
|
|
128
|
+
overflow-x: auto;
|
|
129
|
+
border: none;
|
|
130
|
+
}
|
|
131
|
+
.reactive-snippet pre { margin: 0; font-family: ui-monospace, monospace; white-space: pre-wrap; word-break: break-word; line-height: 1.5; }
|
|
132
|
+
</style>
|
|
133
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
134
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
135
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
136
|
+
</head>
|
|
137
|
+
<body>
|
|
138
|
+
<h1>Enigmatic Webapp</h1>
|
|
139
|
+
|
|
140
|
+
<section>
|
|
141
|
+
<h2>Auth</h2>
|
|
142
|
+
<div class="row">
|
|
143
|
+
<span id="auth-status">Checking…</span>
|
|
144
|
+
<button onclick="window.login()">Login</button>
|
|
145
|
+
<button class="secondary" onclick="window.logout()">Logout</button>
|
|
146
|
+
</div>
|
|
147
|
+
</section>
|
|
148
|
+
|
|
149
|
+
<section>
|
|
150
|
+
<h2>Reactive state</h2>
|
|
151
|
+
<p>Edit the message; the custom element updates automatically.</p>
|
|
152
|
+
<div class="reactive-split">
|
|
153
|
+
<div class="reactive-demo">
|
|
154
|
+
<div class="row">
|
|
155
|
+
<input type="text" id="msg-input" placeholder="Message" value="World">
|
|
156
|
+
<button onclick="window.state.message = window.$('#msg-input').value">Update</button>
|
|
157
|
+
</div>
|
|
158
|
+
<p style="margin: 12px 0 0; font-size: 14px;"><hello-world data="message"></hello-world></p>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="reactive-snippet">
|
|
161
|
+
<pre><!-- HTML: data="message" binds to state.message -->
|
|
162
|
+
<hello-world data="message"></hello-world>
|
|
163
|
+
|
|
164
|
+
// JS: updating state re-renders the element
|
|
165
|
+
window.state.message = "World";</pre>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</section>
|
|
169
|
+
|
|
170
|
+
<section>
|
|
171
|
+
<h2>KV storage</h2>
|
|
172
|
+
<div class="row">
|
|
173
|
+
<input type="text" id="kv-key" placeholder="Key" value="greeting">
|
|
174
|
+
<input type="text" id="kv-value" placeholder="Value" value="Hello from KV">
|
|
175
|
+
</div>
|
|
176
|
+
<div class="row">
|
|
177
|
+
<button onclick="window.get(window.$('#kv-key').value).then(r => { window.$('#kv-result').textContent = JSON.stringify(r); window.$('#kv-result').classList.remove('error'); }).catch(e => { window.$('#kv-result').textContent = e.message; window.$('#kv-result').classList.add('error'); })">Get</button>
|
|
178
|
+
<button onclick="window.set(window.$('#kv-key').value, window.$('#kv-value').value).then(r => { window.$('#kv-result').textContent = JSON.stringify(r); window.$('#kv-result').classList.remove('error'); }).catch(e => { window.$('#kv-result').textContent = e.message; window.$('#kv-result').classList.add('error'); })">Set</button>
|
|
179
|
+
<button onclick="window.delete(window.$('#kv-key').value).then(r => { window.$('#kv-result').textContent = JSON.stringify(r); window.$('#kv-result').classList.remove('error'); }).catch(e => { window.$('#kv-result').textContent = e.message; window.$('#kv-result').classList.add('error'); })">Delete</button>
|
|
180
|
+
</div>
|
|
181
|
+
<div id="kv-result"></div>
|
|
182
|
+
</section>
|
|
183
|
+
|
|
184
|
+
<section>
|
|
185
|
+
<h2>Files</h2>
|
|
186
|
+
<file-widget></file-widget>
|
|
187
|
+
</section>
|
|
188
|
+
|
|
189
|
+
<script src="client.js"></script>
|
|
190
|
+
<script src="custom.js"></script>
|
|
191
|
+
|
|
192
|
+
<script>
|
|
193
|
+
window.state.message = window.$('#msg-input').value;
|
|
194
|
+
|
|
195
|
+
window.me().then(function(u) {
|
|
196
|
+
window.$('#auth-status').textContent = u ? ('Logged in as ' + (u.email || u.sub)) : 'Not logged in';
|
|
197
|
+
}).catch(function() {
|
|
198
|
+
window.$('#auth-status').textContent = 'Not logged in';
|
|
199
|
+
});
|
|
200
|
+
</script>
|
|
201
|
+
</body>
|
|
202
|
+
</html>
|
package/client/public/client.js
CHANGED
|
@@ -1,91 +1,74 @@
|
|
|
1
|
-
const D = document, W = window, Enc = encodeURIComponent;
|
|
2
|
-
|
|
3
|
-
// 1. Unified Render Logic (Handles both State & Custom Elements)
|
|
4
1
|
const ren = async (el, v) => {
|
|
5
|
-
const f =
|
|
2
|
+
const f = window.custom?.[el.tagName.toLowerCase()];
|
|
6
3
|
if (f) {
|
|
7
4
|
const dataAttr = el.getAttribute('data');
|
|
8
|
-
const val = v !== undefined ? v : (dataAttr ?
|
|
5
|
+
const val = v !== undefined ? v : (dataAttr ? window.state[dataAttr] : undefined);
|
|
9
6
|
try {
|
|
10
7
|
if (f.render) {
|
|
11
8
|
el.innerHTML = await f.render.call(f, val);
|
|
12
9
|
} else if (typeof f === 'function') {
|
|
13
10
|
el.innerHTML = await f(val);
|
|
14
11
|
}
|
|
15
|
-
} catch(e) {
|
|
12
|
+
} catch (e) {
|
|
13
|
+
console.error(e);
|
|
14
|
+
}
|
|
16
15
|
}
|
|
17
16
|
};
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
const cProx = new Proxy({}, {
|
|
21
|
-
set(t, p, v) {
|
|
22
|
-
t[p] = v;
|
|
23
|
-
setTimeout(() => {
|
|
24
|
-
if (W.$$ && D.body) {
|
|
25
|
-
W.$$(p).forEach(el => ren(el));
|
|
26
|
-
}
|
|
27
|
-
}, 0);
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
Object.defineProperty(W, 'custom', {
|
|
32
|
-
get: () => cProx,
|
|
33
|
-
set: v => {
|
|
34
|
-
Object.keys(v || {}).forEach(k => cProx[k] = v[k]);
|
|
35
|
-
// Defer initialization to ensure DOM and functions are ready
|
|
36
|
-
setTimeout(() => {
|
|
37
|
-
if (W.initCustomElements && D.body) W.initCustomElements();
|
|
38
|
-
}, 50);
|
|
39
|
-
},
|
|
40
|
-
configurable: true
|
|
41
|
-
});
|
|
18
|
+
window.custom = {};
|
|
42
19
|
|
|
20
|
+
// 2. State proxy
|
|
43
21
|
const sProx = new Proxy({}, {
|
|
44
22
|
set(o, p, v) {
|
|
45
23
|
o[p] = v;
|
|
46
|
-
|
|
24
|
+
window.$$(`[data="${p}"]`).forEach(el => ren(el, v));
|
|
47
25
|
return true;
|
|
48
26
|
}
|
|
49
27
|
});
|
|
50
28
|
|
|
51
|
-
//
|
|
52
|
-
const req = (
|
|
53
|
-
|
|
54
|
-
|
|
29
|
+
// 4. API helpers
|
|
30
|
+
const req = (method, key, body) =>
|
|
31
|
+
fetch(`${window.api_url}/${key ? encodeURIComponent(key) : ''}`, {
|
|
32
|
+
method,
|
|
33
|
+
body: body instanceof Blob || typeof body === 'string' ? body : JSON.stringify(body),
|
|
34
|
+
credentials: 'include',
|
|
35
|
+
});
|
|
55
36
|
|
|
56
37
|
const toJson = (r) => {
|
|
57
38
|
const ct = (r.headers.get('content-type') || '').toLowerCase();
|
|
58
|
-
if (!ct.includes('application/json'))
|
|
39
|
+
if (!ct.includes('application/json')) {
|
|
40
|
+
return r.text().then((t) => { throw new Error('Server returned non-JSON (HTML?): ' + (t.slice(0, 60) || r.status)); });
|
|
41
|
+
}
|
|
59
42
|
return r.json();
|
|
60
43
|
};
|
|
61
44
|
|
|
62
|
-
Object.assign(
|
|
63
|
-
$: s =>
|
|
64
|
-
$$: s =>
|
|
65
|
-
$c: s => $0.closest(s),
|
|
45
|
+
Object.assign(window, {
|
|
46
|
+
$: (s) => document.querySelector(s),
|
|
47
|
+
$$: (s) => document.querySelectorAll(s),
|
|
48
|
+
$c: (s) => $0.closest(s),
|
|
66
49
|
state: sProx,
|
|
67
|
-
get: k => req('GET', k).then(toJson),
|
|
50
|
+
get: (k) => req('GET', k).then(toJson),
|
|
68
51
|
set: (k, v) => req('POST', k, v).then(toJson),
|
|
69
52
|
put: (k, v) => req('PUT', k, v).then(toJson),
|
|
70
|
-
delete: k => req('DELETE', k).then(toJson),
|
|
71
|
-
purge: k => req('PURGE', k).then(toJson),
|
|
53
|
+
delete: (k) => req('DELETE', k).then(toJson),
|
|
54
|
+
purge: (k) => req('PURGE', k).then(toJson),
|
|
72
55
|
list: () => req('PROPFIND').then(toJson),
|
|
73
|
-
me: () => fetch(`${
|
|
74
|
-
login: () =>
|
|
75
|
-
logout: () =>
|
|
56
|
+
me: () => fetch(`${window.api_url}/me`, { credentials: 'include' }).then((r) => (r.ok ? r.json() : null)),
|
|
57
|
+
login: () => window.location.href = `${window.api_url}/login`,
|
|
58
|
+
logout: () => window.location.href = `${window.api_url}/logout`,
|
|
76
59
|
download: async (k) => {
|
|
77
60
|
const r = await req('PATCH', k);
|
|
78
61
|
if (!r.ok) throw new Error('Download failed');
|
|
79
|
-
const a =
|
|
62
|
+
const a = document.createElement('a');
|
|
80
63
|
a.href = URL.createObjectURL(await r.blob());
|
|
81
64
|
a.download = k;
|
|
82
65
|
a.click();
|
|
83
66
|
URL.revokeObjectURL(a.href);
|
|
84
67
|
},
|
|
85
68
|
initCustomElements: () => {
|
|
86
|
-
if (!
|
|
87
|
-
Object.keys(
|
|
88
|
-
const elements =
|
|
69
|
+
if (!document.body) return;
|
|
70
|
+
Object.keys(window.custom || {}).forEach((t) => {
|
|
71
|
+
const elements = window.$$(t);
|
|
89
72
|
if (elements.length > 0) {
|
|
90
73
|
elements.forEach(el => ren(el));
|
|
91
74
|
}
|
|
@@ -93,33 +76,31 @@ Object.assign(W, {
|
|
|
93
76
|
}
|
|
94
77
|
});
|
|
95
78
|
|
|
96
|
-
//
|
|
79
|
+
// 5. Boot
|
|
97
80
|
const boot = () => {
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
setTimeout(() => W.initCustomElements(), 10);
|
|
81
|
+
if (window.initCustomElements) {
|
|
82
|
+
window.initCustomElements();
|
|
83
|
+
setTimeout(() => window.initCustomElements(), 10);
|
|
102
84
|
}
|
|
103
|
-
if (
|
|
85
|
+
if (document.body) {
|
|
104
86
|
new MutationObserver((mutations) => {
|
|
105
|
-
mutations.forEach(m => {
|
|
106
|
-
m.addedNodes.forEach(node => {
|
|
107
|
-
if (node.nodeType === 1) {
|
|
87
|
+
mutations.forEach((m) => {
|
|
88
|
+
m.addedNodes.forEach((node) => {
|
|
89
|
+
if (node.nodeType === 1) {
|
|
108
90
|
const tag = node.tagName?.toLowerCase();
|
|
109
|
-
if (tag &&
|
|
110
|
-
|
|
111
|
-
node.querySelectorAll && Array.from(node.querySelectorAll('*')).forEach(child => {
|
|
91
|
+
if (tag && window.custom?.[tag]) ren(node);
|
|
92
|
+
node.querySelectorAll && Array.from(node.querySelectorAll('*')).forEach((child) => {
|
|
112
93
|
const childTag = child.tagName?.toLowerCase();
|
|
113
|
-
if (childTag &&
|
|
94
|
+
if (childTag && window.custom?.[childTag]) ren(child);
|
|
114
95
|
});
|
|
115
96
|
}
|
|
116
97
|
});
|
|
117
98
|
});
|
|
118
|
-
}).observe(
|
|
99
|
+
}).observe(document.body, { childList: true, subtree: true });
|
|
119
100
|
}
|
|
120
101
|
};
|
|
121
|
-
if (
|
|
122
|
-
|
|
102
|
+
if (document.readyState === 'loading') {
|
|
103
|
+
document.addEventListener('DOMContentLoaded', boot);
|
|
123
104
|
} else {
|
|
124
105
|
setTimeout(boot, 0);
|
|
125
|
-
}
|
|
106
|
+
}
|
package/client/public/index.html
CHANGED
|
@@ -3,195 +3,412 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>Enigmatic
|
|
6
|
+
<title>Enigmatic - Lightweight JavaScript Library</title>
|
|
7
7
|
<style>
|
|
8
8
|
:root {
|
|
9
|
-
--bg: #
|
|
9
|
+
--bg: #f8fafc;
|
|
10
10
|
--surface: #ffffff;
|
|
11
11
|
--text: #0f172a;
|
|
12
12
|
--text-muted: #64748b;
|
|
13
13
|
--accent: #6366f1;
|
|
14
14
|
--accent-hover: #4f46e5;
|
|
15
15
|
--border: #e2e8f0;
|
|
16
|
-
--
|
|
17
|
-
--
|
|
18
|
-
--radius-sm: 8px;
|
|
19
|
-
--shadow: 0 1px 3px rgba(0,0,0,.06);
|
|
20
|
-
--shadow-md: 0 4px 12px rgba(0,0,0,.06);
|
|
16
|
+
--code-bg: #1e293b;
|
|
17
|
+
--code-text: #e2e8f0;
|
|
21
18
|
}
|
|
22
|
-
* { box-sizing: border-box; }
|
|
19
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
23
20
|
body {
|
|
24
21
|
font-family: "Inter", system-ui, -apple-system, sans-serif;
|
|
25
|
-
font-size:
|
|
22
|
+
font-size: 16px;
|
|
26
23
|
line-height: 1.6;
|
|
27
|
-
max-width: 600px;
|
|
28
|
-
margin: 0 auto;
|
|
29
|
-
padding: 32px 24px;
|
|
30
24
|
background: var(--bg);
|
|
31
25
|
color: var(--text);
|
|
32
26
|
-webkit-font-smoothing: antialiased;
|
|
33
27
|
}
|
|
28
|
+
.container {
|
|
29
|
+
max-width: 1200px;
|
|
30
|
+
margin: 0 auto;
|
|
31
|
+
padding: 0 24px;
|
|
32
|
+
}
|
|
33
|
+
header {
|
|
34
|
+
background: var(--surface);
|
|
35
|
+
border-bottom: 1px solid var(--border);
|
|
36
|
+
padding: 24px 0;
|
|
37
|
+
margin-bottom: 48px;
|
|
38
|
+
}
|
|
34
39
|
h1 {
|
|
35
|
-
font-size:
|
|
40
|
+
font-size: 2.5rem;
|
|
36
41
|
font-weight: 700;
|
|
37
42
|
letter-spacing: -0.02em;
|
|
38
|
-
margin:
|
|
39
|
-
|
|
43
|
+
margin-bottom: 12px;
|
|
44
|
+
background: linear-gradient(135deg, var(--accent), #8b5cf6);
|
|
45
|
+
-webkit-background-clip: text;
|
|
46
|
+
-webkit-text-fill-color: transparent;
|
|
47
|
+
background-clip: text;
|
|
48
|
+
}
|
|
49
|
+
.tagline {
|
|
50
|
+
font-size: 1.25rem;
|
|
51
|
+
color: var(--text-muted);
|
|
52
|
+
margin-bottom: 32px;
|
|
53
|
+
}
|
|
54
|
+
.badge {
|
|
55
|
+
display: inline-block;
|
|
56
|
+
padding: 4px 12px;
|
|
57
|
+
background: var(--accent);
|
|
58
|
+
color: white;
|
|
59
|
+
border-radius: 6px;
|
|
60
|
+
font-size: 0.875rem;
|
|
61
|
+
font-weight: 500;
|
|
62
|
+
margin-bottom: 24px;
|
|
40
63
|
}
|
|
41
64
|
section {
|
|
42
65
|
background: var(--surface);
|
|
43
|
-
border-radius:
|
|
44
|
-
padding:
|
|
45
|
-
margin-bottom:
|
|
46
|
-
box-shadow:
|
|
66
|
+
border-radius: 12px;
|
|
67
|
+
padding: 32px;
|
|
68
|
+
margin-bottom: 32px;
|
|
69
|
+
box-shadow: 0 1px 3px rgba(0,0,0,.06);
|
|
47
70
|
border: 1px solid var(--border);
|
|
48
71
|
}
|
|
49
|
-
|
|
50
|
-
font-size:
|
|
72
|
+
h2 {
|
|
73
|
+
font-size: 1.5rem;
|
|
51
74
|
font-weight: 600;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
color: var(--text-muted);
|
|
55
|
-
margin: 0 0 14px;
|
|
75
|
+
margin-bottom: 16px;
|
|
76
|
+
color: var(--text);
|
|
56
77
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
font-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
border: 1px solid var(--border);
|
|
63
|
-
background: var(--input-bg);
|
|
78
|
+
h3 {
|
|
79
|
+
font-size: 1.125rem;
|
|
80
|
+
font-weight: 600;
|
|
81
|
+
margin-top: 24px;
|
|
82
|
+
margin-bottom: 12px;
|
|
64
83
|
color: var(--text);
|
|
65
|
-
width: 100%;
|
|
66
|
-
max-width: 200px;
|
|
67
|
-
transition: border-color .15s, box-shadow .15s;
|
|
68
84
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
box-shadow: 0 0 0 3px rgba(99, 102, 241, .15);
|
|
85
|
+
p {
|
|
86
|
+
margin-bottom: 16px;
|
|
87
|
+
color: var(--text-muted);
|
|
73
88
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
ul, ol {
|
|
90
|
+
margin-left: 24px;
|
|
91
|
+
margin-bottom: 16px;
|
|
92
|
+
color: var(--text-muted);
|
|
93
|
+
}
|
|
94
|
+
li {
|
|
95
|
+
margin-bottom: 8px;
|
|
96
|
+
}
|
|
97
|
+
code {
|
|
98
|
+
background: var(--code-bg);
|
|
99
|
+
color: var(--code-text);
|
|
100
|
+
padding: 2px 6px;
|
|
101
|
+
border-radius: 4px;
|
|
102
|
+
font-family: ui-monospace, monospace;
|
|
103
|
+
font-size: 0.9em;
|
|
104
|
+
}
|
|
105
|
+
pre {
|
|
106
|
+
background: var(--code-bg);
|
|
107
|
+
color: var(--code-text);
|
|
108
|
+
padding: 20px;
|
|
109
|
+
border-radius: 8px;
|
|
110
|
+
overflow-x: auto;
|
|
111
|
+
margin: 16px 0;
|
|
112
|
+
font-family: ui-monospace, monospace;
|
|
113
|
+
font-size: 0.875rem;
|
|
114
|
+
line-height: 1.6;
|
|
115
|
+
}
|
|
116
|
+
pre code {
|
|
117
|
+
background: none;
|
|
118
|
+
padding: 0;
|
|
119
|
+
}
|
|
120
|
+
.btn {
|
|
121
|
+
display: inline-block;
|
|
122
|
+
padding: 12px 24px;
|
|
86
123
|
background: var(--accent);
|
|
87
|
-
color:
|
|
88
|
-
|
|
124
|
+
color: white;
|
|
125
|
+
text-decoration: none;
|
|
126
|
+
border-radius: 8px;
|
|
127
|
+
font-weight: 500;
|
|
128
|
+
transition: background 0.15s;
|
|
129
|
+
margin-right: 12px;
|
|
130
|
+
margin-bottom: 12px;
|
|
131
|
+
}
|
|
132
|
+
.btn:hover {
|
|
133
|
+
background: var(--accent-hover);
|
|
89
134
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
background: var(--input-bg);
|
|
135
|
+
.btn-secondary {
|
|
136
|
+
background: var(--border);
|
|
93
137
|
color: var(--text);
|
|
94
|
-
border: 1px solid var(--border);
|
|
95
138
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
min-height: 24px;
|
|
139
|
+
.btn-secondary:hover {
|
|
140
|
+
background: #cbd5e1;
|
|
141
|
+
}
|
|
142
|
+
.features {
|
|
143
|
+
display: grid;
|
|
144
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
145
|
+
gap: 24px;
|
|
146
|
+
margin-top: 24px;
|
|
147
|
+
}
|
|
148
|
+
.feature {
|
|
149
|
+
padding: 20px;
|
|
150
|
+
background: var(--bg);
|
|
151
|
+
border-radius: 8px;
|
|
110
152
|
border: 1px solid var(--border);
|
|
111
153
|
}
|
|
112
|
-
.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
border:
|
|
154
|
+
.feature h4 {
|
|
155
|
+
font-size: 1rem;
|
|
156
|
+
font-weight: 600;
|
|
157
|
+
margin-bottom: 8px;
|
|
158
|
+
color: var(--text);
|
|
159
|
+
}
|
|
160
|
+
.feature p {
|
|
161
|
+
font-size: 0.9375rem;
|
|
162
|
+
margin: 0;
|
|
163
|
+
}
|
|
164
|
+
table {
|
|
165
|
+
width: 100%;
|
|
166
|
+
border-collapse: collapse;
|
|
167
|
+
margin: 16px 0;
|
|
168
|
+
}
|
|
169
|
+
th, td {
|
|
170
|
+
padding: 12px;
|
|
171
|
+
text-align: left;
|
|
172
|
+
border-bottom: 1px solid var(--border);
|
|
173
|
+
}
|
|
174
|
+
th {
|
|
175
|
+
font-weight: 600;
|
|
176
|
+
color: var(--text);
|
|
177
|
+
background: var(--bg);
|
|
178
|
+
}
|
|
179
|
+
td {
|
|
180
|
+
color: var(--text-muted);
|
|
181
|
+
font-size: 0.9375rem;
|
|
182
|
+
}
|
|
183
|
+
.links {
|
|
184
|
+
display: flex;
|
|
185
|
+
gap: 16px;
|
|
186
|
+
flex-wrap: wrap;
|
|
187
|
+
margin-top: 24px;
|
|
188
|
+
}
|
|
189
|
+
footer {
|
|
190
|
+
text-align: center;
|
|
191
|
+
padding: 48px 0;
|
|
192
|
+
color: var(--text-muted);
|
|
193
|
+
font-size: 0.875rem;
|
|
125
194
|
}
|
|
126
|
-
.reactive-snippet pre { margin: 0; font-family: ui-monospace, monospace; white-space: pre-wrap; word-break: break-word; line-height: 1.5; }
|
|
127
195
|
</style>
|
|
128
196
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
129
197
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
130
198
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
131
199
|
</head>
|
|
132
200
|
<body>
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
201
|
+
<header>
|
|
202
|
+
<div class="container">
|
|
203
|
+
<span class="badge">v0.35.0</span>
|
|
204
|
+
<h1>Enigmatic</h1>
|
|
205
|
+
<p class="tagline">A lightweight client-side JavaScript library for DOM manipulation, reactive state management, and API interactions, with an optional Bun server for backend functionality.</p>
|
|
206
|
+
<div class="links">
|
|
207
|
+
<a href="api.html" class="btn">Try Demo</a>
|
|
208
|
+
<a href="https://www.npmjs.com/package/enigmatic" class="btn btn-secondary" target="_blank">View on npm</a>
|
|
209
|
+
</div>
|
|
141
210
|
</div>
|
|
142
|
-
</
|
|
143
|
-
|
|
144
|
-
<
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
211
|
+
</header>
|
|
212
|
+
|
|
213
|
+
<div class="container">
|
|
214
|
+
<section>
|
|
215
|
+
<h2>Quick Start</h2>
|
|
216
|
+
<h3>Using client.js via CDN</h3>
|
|
217
|
+
<p>Include <code>client.js</code> in any HTML file using the unpkg CDN:</p>
|
|
218
|
+
<pre><code><script src="https://unpkg.com/enigmatic"></script>
|
|
219
|
+
<script src="https://unpkg.com/enigmatic/client/public/custom.js"></script>
|
|
220
|
+
<script>
|
|
221
|
+
window.api_url = 'https://your-server.com';
|
|
222
|
+
window.state.message = 'Hello World';
|
|
223
|
+
</script></code></pre>
|
|
224
|
+
|
|
225
|
+
<h3>Using the Bun Server</h3>
|
|
226
|
+
<p>The Bun server provides a complete backend with:</p>
|
|
227
|
+
<ul>
|
|
228
|
+
<li><strong>Key-value storage</strong> – Per-user KV persisted as append-only JSONL</li>
|
|
229
|
+
<li><strong>File storage</strong> – Per-user files via Cloudflare R2 (or S3-compatible API)</li>
|
|
230
|
+
<li><strong>Authentication</strong> – Auth0 OAuth2 login/logout</li>
|
|
231
|
+
<li><strong>Static files</strong> – Served from <code>client/public/</code></li>
|
|
232
|
+
</ul>
|
|
233
|
+
|
|
234
|
+
<h3>Installation</h3>
|
|
235
|
+
<ol>
|
|
236
|
+
<li>Install <a href="https://bun.sh" target="_blank">Bun</a>:
|
|
237
|
+
<pre><code>curl -fsSL https://bun.sh/install | bash</code></pre>
|
|
238
|
+
</li>
|
|
239
|
+
<li>Install dependencies:
|
|
240
|
+
<pre><code>bun install</code></pre>
|
|
241
|
+
</li>
|
|
242
|
+
<li>TLS certificates: place <code>cert.pem</code> and <code>key.pem</code> in <code>server/certs/</code> for HTTPS</li>
|
|
243
|
+
</ol>
|
|
244
|
+
|
|
245
|
+
<h3>Running the Server</h3>
|
|
246
|
+
<pre><code>npm start
|
|
247
|
+
# or
|
|
248
|
+
npx enigmatic
|
|
249
|
+
# or with hot reload
|
|
250
|
+
npm run hot</code></pre>
|
|
251
|
+
<p>Server runs at <strong>https://localhost:3000</strong> (HTTPS is required for Auth0 cookies).</p>
|
|
252
|
+
</section>
|
|
253
|
+
|
|
254
|
+
<section>
|
|
255
|
+
<h2>Features</h2>
|
|
256
|
+
<div class="features">
|
|
257
|
+
<div class="feature">
|
|
258
|
+
<h4>DOM Utilities</h4>
|
|
259
|
+
<p>Simple selectors: <code>window.$</code>, <code>window.$$</code>, <code>window.$c</code></p>
|
|
260
|
+
</div>
|
|
261
|
+
<div class="feature">
|
|
262
|
+
<h4>Reactive State</h4>
|
|
263
|
+
<p>Proxy-based state management that automatically updates DOM elements</p>
|
|
264
|
+
</div>
|
|
265
|
+
<div class="feature">
|
|
266
|
+
<h4>Custom Elements</h4>
|
|
267
|
+
<p>Automatic initialization and reactive updates for custom HTML elements</p>
|
|
268
|
+
</div>
|
|
269
|
+
<div class="feature">
|
|
270
|
+
<h4>KV Storage</h4>
|
|
271
|
+
<p>Simple key-value operations: <code>get</code>, <code>set</code>, <code>delete</code></p>
|
|
272
|
+
</div>
|
|
273
|
+
<div class="feature">
|
|
274
|
+
<h4>File Storage</h4>
|
|
275
|
+
<p>Upload, download, list, and delete files via R2/S3-compatible storage</p>
|
|
276
|
+
</div>
|
|
277
|
+
<div class="feature">
|
|
278
|
+
<h4>Authentication</h4>
|
|
279
|
+
<p>Built-in Auth0 OAuth2 integration with login/logout</p>
|
|
152
280
|
</div>
|
|
153
|
-
<p style="margin: 12px 0 0; font-size: 14px;"><hello-world data="message"></hello-world></p>
|
|
154
281
|
</div>
|
|
155
|
-
|
|
156
|
-
<pre><!-- HTML: data="message" binds to state.message -->
|
|
157
|
-
<hello-world data="message"></hello-world>
|
|
282
|
+
</section>
|
|
158
283
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
284
|
+
<section>
|
|
285
|
+
<h2>API Endpoints</h2>
|
|
286
|
+
<table>
|
|
287
|
+
<thead>
|
|
288
|
+
<tr>
|
|
289
|
+
<th>Method</th>
|
|
290
|
+
<th>Path</th>
|
|
291
|
+
<th>Description</th>
|
|
292
|
+
</tr>
|
|
293
|
+
</thead>
|
|
294
|
+
<tbody>
|
|
295
|
+
<tr>
|
|
296
|
+
<td>GET</td>
|
|
297
|
+
<td><code>/</code></td>
|
|
298
|
+
<td>Serves index.html</td>
|
|
299
|
+
</tr>
|
|
300
|
+
<tr>
|
|
301
|
+
<td>GET</td>
|
|
302
|
+
<td><code>/login</code></td>
|
|
303
|
+
<td>Redirects to Auth0 login</td>
|
|
304
|
+
</tr>
|
|
305
|
+
<tr>
|
|
306
|
+
<td>GET</td>
|
|
307
|
+
<td><code>/callback</code></td>
|
|
308
|
+
<td>Auth0 OAuth callback</td>
|
|
309
|
+
</tr>
|
|
310
|
+
<tr>
|
|
311
|
+
<td>GET</td>
|
|
312
|
+
<td><code>/logout</code></td>
|
|
313
|
+
<td>Logs out and clears session</td>
|
|
314
|
+
</tr>
|
|
315
|
+
<tr>
|
|
316
|
+
<td>GET</td>
|
|
317
|
+
<td><code>/me</code></td>
|
|
318
|
+
<td>Current user or 401 (no auth)</td>
|
|
319
|
+
</tr>
|
|
320
|
+
<tr>
|
|
321
|
+
<td>GET</td>
|
|
322
|
+
<td><code>/{key}</code></td>
|
|
323
|
+
<td>KV get (auth required)</td>
|
|
324
|
+
</tr>
|
|
325
|
+
<tr>
|
|
326
|
+
<td>POST</td>
|
|
327
|
+
<td><code>/{key}</code></td>
|
|
328
|
+
<td>KV set (auth required)</td>
|
|
329
|
+
</tr>
|
|
330
|
+
<tr>
|
|
331
|
+
<td>DELETE</td>
|
|
332
|
+
<td><code>/{key}</code></td>
|
|
333
|
+
<td>KV delete (auth required)</td>
|
|
334
|
+
</tr>
|
|
335
|
+
<tr>
|
|
336
|
+
<td>PUT</td>
|
|
337
|
+
<td><code>/{key}</code></td>
|
|
338
|
+
<td>Upload file to R2 (auth required)</td>
|
|
339
|
+
</tr>
|
|
340
|
+
<tr>
|
|
341
|
+
<td>PURGE</td>
|
|
342
|
+
<td><code>/{key}</code></td>
|
|
343
|
+
<td>Delete file from R2 (auth required)</td>
|
|
344
|
+
</tr>
|
|
345
|
+
<tr>
|
|
346
|
+
<td>PROPFIND</td>
|
|
347
|
+
<td><code>/</code></td>
|
|
348
|
+
<td>List R2 files (auth required)</td>
|
|
349
|
+
</tr>
|
|
350
|
+
<tr>
|
|
351
|
+
<td>PATCH</td>
|
|
352
|
+
<td><code>/{key}</code></td>
|
|
353
|
+
<td>Download file from R2 (auth required)</td>
|
|
354
|
+
</tr>
|
|
355
|
+
</tbody>
|
|
356
|
+
</table>
|
|
357
|
+
</section>
|
|
164
358
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
<
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
<
|
|
359
|
+
<section>
|
|
360
|
+
<h2>Example Usage</h2>
|
|
361
|
+
<h3>Reactive State</h3>
|
|
362
|
+
<pre><code><hello-world data="message"></hello-world>
|
|
363
|
+
<script>
|
|
364
|
+
window.custom['hello-world'] = (data) => `Hello ${data}`;
|
|
365
|
+
window.state.message = "World"; // Automatically updates the element
|
|
366
|
+
</script></code></pre>
|
|
367
|
+
|
|
368
|
+
<h3>KV Storage</h3>
|
|
369
|
+
<pre><code>// Get value
|
|
370
|
+
const value = await window.get('my-key');
|
|
371
|
+
|
|
372
|
+
// Set value
|
|
373
|
+
await window.set('my-key', 'my-value');
|
|
374
|
+
|
|
375
|
+
// Delete key
|
|
376
|
+
await window.delete('my-key');</code></pre>
|
|
377
|
+
|
|
378
|
+
<h3>File Operations</h3>
|
|
379
|
+
<pre><code>// Upload file
|
|
380
|
+
await window.put('filename.txt', fileBlob);
|
|
381
|
+
|
|
382
|
+
// List files
|
|
383
|
+
const files = await window.list();
|
|
384
|
+
|
|
385
|
+
// Download file
|
|
386
|
+
await window.download('filename.txt');
|
|
387
|
+
|
|
388
|
+
// Delete file
|
|
389
|
+
await window.purge('filename.txt');</code></pre>
|
|
390
|
+
</section>
|
|
391
|
+
|
|
392
|
+
<section>
|
|
393
|
+
<h2>Environment Variables</h2>
|
|
394
|
+
<p>Create a <code>.env</code> file in the project root:</p>
|
|
395
|
+
<pre><code># Auth0
|
|
396
|
+
AUTH0_DOMAIN=your-tenant.auth0.com
|
|
397
|
+
AUTH0_CLIENT_ID=your-client-id
|
|
398
|
+
AUTH0_CLIENT_SECRET=your-client-secret
|
|
399
|
+
|
|
400
|
+
# Cloudflare R2 (optional, for file storage)
|
|
401
|
+
CLOUDFLARE_ACCESS_KEY_ID=your-access-key-id
|
|
402
|
+
CLOUDFLARE_SECRET_ACCESS_KEY=your-secret-access-key
|
|
403
|
+
CLOUDFLARE_BUCKET_NAME=your-bucket-name
|
|
404
|
+
CLOUDFLARE_PUBLIC_URL=https://your-account-id.r2.cloudflarestorage.com</code></pre>
|
|
405
|
+
</section>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
<footer>
|
|
409
|
+
<div class="container">
|
|
410
|
+
<p>MIT License • Built with Bun</p>
|
|
175
411
|
</div>
|
|
176
|
-
|
|
177
|
-
</section>
|
|
178
|
-
|
|
179
|
-
<section>
|
|
180
|
-
<h2>Files</h2>
|
|
181
|
-
<file-widget></file-widget>
|
|
182
|
-
</section>
|
|
183
|
-
|
|
184
|
-
<script src="client.js"></script>
|
|
185
|
-
<script src="custom.js"></script>
|
|
186
|
-
<script>
|
|
187
|
-
window.api_url = window.api_url || window.location.origin;
|
|
188
|
-
window.state.message = window.$('#msg-input').value;
|
|
189
|
-
|
|
190
|
-
window.me().then(function(u) {
|
|
191
|
-
window.$('#auth-status').textContent = u ? ('Logged in as ' + (u.email || u.sub)) : 'Not logged in';
|
|
192
|
-
}).catch(function() {
|
|
193
|
-
window.$('#auth-status').textContent = 'Not logged in';
|
|
194
|
-
});
|
|
195
|
-
</script>
|
|
412
|
+
</footer>
|
|
196
413
|
</body>
|
|
197
414
|
</html>
|
package/package.json
CHANGED
package/server/bun-server.js
CHANGED
|
@@ -8,9 +8,10 @@ const publicDir = join(dir, "..", "client", "public");
|
|
|
8
8
|
const kvDir = join(dir, "kv");
|
|
9
9
|
const sessions = new Map();
|
|
10
10
|
const userKv = {};
|
|
11
|
+
let site_origin = "";
|
|
11
12
|
|
|
12
13
|
const kvPath = (sub) => join(kvDir, `${String(sub).replace(/[^a-zA-Z0-9_-]/g, "_")}.jsonl`);
|
|
13
|
-
const json = (d, s = 200, h = {}) => new Response(JSON.stringify(d), { status: s, headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PURGE, PROPFIND, PATCH, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization, Cookie", "Access-Control-Allow-Credentials": "true", "Content-Type": "application/json", ...h } });
|
|
14
|
+
const json = (d, s = 200, h = {}, origin = null) => new Response(JSON.stringify(d), { status: s, headers: { "Access-Control-Allow-Origin": origin || "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PURGE, PROPFIND, PATCH, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization, Cookie", "Access-Control-Allow-Credentials": "true", "Content-Type": "application/json", ...h } });
|
|
14
15
|
const redir = (url, cookie) => new Response(null, { status: 302, headers: { Location: url, ...(cookie && { "Set-Cookie": cookie }) } });
|
|
15
16
|
|
|
16
17
|
async function getUserMap(sub) {
|
|
@@ -58,10 +59,11 @@ const s3 = new S3Client({
|
|
|
58
59
|
export default {
|
|
59
60
|
async fetch(req) {
|
|
60
61
|
const url = new URL(req.url), key = url.pathname.slice(1), cb = `${url.origin}/callback`;
|
|
62
|
+
const origin = req.headers.get("Origin") || url.origin;
|
|
61
63
|
const token = req.headers.get("Cookie")?.match(/token=([^;]+)/)?.[1];
|
|
62
64
|
const user = (Bun.env.TEST_MODE === "1" && token === Bun.env.TEST_SESSION_ID) ? { sub: "test-user" } : (token ? sessions.get(token) : null);
|
|
63
65
|
|
|
64
|
-
if (req.method === "OPTIONS") return json(null, 204);
|
|
66
|
+
if (req.method === "OPTIONS") return json(null, 204, {}, origin);
|
|
65
67
|
|
|
66
68
|
if (req.method === "GET") {
|
|
67
69
|
const p = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
|
|
@@ -71,47 +73,58 @@ export default {
|
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
if (url.pathname === "/login")
|
|
76
|
+
if (url.pathname === "/login") {
|
|
77
|
+
site_origin = req.headers.get("referer");
|
|
78
|
+
return Response.redirect(`https://${Bun.env.AUTH0_DOMAIN}/authorize?${new URLSearchParams({ response_type: "code", client_id: Bun.env.AUTH0_CLIENT_ID, redirect_uri: cb, scope: "openid email profile" })}`);
|
|
79
|
+
}
|
|
75
80
|
|
|
76
81
|
if (url.pathname === "/callback") {
|
|
77
82
|
const code = url.searchParams.get("code");
|
|
78
|
-
if (!code) return json({ error: "No code" }, 400);
|
|
83
|
+
if (!code) return json({ error: "No code" }, 400, {}, origin);
|
|
79
84
|
const tRes = await fetch(`https://${Bun.env.AUTH0_DOMAIN}/oauth/token`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ grant_type: "authorization_code", client_id: Bun.env.AUTH0_CLIENT_ID, client_secret: Bun.env.AUTH0_CLIENT_SECRET, code, redirect_uri: cb }) });
|
|
80
|
-
if (!tRes.ok) return json({ error: "Auth error" }, 401);
|
|
85
|
+
if (!tRes.ok) return json({ error: "Auth error" }, 401, {}, origin);
|
|
81
86
|
const tokens = await tRes.json();
|
|
82
87
|
const userInfo = await (await fetch(`https://${Bun.env.AUTH0_DOMAIN}/userinfo`, { headers: { Authorization: `Bearer ${tokens.access_token}` } })).json();
|
|
83
88
|
const sid = crypto.randomUUID();
|
|
84
89
|
sessions.set(sid, { ...userInfo, login_time: new Date().toISOString(), access_token_expires_at: tokens.expires_in ? new Date(Date.now() + tokens.expires_in * 1000).toISOString() : null });
|
|
85
|
-
return redir(url.origin, `token=${sid}; HttpOnly; Path=/; Secure; SameSite=
|
|
90
|
+
return redir(site_origin || url.origin, `token=${sid}; HttpOnly; Path=/; Secure; SameSite=None; Max-Age=86400`);
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
if (url.pathname === "/me") return user ? json(user) : json({ error: "Unauthorized" }, 401);
|
|
89
|
-
if (!token || !user) return json({ error: "Unauthorized" }, 401);
|
|
90
|
-
if (url.pathname === "/logout") { sessions.delete(token); return redir(url.origin, "token=; Max-Age=0; Path
|
|
93
|
+
if (url.pathname === "/me") return user ? json(user, 200, {}, origin) : json({ error: "Unauthorized" }, 401, {}, origin);
|
|
94
|
+
if (!token || !user) return json({ error: "Unauthorized" }, 401, {}, origin);
|
|
95
|
+
if (url.pathname === "/logout") { sessions.delete(token); return redir(url.origin, "token=; Max-Age=0; Path=/; Secure; SameSite=None"); }
|
|
91
96
|
|
|
92
97
|
const m = await getUserMap(user.sub);
|
|
93
98
|
switch (req.method) {
|
|
94
|
-
case "GET": return json(m.get(key) ?? null);
|
|
99
|
+
case "GET": return json(m.get(key) ?? null, 200, {}, origin);
|
|
95
100
|
case "POST":
|
|
96
101
|
const val = await req.text();
|
|
97
102
|
const v = (() => { try { return JSON.parse(val); } catch { return val; } })();
|
|
98
103
|
m.set(key, v);
|
|
99
104
|
await saveUserKv(user.sub, "update", key, v);
|
|
100
|
-
return json({ key, value: v });
|
|
101
|
-
case "DELETE": m.delete(key); await saveUserKv(user.sub, "delete", key); return json({ status: "Deleted" });
|
|
102
|
-
case "PUT": await s3.write(`${user.sub}/${key}`, req.body); return json({ status: "Saved to R2" });
|
|
103
|
-
case "PURGE": await s3.delete(`${user.sub}/${key}`); return json({ status: "Deleted from R2" });
|
|
105
|
+
return json({ key, value: v }, 200, {}, origin);
|
|
106
|
+
case "DELETE": m.delete(key); await saveUserKv(user.sub, "delete", key); return json({ status: "Deleted" }, 200, {}, origin);
|
|
107
|
+
case "PUT": await s3.write(`${user.sub}/${key}`, req.body); return json({ status: "Saved to R2" }, 200, {}, origin);
|
|
108
|
+
case "PURGE": await s3.delete(`${user.sub}/${key}`); return json({ status: "Deleted from R2" }, 200, {}, origin);
|
|
104
109
|
case "PROPFIND":
|
|
105
110
|
const list = await s3.list({ prefix: `${user.sub}/` });
|
|
106
111
|
const items = Array.isArray(list) ? list : (list?.contents || []);
|
|
107
|
-
return json(items.map((i) => ({ name: i.key?.split("/").pop() || i.name || i.Key, lastModified: i.lastModified || i.LastModified, size: i.size || i.Size || 0 })));
|
|
112
|
+
return json(items.map((i) => ({ name: i.key?.split("/").pop() || i.name || i.Key, lastModified: i.lastModified || i.LastModified, size: i.size || i.Size || 0 })), 200, {}, origin);
|
|
108
113
|
case "PATCH":
|
|
109
114
|
try {
|
|
110
|
-
if (!(await s3.exists(`${user.sub}/${key}`)))
|
|
115
|
+
if (!(await s3.exists(`${user.sub}/${key}`))) {
|
|
116
|
+
const headers = { "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Credentials": "true" };
|
|
117
|
+
return new Response(JSON.stringify({ error: "File not found" }), { status: 404, headers: { ...headers, "Content-Type": "application/json" } });
|
|
118
|
+
}
|
|
111
119
|
const f = await s3.file(`${user.sub}/${key}`);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
120
|
+
if (f) {
|
|
121
|
+
const headers = { ...f.headers, "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Credentials": "true" };
|
|
122
|
+
return new Response(f.stream(), { headers });
|
|
123
|
+
}
|
|
124
|
+
const headers = { "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Credentials": "true" };
|
|
125
|
+
return new Response(JSON.stringify({ error: "File not found" }), { status: 404, headers: { ...headers, "Content-Type": "application/json" } });
|
|
126
|
+
} catch (e) { return json({ error: "File not found", details: e.message }, 404, {}, origin); }
|
|
127
|
+
default: return json({ error: "Method not allowed" }, 405, {}, origin);
|
|
115
128
|
}
|
|
116
129
|
},
|
|
117
130
|
port: 3000,
|