enigmatic 0.26.0 → 0.29.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/CLIENT_JS_DOCS.md +322 -0
- package/__tests__/e2.test.js +231 -138
- package/__tests__/jest.config.js +7 -0
- package/{jest.setup.js → __tests__/jest.setup.js} +4 -0
- package/bun-server.js +130 -0
- package/package.json +12 -11
- package/public/client.js +98 -0
- package/public/custom.js +29 -0
- package/public/index.html +45 -0
- package/README.md +0 -218
- package/__tests__/enigmatic.test.js +0 -328
- package/components.js +0 -169
- package/e2.js +0 -38
- package/enigmatic.js +0 -248
- package/index.html +0 -34
- package/jest.config.js +0 -6
- /package/{enigmatic.css → public/client.css} +0 -0
- /package/{theme.css → public/theme.css} +0 -0
package/__tests__/e2.test.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
2
|
const path = require('path')
|
|
3
3
|
|
|
4
|
-
// Load
|
|
5
|
-
const
|
|
4
|
+
// Load client.js and custom.js into jsdom
|
|
5
|
+
const clientCode = fs.readFileSync(path.join(__dirname, '../public/client.js'), 'utf8')
|
|
6
|
+
const customCode = fs.readFileSync(path.join(__dirname, '../public/custom.js'), 'utf8')
|
|
6
7
|
|
|
7
|
-
describe('
|
|
8
|
+
describe('client.js', () => {
|
|
8
9
|
beforeEach(() => {
|
|
9
10
|
// Reset DOM
|
|
10
11
|
global.document.body.innerHTML = ''
|
|
@@ -13,205 +14,297 @@ describe('e2.js', () => {
|
|
|
13
14
|
// Clear window.custom
|
|
14
15
|
global.window.custom = {}
|
|
15
16
|
|
|
16
|
-
// Execute
|
|
17
|
-
eval(
|
|
17
|
+
// Execute custom.js first (defines window.custom)
|
|
18
|
+
eval(customCode)
|
|
19
|
+
// Execute client.js code
|
|
20
|
+
eval(clientCode)
|
|
18
21
|
})
|
|
19
22
|
|
|
20
|
-
describe('
|
|
21
|
-
test('
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
)
|
|
23
|
+
describe('$ and $$ selectors', () => {
|
|
24
|
+
test('$ selects single element', () => {
|
|
25
|
+
global.document.body.innerHTML = '<div id="test">Hello</div>'
|
|
26
|
+
expect(window.$('#test').textContent).toBe('Hello')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('$$ selects multiple elements', () => {
|
|
30
|
+
global.document.body.innerHTML = '<div class="item">1</div><div class="item">2</div>'
|
|
31
|
+
const items = window.$$('.item')
|
|
32
|
+
expect(items.length).toBe(2)
|
|
33
|
+
expect(items[0].textContent).toBe('1')
|
|
34
|
+
expect(items[1].textContent).toBe('2')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('$ returns null for non-existent element', () => {
|
|
38
|
+
global.document.body.innerHTML = '<div>Test</div>'
|
|
39
|
+
expect(window.$('#nonexistent')).toBeNull()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('$$ returns empty NodeList for non-existent elements', () => {
|
|
43
|
+
global.document.body.innerHTML = '<div>Test</div>'
|
|
44
|
+
expect(window.$$('.nonexistent').length).toBe(0)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('window API functions', () => {
|
|
49
|
+
let originalFetch
|
|
50
|
+
let originalLocation
|
|
32
51
|
|
|
33
|
-
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
originalFetch = global.fetch
|
|
54
|
+
global.fetch = jest.fn()
|
|
55
|
+
originalLocation = global.window.location
|
|
56
|
+
delete global.window.location
|
|
57
|
+
global.window.location = { href: '' }
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
global.fetch = originalFetch
|
|
62
|
+
global.window.location = originalLocation
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('window.get makes GET request with encoded key', async () => {
|
|
66
|
+
global.fetch.mockResolvedValueOnce({
|
|
67
|
+
ok: true,
|
|
68
|
+
json: () => Promise.resolve({ value: 'test' })
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const result = await window.get('test key')
|
|
34
72
|
|
|
35
73
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
36
|
-
|
|
37
|
-
expect.objectContaining({
|
|
38
|
-
method: 'GET',
|
|
39
|
-
headers: { 'Content-Type': 'application/json' },
|
|
40
|
-
credentials: 'include'
|
|
41
|
-
})
|
|
74
|
+
`${window.api_url}/test%20key`
|
|
42
75
|
)
|
|
43
|
-
expect(result
|
|
44
|
-
expect(result.status).toBe(200)
|
|
45
|
-
expect(result.statusText).toBe('OK')
|
|
46
|
-
expect(result.headers).toBeDefined()
|
|
76
|
+
expect(result).toEqual({ value: 'test' })
|
|
47
77
|
})
|
|
48
78
|
|
|
49
|
-
test('
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
79
|
+
test('window.set makes POST request with string value', async () => {
|
|
80
|
+
global.fetch.mockResolvedValueOnce({
|
|
81
|
+
ok: true,
|
|
82
|
+
json: () => Promise.resolve({ status: 'saved' })
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const result = await window.set('test', 'value')
|
|
86
|
+
|
|
87
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
88
|
+
`${window.api_url}/test`,
|
|
89
|
+
expect.objectContaining({
|
|
90
|
+
method: 'POST',
|
|
91
|
+
body: 'value'
|
|
60
92
|
})
|
|
61
93
|
)
|
|
94
|
+
expect(result).toEqual({ status: 'saved' })
|
|
95
|
+
})
|
|
62
96
|
|
|
63
|
-
|
|
64
|
-
|
|
97
|
+
test('window.set stringifies object values', async () => {
|
|
98
|
+
global.fetch.mockResolvedValueOnce({
|
|
99
|
+
ok: true,
|
|
100
|
+
json: () => Promise.resolve({ status: 'saved' })
|
|
65
101
|
})
|
|
66
102
|
|
|
103
|
+
await window.set('test', { key: 'value' })
|
|
104
|
+
|
|
67
105
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
68
|
-
|
|
106
|
+
`${window.api_url}/test`,
|
|
69
107
|
expect.objectContaining({
|
|
70
108
|
method: 'POST',
|
|
71
|
-
|
|
72
|
-
credentials: 'include',
|
|
73
|
-
body: JSON.stringify(requestBody)
|
|
109
|
+
body: JSON.stringify({ key: 'value' })
|
|
74
110
|
})
|
|
75
111
|
)
|
|
76
|
-
expect(result.data).toEqual(mockData)
|
|
77
|
-
expect(result.status).toBe(201)
|
|
78
|
-
expect(result.statusText).toBe('Created')
|
|
79
112
|
})
|
|
80
113
|
|
|
81
|
-
test('
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
114
|
+
test('window.delete makes DELETE request', async () => {
|
|
115
|
+
global.fetch.mockResolvedValueOnce({
|
|
116
|
+
ok: true,
|
|
117
|
+
json: () => Promise.resolve({ status: 'deleted' })
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const result = await window.delete('test')
|
|
121
|
+
|
|
122
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
123
|
+
`${window.api_url}/test`,
|
|
124
|
+
expect.objectContaining({
|
|
125
|
+
method: 'DELETE'
|
|
91
126
|
})
|
|
92
127
|
)
|
|
128
|
+
expect(result).toEqual({ status: 'deleted' })
|
|
129
|
+
})
|
|
93
130
|
|
|
94
|
-
|
|
95
|
-
|
|
131
|
+
test('window.put makes PUT request with string body', async () => {
|
|
132
|
+
global.fetch.mockResolvedValueOnce({
|
|
133
|
+
ok: true,
|
|
134
|
+
json: () => Promise.resolve({ status: 'saved' })
|
|
96
135
|
})
|
|
97
136
|
|
|
137
|
+
const result = await window.put('test', 'content')
|
|
138
|
+
|
|
98
139
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
99
|
-
|
|
140
|
+
`${window.api_url}/test`,
|
|
100
141
|
expect.objectContaining({
|
|
101
142
|
method: 'PUT',
|
|
102
|
-
|
|
103
|
-
credentials: 'include'
|
|
143
|
+
body: 'content'
|
|
104
144
|
})
|
|
105
145
|
)
|
|
106
|
-
expect(result
|
|
146
|
+
expect(result).toEqual({ status: 'saved' })
|
|
107
147
|
})
|
|
108
148
|
|
|
109
|
-
test('
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
ok: true,
|
|
115
|
-
status: 200,
|
|
116
|
-
statusText: 'OK',
|
|
117
|
-
headers: new Headers(),
|
|
118
|
-
json: () => Promise.resolve(mockData)
|
|
119
|
-
})
|
|
120
|
-
)
|
|
149
|
+
test('window.put stringifies object body', async () => {
|
|
150
|
+
global.fetch.mockResolvedValueOnce({
|
|
151
|
+
ok: true,
|
|
152
|
+
json: () => Promise.resolve({ status: 'saved' })
|
|
153
|
+
})
|
|
121
154
|
|
|
122
|
-
|
|
155
|
+
await window.put('test', { data: 'value' })
|
|
123
156
|
|
|
124
157
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
125
|
-
|
|
158
|
+
`${window.api_url}/test`,
|
|
126
159
|
expect.objectContaining({
|
|
127
|
-
method: '
|
|
128
|
-
|
|
129
|
-
credentials: 'include'
|
|
160
|
+
method: 'PUT',
|
|
161
|
+
body: JSON.stringify({ data: 'value' })
|
|
130
162
|
})
|
|
131
163
|
)
|
|
132
|
-
expect(result.data).toEqual(mockData)
|
|
133
164
|
})
|
|
134
165
|
|
|
135
|
-
test('
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
statusText: 'OK',
|
|
141
|
-
headers: new Headers(),
|
|
142
|
-
json: () => Promise.resolve({})
|
|
143
|
-
})
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
await window.fetchJson('GET', 'http://test.com/api', {
|
|
147
|
-
headers: { 'Authorization': 'Bearer token123' }
|
|
166
|
+
test('window.put handles Blob body', async () => {
|
|
167
|
+
const blob = new Blob(['test'], { type: 'text/plain' })
|
|
168
|
+
global.fetch.mockResolvedValueOnce({
|
|
169
|
+
ok: true,
|
|
170
|
+
json: () => Promise.resolve({ status: 'saved' })
|
|
148
171
|
})
|
|
149
172
|
|
|
173
|
+
await window.put('test', blob)
|
|
174
|
+
|
|
150
175
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
151
|
-
|
|
176
|
+
`${window.api_url}/test`,
|
|
152
177
|
expect.objectContaining({
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
178
|
+
method: 'PUT',
|
|
179
|
+
body: blob
|
|
156
180
|
})
|
|
157
181
|
)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('window.purge makes PURGE request', async () => {
|
|
185
|
+
global.fetch.mockResolvedValueOnce({
|
|
186
|
+
ok: true,
|
|
187
|
+
json: () => Promise.resolve({ status: 'deleted' })
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
const result = await window.purge('test')
|
|
191
|
+
|
|
192
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
193
|
+
`${window.api_url}/test`,
|
|
161
194
|
expect.objectContaining({
|
|
162
|
-
|
|
163
|
-
'Authorization': 'Bearer token123'
|
|
164
|
-
})
|
|
195
|
+
method: 'PURGE'
|
|
165
196
|
})
|
|
166
197
|
)
|
|
198
|
+
expect(result).toEqual({ status: 'deleted' })
|
|
167
199
|
})
|
|
168
200
|
|
|
169
|
-
test('
|
|
170
|
-
global.fetch
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
statusText: 'OK',
|
|
175
|
-
headers: new Headers(),
|
|
176
|
-
json: () => Promise.resolve({})
|
|
177
|
-
})
|
|
178
|
-
)
|
|
201
|
+
test('window.list makes PROPFIND request to base URL', async () => {
|
|
202
|
+
global.fetch.mockResolvedValueOnce({
|
|
203
|
+
ok: true,
|
|
204
|
+
json: () => Promise.resolve([{ name: 'file1' }])
|
|
205
|
+
})
|
|
179
206
|
|
|
180
|
-
await window.
|
|
207
|
+
const result = await window.list()
|
|
181
208
|
|
|
182
209
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
183
|
-
|
|
210
|
+
window.api_url,
|
|
184
211
|
expect.objectContaining({
|
|
185
|
-
|
|
212
|
+
method: 'PROPFIND'
|
|
186
213
|
})
|
|
187
214
|
)
|
|
215
|
+
expect(result).toEqual([{ name: 'file1' }])
|
|
188
216
|
})
|
|
189
217
|
|
|
190
|
-
test('
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
status: 404,
|
|
195
|
-
statusText: 'Not Found',
|
|
196
|
-
headers: new Headers(),
|
|
197
|
-
json: () => Promise.resolve({ error: 'Not found' })
|
|
198
|
-
})
|
|
199
|
-
)
|
|
218
|
+
test('window.login redirects to /login', () => {
|
|
219
|
+
window.login()
|
|
220
|
+
expect(global.window.location.href).toBe(`${window.api_url}/login`)
|
|
221
|
+
})
|
|
200
222
|
|
|
201
|
-
|
|
223
|
+
test('window.logout redirects to /logout', () => {
|
|
224
|
+
window.logout()
|
|
225
|
+
expect(global.window.location.href).toBe(`${window.api_url}/logout`)
|
|
226
|
+
})
|
|
227
|
+
})
|
|
202
228
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
expect(
|
|
229
|
+
describe('window.custom components', () => {
|
|
230
|
+
test('hello-world component is a function', () => {
|
|
231
|
+
expect(typeof window.custom['hello-world']).toBe('function')
|
|
206
232
|
})
|
|
207
233
|
|
|
208
|
-
test('
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
234
|
+
test('hello-world component renders correctly', () => {
|
|
235
|
+
const result = window.custom['hello-world']('Test')
|
|
236
|
+
expect(result).toBe('Hello Test')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test('hello-world-2 component has prop and render methods', () => {
|
|
240
|
+
expect(window.custom['hello-world-2']).toBeDefined()
|
|
241
|
+
expect(typeof window.custom['hello-world-2'].prop).toBe('function')
|
|
242
|
+
expect(typeof window.custom['hello-world-2'].render).toBe('function')
|
|
243
|
+
})
|
|
212
244
|
|
|
213
|
-
|
|
245
|
+
test('hello-world-2 component prop works', () => {
|
|
246
|
+
const result = window.custom['hello-world-2'].prop('Test')
|
|
247
|
+
expect(result).toBe('Test World')
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test('hello-world-2 component render works', () => {
|
|
251
|
+
const result = window.custom['hello-world-2'].render('Test')
|
|
252
|
+
expect(result).toBe('Test World')
|
|
214
253
|
})
|
|
215
254
|
})
|
|
216
|
-
})
|
|
217
255
|
|
|
256
|
+
|
|
257
|
+
describe('window.state proxy', () => {
|
|
258
|
+
test('state.set updates DOM elements with data attribute', () => {
|
|
259
|
+
global.document.body.innerHTML = '<hello-world data="name"></hello-world>'
|
|
260
|
+
|
|
261
|
+
window.state.name = 'John'
|
|
262
|
+
|
|
263
|
+
const el = global.document.querySelector('hello-world')
|
|
264
|
+
expect(el.innerHTML).toBe('Hello John')
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
test('state.set updates multiple elements with same data attribute', () => {
|
|
268
|
+
global.document.body.innerHTML = '<hello-world data="name"></hello-world><hello-world data="name"></hello-world>'
|
|
269
|
+
|
|
270
|
+
window.state.name = 'Jane'
|
|
271
|
+
|
|
272
|
+
const els = global.document.querySelectorAll('hello-world')
|
|
273
|
+
expect(els[0].innerHTML).toBe('Hello Jane')
|
|
274
|
+
expect(els[1].innerHTML).toBe('Hello Jane')
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
test('state.set works with object components', () => {
|
|
278
|
+
global.document.body.innerHTML = '<hello-world-2 data="test"></hello-world-2>'
|
|
279
|
+
|
|
280
|
+
window.state.test = 'Hello'
|
|
281
|
+
|
|
282
|
+
const el = global.document.querySelector('hello-world-2')
|
|
283
|
+
expect(el.innerHTML).toBe('Hello World')
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
test('state.set stores value in proxy object', () => {
|
|
287
|
+
window.state.test = 'value'
|
|
288
|
+
expect(window.state.test).toBe('value')
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
test('state.set handles multiple properties', () => {
|
|
292
|
+
global.document.body.innerHTML = '<hello-world data="a"></hello-world><hello-world data="b"></hello-world>'
|
|
293
|
+
|
|
294
|
+
window.state.a = 'A'
|
|
295
|
+
window.state.b = 'B'
|
|
296
|
+
|
|
297
|
+
expect(global.document.querySelector('[data="a"]').innerHTML).toBe('Hello A')
|
|
298
|
+
expect(global.document.querySelector('[data="b"]').innerHTML).toBe('Hello B')
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
test('state.set does not update elements without matching data attribute', () => {
|
|
302
|
+
global.document.body.innerHTML = '<hello-world data="name"></hello-world><div data="other">Original</div>'
|
|
303
|
+
|
|
304
|
+
window.state.name = 'John'
|
|
305
|
+
|
|
306
|
+
expect(global.document.querySelector('[data="name"]').innerHTML).toBe('Hello John')
|
|
307
|
+
expect(global.document.querySelector('[data="other"]').innerHTML).toBe('Original')
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
})
|
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,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
"name": "enigmatic",
|
|
3
|
+
"version": "0.29.0",
|
|
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
|
+
}
|
|
13
14
|
}
|