@alstar/studio 0.0.0-beta.10 → 0.0.0-beta.11

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.
Files changed (43) hide show
  1. package/api/api-key.ts +40 -44
  2. package/api/auth.ts +66 -0
  3. package/api/backup.ts +30 -28
  4. package/api/block.ts +109 -110
  5. package/api/index.ts +17 -10
  6. package/api/mcp.ts +39 -42
  7. package/components/AdminPanel.ts +17 -4
  8. package/components/Backup.ts +5 -2
  9. package/components/BlockFieldRenderer.ts +6 -2
  10. package/components/Entries.ts +2 -2
  11. package/components/FieldRenderer.ts +1 -1
  12. package/components/Settings.ts +21 -18
  13. package/components/SiteLayout.ts +8 -5
  14. package/components/Users.ts +46 -0
  15. package/components/fields/Markdown.ts +1 -1
  16. package/components/fields/Slug.ts +113 -0
  17. package/components/fields/Text.ts +1 -1
  18. package/components/fields/index.ts +4 -1
  19. package/components/icons.ts +36 -0
  20. package/index.ts +40 -12
  21. package/package.json +6 -3
  22. package/pages/entry/[id].ts +1 -3
  23. package/pages/error.ts +14 -0
  24. package/pages/index.ts +1 -1
  25. package/pages/login.ts +21 -0
  26. package/pages/register.ts +33 -0
  27. package/pages/settings.ts +1 -3
  28. package/public/studio/css/settings.css +4 -0
  29. package/public/studio/js/markdown-editor.js +1 -1
  30. package/public/studio/js/sortable-list.js +1 -1
  31. package/public/studio/main.css +5 -0
  32. package/public/studio/main.js +12 -0
  33. package/queries/block-2.ts +339 -0
  34. package/queries/getBlockTrees-2.ts +71 -0
  35. package/queries/index.ts +1 -1
  36. package/readme.md +2 -2
  37. package/schema.sql +18 -0
  38. package/schemas.ts +11 -1
  39. package/types.ts +11 -0
  40. package/utils/auth.ts +54 -0
  41. package/utils/create-hash.ts +9 -0
  42. package/utils/define.ts +1 -3
  43. package/utils/startup-log.ts +15 -6
package/api/api-key.ts CHANGED
@@ -8,66 +8,62 @@ import crypto from 'node:crypto'
8
8
  import { stripNewlines } from '../utils/strip-newlines.ts'
9
9
  import { sql } from '../utils/sql.ts'
10
10
  import Settings from '../components/Settings.ts'
11
+ import { createHash } from '../utils/create-hash.ts'
11
12
 
12
- export default () => {
13
- const app = new Hono<{ Bindings: HttpBindings }>()
13
+ const app = new Hono<{ Bindings: HttpBindings }>()
14
14
 
15
- app.post('/api-key', async (c) => {
16
- return streamSSE(c, async (stream) => {
17
- const formData = await c.req.formData()
18
- const data = Object.fromEntries(formData.entries())
15
+ app.post('/api-key', async (c) => {
16
+ return streamSSE(c, async (stream) => {
17
+ const formData = await c.req.formData()
18
+ const data = Object.fromEntries(formData.entries())
19
19
 
20
- if (!data) return
20
+ if (!data) return
21
21
 
22
- const apiKey = crypto.randomUUID()
23
- const hash = crypto.createHash('sha256')
22
+ const apiKey = crypto.randomUUID()
24
23
 
25
- hash.update(apiKey)
24
+ const hash = createHash(apiKey)
26
25
 
27
- const digest = hash.digest().toString('base64')
26
+ const xs = (length: number) => '*'.repeat(length)
28
27
 
29
- const xs = (length: number) => '*'.repeat(length)
30
-
31
- db.insertInto('api_keys', {
32
- name: data.name?.toString(),
33
- value: digest,
34
- hint: `${apiKey.substring(0, 8)}-${xs(4)}-${xs(4)}-${xs(4)}-${xs(12)}`,
35
- })
28
+ db.insertInto('api_keys', {
29
+ name: data.name?.toString(),
30
+ value: hash,
31
+ hint: `${apiKey.substring(0, 8)}-${xs(4)}-${xs(4)}-${xs(4)}-${xs(12)}`,
32
+ })
36
33
 
37
- await stream.writeSSE({
38
- event: 'datastar-patch-signals',
39
- data: `signals { apiKey: '${apiKey}', name: '' }`,
40
- })
34
+ await stream.writeSSE({
35
+ event: 'datastar-patch-signals',
36
+ data: `signals { apiKey: '${apiKey}', name: '' }`,
37
+ })
41
38
 
42
- await stream.writeSSE({
43
- event: 'datastar-patch-elements',
44
- data: `elements ${stripNewlines(Settings())}`,
45
- })
39
+ await stream.writeSSE({
40
+ event: 'datastar-patch-elements',
41
+ data: `elements ${stripNewlines(Settings())}`,
46
42
  })
47
43
  })
44
+ })
48
45
 
49
- app.delete('/api-key', async (c) => {
50
- return streamSSE(c, async (stream) => {
51
- const formData = await c.req.formData()
46
+ app.delete('/api-key', async (c) => {
47
+ return streamSSE(c, async (stream) => {
48
+ const formData = await c.req.formData()
52
49
 
53
- const value = formData.get('value')?.toString()
50
+ const value = formData.get('value')?.toString()
54
51
 
55
- if (!value) return
52
+ if (!value) return
56
53
 
57
- db.database
58
- .prepare(sql`
59
- delete from api_keys
60
- where
61
- value = ?
62
- `)
63
- .run(value)
54
+ db.database
55
+ .prepare(sql`
56
+ delete from api_keys
57
+ where
58
+ value = ?
59
+ `)
60
+ .run(value)
64
61
 
65
- await stream.writeSSE({
66
- event: 'datastar-patch-elements',
67
- data: `elements ${stripNewlines(Settings())}`,
68
- })
62
+ await stream.writeSSE({
63
+ event: 'datastar-patch-elements',
64
+ data: `elements ${stripNewlines(Settings())}`,
69
65
  })
70
66
  })
67
+ })
71
68
 
72
- return app
73
- }
69
+ export const apiKeyRoutes = app
package/api/auth.ts ADDED
@@ -0,0 +1,66 @@
1
+ import { type HttpBindings } from '@hono/node-server'
2
+ import { Hono } from 'hono'
3
+ import { createHash } from '../utils/create-hash.ts'
4
+ import { db } from '@alstar/db'
5
+ import { streamSSE } from 'hono/streaming'
6
+ import { sql } from '../utils/sql.ts'
7
+ import { setCookie } from 'hono/cookie'
8
+
9
+ const app = new Hono<{ Bindings: HttpBindings }>()
10
+
11
+ app.post('/register', async (c) => {
12
+ return streamSSE(c, async (stream) => {
13
+ const formData = await c.req.formData()
14
+ const data = Object.fromEntries(formData.entries())
15
+
16
+ if (!data || !data.email || !data.password) return
17
+
18
+ const hash = createHash(data.password.toString())
19
+
20
+ db.insertInto('users', {
21
+ email: data.email?.toString(),
22
+ hash: hash,
23
+ })
24
+
25
+ await stream.writeSSE({
26
+ event: 'datastar-patch-signals',
27
+ data: `signals { status: 200, message: 'User "${data.email}" created successfully' }`,
28
+ })
29
+ })
30
+ })
31
+
32
+ app.post('/login', async (c) => {
33
+ const formData = await c.req.formData()
34
+ const data = Object.fromEntries(formData.entries())
35
+
36
+ if (!data || !data.email || !data.password) return
37
+
38
+ const user = db.database
39
+ .prepare(sql`
40
+ select
41
+ *
42
+ from
43
+ users
44
+ where
45
+ email = ?
46
+ `)
47
+ .get(data.email.toString())
48
+
49
+ if (!user) {
50
+ return c.json({ status: 404, message: 'No user with that email' })
51
+ }
52
+
53
+ const passwordHash = createHash(data.password.toString())
54
+
55
+ if (passwordHash !== user.hash) {
56
+ return c.json({ status: 401, message: 'Wrong password' })
57
+ }
58
+
59
+ setCookie(c, 'login', 'yes')
60
+
61
+ return c.redirect('/studio')
62
+
63
+ // return c.json({ status: 200, message: 'Logged in!' })
64
+ })
65
+
66
+ export const authRoutes = app
package/api/backup.ts CHANGED
@@ -1,38 +1,40 @@
1
- import { type HttpBindings } from '@hono/node-server'
1
+ import fsp from 'node:fs/promises'
2
+ import { backup } from 'node:sqlite'
3
+
2
4
  import { Hono } from 'hono'
5
+ import { type HttpBindings } from '@hono/node-server'
3
6
  import { streamSSE } from 'hono/streaming'
4
- import { DatabaseSync } from 'node:sqlite'
5
-
6
- import { stripNewlines } from '../utils/strip-newlines.ts'
7
7
  import { db } from '@alstar/db'
8
8
 
9
- export default () => {
10
- const app = new Hono<{ Bindings: HttpBindings }>()
9
+ const app = new Hono<{ Bindings: HttpBindings }>()
10
+
11
+ app.post('/backup', async (c) => {
12
+ const date = new Date()
13
+ const name = `./backups/backup-${date.toISOString()}.db`
11
14
 
12
- app.post('/backup', async (c) => {
13
- // const totalPagesTransferred = await backup(db.database, './backups/backup.db', {
14
- // rate: 1, // Copy one page at a time.
15
- // progress: ({ totalPages, remainingPages }) => {
16
- // console.log('Backup in progress', { totalPages, remainingPages })
17
- // },
18
- // })
15
+ try {
16
+ fsp.mkdir('./backups', { recursive: true })
19
17
 
20
- // console.log('Backup completed', totalPagesTransferred)
18
+ await backup(db.database, name)
21
19
 
22
- return c.html('good')
20
+ console.log('Backup')
23
21
 
24
- // return streamSSE(c, async (stream) => {
25
- // await stream.writeSSE({
26
- // event: 'datastar-patch-signals',
27
- // data: `signals {}`,
28
- // })
22
+ return streamSSE(c, async (stream) => {
23
+ await stream.writeSSE({
24
+ event: 'datastar-patch-signals',
25
+ data: `signals { status: 200, message: '${name} created' }`,
26
+ })
27
+ })
28
+ } catch (error) {
29
+ console.log(error)
29
30
 
30
- // await stream.writeSSE({
31
- // event: 'datastar-patch-elements',
32
- // data: `elements ${stripNewlines(Settings())}`,
33
- // })
34
- // })
35
- })
31
+ return streamSSE(c, async (stream) => {
32
+ await stream.writeSSE({
33
+ event: 'datastar-patch-signals',
34
+ data: `signals { status: 500, message: '${name} failed' }`,
35
+ })
36
+ })
37
+ }
38
+ })
36
39
 
37
- return app
38
- }
40
+ export const backupRoutes = app
package/api/block.ts CHANGED
@@ -13,92 +13,64 @@ import {
13
13
  } from '../queries/block-with-children.ts'
14
14
  import AdminPanel from '../components/AdminPanel.ts'
15
15
  import { query } from '../queries/index.ts'
16
+ import { studioStructure } from '../index.ts'
16
17
 
17
- export default (structure: Structure) => {
18
- const app = new Hono<{ Bindings: HttpBindings }>()
18
+ const app = new Hono<{ Bindings: HttpBindings }>()
19
19
 
20
- app.post('/block', async (c) => {
21
- return streamSSE(c, async (stream) => {
22
- const formData = await c.req.formData()
23
- const data = Object.fromEntries(formData.entries())
20
+ app.post('/block', async (c) => {
21
+ return streamSSE(c, async (stream) => {
22
+ const formData = await c.req.formData()
23
+ const data = Object.fromEntries(formData.entries())
24
24
 
25
- const row = structure[data.name?.toString()]
25
+ const row = studioStructure[data.name?.toString()]
26
26
 
27
- if (!row) return
27
+ if (!row) return
28
28
 
29
- db.insertInto('blocks', {
30
- name: data.name?.toString(),
31
- label: row.label,
32
- type: row.type,
33
- })
29
+ db.insertInto('blocks', {
30
+ name: data.name?.toString(),
31
+ label: row.label,
32
+ type: row.type,
33
+ })
34
34
 
35
- await stream.writeSSE({
36
- event: 'datastar-patch-elements',
37
- data: `elements ${stripNewlines(AdminPanel())}`,
38
- })
35
+ await stream.writeSSE({
36
+ event: 'datastar-patch-elements',
37
+ data: `elements ${stripNewlines(AdminPanel())}`,
39
38
  })
40
39
  })
40
+ })
41
+
42
+ app.post('/new-block', async (c) => {
43
+ return streamSSE(c, async (stream) => {
44
+ const formData = await c.req.formData()
45
+ const data = Object.fromEntries(formData)
46
+
47
+ db.insertInto('blocks', {
48
+ type: data.type.toString(),
49
+ name: data.name.toString(),
50
+ label: data.label.toString(),
51
+ parent_id: data.parent_id.toString(),
52
+ sort_order: data.sort_order.toString(),
53
+ })
41
54
 
42
- app.post('/new-block', async (c) => {
43
- return streamSSE(c, async (stream) => {
44
- const formData = await c.req.formData()
45
- const data = Object.fromEntries(formData)
46
-
47
- db.insertInto('blocks', {
48
- type: data.type.toString(),
49
- name: data.name.toString(),
50
- label: data.label.toString(),
51
- parent_id: data.parent_id.toString(),
52
- sort_order: data.sort_order.toString(),
53
- })
54
-
55
- await stream.writeSSE({
56
- event: 'datastar-patch-elements',
57
- data: `elements ${stripNewlines(Entry({ entryId: parseInt(data.entry_id.toString()) }))}`,
58
- })
55
+ await stream.writeSSE({
56
+ event: 'datastar-patch-elements',
57
+ data: `elements ${stripNewlines(Entry({ entryId: parseInt(data.entry_id.toString()) }))}`,
59
58
  })
60
59
  })
60
+ })
61
61
 
62
- app.patch('/block', async (c) => {
63
- return streamSSE(c, async (stream) => {
64
- const formData = await c.req.formData()
65
-
66
- const id = formData.get('id')?.toString()
67
- const value = formData.get('value')?.toString()
68
- const name = formData.get('name')?.toString()
69
- const entryId = formData.get('entryId')?.toString()
70
- const parentId = formData.get('parentId')?.toString()
71
- // const sortOrder = formData.get('sort_order')?.toString()
62
+ app.patch('/block', async (c) => {
63
+ return streamSSE(c, async (stream) => {
64
+ const formData = await c.req.formData()
72
65
 
73
- if (!id || !value) return
66
+ const id = formData.get('id')?.toString()
67
+ const value = formData.get('value')?.toString()
68
+ const name = formData.get('name')?.toString()
69
+ const entryId = formData.get('entryId')?.toString()
70
+ const parentId = formData.get('parentId')?.toString()
71
+ // const sortOrder = formData.get('sort_order')?.toString()
74
72
 
75
- const transaction = db.database.prepare(sql`
76
- update blocks
77
- set
78
- value = ?
79
- where
80
- id = ?;
81
- `)
82
-
83
- transaction.run(value, id)
84
-
85
- if (entryId === parentId && name?.toString() === 'title') {
86
- const rootBlock = query.block({
87
- id: parentId?.toString() || null,
88
- })
89
-
90
- if (rootBlock.type !== 'single') {
91
- await stream.writeSSE({
92
- event: 'datastar-patch-elements',
93
- data: `elements <a href="/admin/entry/${entryId}" id="block_link_${entryId}">${value}</a>`,
94
- })
95
- }
96
- }
97
- })
98
- })
99
-
100
- app.patch('/value', async (c) => {
101
- const body = await c.req.json()
73
+ if (!id || !value) return
102
74
 
103
75
  const transaction = db.database.prepare(sql`
104
76
  update blocks
@@ -108,56 +80,83 @@ export default (structure: Structure) => {
108
80
  id = ?;
109
81
  `)
110
82
 
111
- transaction.run(body.value, body.id)
83
+ transaction.run(value, id)
84
+
85
+ if (entryId === parentId && name?.toString() === 'title') {
86
+ const rootBlock = query.block({
87
+ id: parentId?.toString() || null,
88
+ })
112
89
 
113
- return c.json({ status: 200, message: 'success' })
90
+ if (rootBlock.type !== 'single') {
91
+ await stream.writeSSE({
92
+ event: 'datastar-patch-elements',
93
+ data: `elements <a href="/studio/entry/${entryId}" id="block_link_${entryId}">${value}</a>`,
94
+ })
95
+ }
96
+ }
114
97
  })
98
+ })
115
99
 
116
- app.delete('/block', async (c) => {
117
- return streamSSE(c, async (stream) => {
118
- const formData = await c.req.formData()
100
+ app.patch('/value', async (c) => {
101
+ const body = await c.req.json()
119
102
 
120
- const id = formData.get('id')?.toString()
121
- const entryId = formData.get('entry_id')?.toString()
103
+ const transaction = db.database.prepare(sql`
104
+ update blocks
105
+ set
106
+ value = ?
107
+ where
108
+ id = ?;
109
+ `)
122
110
 
123
- if (!id) return
111
+ transaction.run(body.value, body.id)
124
112
 
125
- const transaction = db.database.prepare(deleteBlockWithChildren)
113
+ return c.json({ status: 200, message: 'success' })
114
+ })
126
115
 
127
- transaction.all(id)
116
+ app.delete('/block', async (c) => {
117
+ return streamSSE(c, async (stream) => {
118
+ const formData = await c.req.formData()
128
119
 
129
- if (entryId) {
130
- await stream.writeSSE({
131
- event: 'datastar-patch-elements',
132
- data: `elements ${stripNewlines(Entry({ entryId: parseInt(entryId.toString()) }))}`,
133
- })
134
- } else {
135
- await stream.writeSSE({
136
- event: 'datastar-patch-elements',
137
- data: `elements ${stripNewlines(AdminPanel())}`,
138
- })
139
- }
140
- })
120
+ const id = formData.get('id')?.toString()
121
+ const entryId = formData.get('entry_id')?.toString()
122
+
123
+ if (!id) return
124
+
125
+ const transaction = db.database.prepare(deleteBlockWithChildren)
126
+
127
+ transaction.all(id)
128
+
129
+ if (entryId) {
130
+ await stream.writeSSE({
131
+ event: 'datastar-patch-elements',
132
+ data: `elements ${stripNewlines(Entry({ entryId: parseInt(entryId.toString()) }))}`,
133
+ })
134
+ } else {
135
+ await stream.writeSSE({
136
+ event: 'datastar-patch-elements',
137
+ data: `elements ${stripNewlines(AdminPanel())}`,
138
+ })
139
+ }
141
140
  })
141
+ })
142
142
 
143
- app.post('/sort-order', async (c) => {
144
- const id = c.req.query('id')
145
- const sortOrder = c.req.query('sort-order')
143
+ app.post('/sort-order', async (c) => {
144
+ const id = c.req.query('id')
145
+ const sortOrder = c.req.query('sort-order')
146
146
 
147
- if (!id || !sortOrder) return
147
+ if (!id || !sortOrder) return
148
148
 
149
- const transaction = db.database.prepare(sql`
150
- update blocks
151
- set
152
- sort_order = ?
153
- where
154
- id = ?
155
- `)
149
+ const transaction = db.database.prepare(sql`
150
+ update blocks
151
+ set
152
+ sort_order = ?
153
+ where
154
+ id = ?
155
+ `)
156
156
 
157
- transaction.run(sortOrder, id)
157
+ transaction.run(sortOrder, id)
158
158
 
159
- return c.json({ status: 200, message: 'success' })
160
- })
159
+ return c.json({ status: 200, message: 'success' })
160
+ })
161
161
 
162
- return app
163
- }
162
+ export const blockRoutes = app
package/api/index.ts CHANGED
@@ -1,13 +1,20 @@
1
- import block from './block.ts'
2
- import apiKey from './api-key.ts'
3
- import type { Structure } from '../types.ts'
4
- import backup from './backup.ts'
1
+ import { Hono } from 'hono'
5
2
 
6
- export const api = (structure: Structure) => {
7
- const app = block(structure)
3
+ import { blockRoutes } from './block.ts'
4
+ import { apiKeyRoutes } from './api-key.ts'
5
+ import { backupRoutes } from './backup.ts'
6
+ import { authRoutes } from './auth.ts'
7
+ import { fieldRoutes } from '../components/fields/index.ts'
8
8
 
9
- app.route('/', apiKey())
10
- app.route('/', backup())
9
+ const routes = new Hono()
11
10
 
12
- return app
13
- }
11
+ routes.route('/', blockRoutes)
12
+ routes.route('/', apiKeyRoutes)
13
+ routes.route('/', backupRoutes)
14
+ routes.route('/auth', authRoutes)
15
+
16
+ fieldRoutes.forEach((fieldRoute) => {
17
+ routes.route('/field', fieldRoute)
18
+ })
19
+
20
+ export const apiRoutes = routes
package/api/mcp.ts CHANGED
@@ -1,53 +1,50 @@
1
1
  import { type HttpBindings } from '@hono/node-server'
2
- import { ServerSentEventGenerator } from '@starfederation/datastar-sdk'
3
2
  import { Hono } from 'hono'
4
3
  import { sql } from '../utils/sql.ts'
5
4
  import { db } from '@alstar/db'
6
5
  import { bearerAuth } from 'hono/bearer-auth'
7
6
  import crypto from 'node:crypto'
8
7
 
9
- export default () => {
10
- const app = new Hono<{ Bindings: HttpBindings }>()
11
-
12
- app.use(
13
- '/*',
14
- bearerAuth({
15
- verifyToken: async (token, c) => {
16
- const hash = crypto.createHash('sha256')
17
-
18
- hash.update(token)
19
-
20
- const digest = hash.digest().toString('base64')
21
-
22
- const exists = db.database
23
- .prepare(sql`
24
- select
25
- value
26
- from
27
- api_keys
28
- where
29
- value = ?
30
- `)
31
- .get(digest)
32
-
33
- return !!exists
34
- },
35
- }),
36
- )
37
-
38
- app.get('/entry', async (c) => {
39
- return c.json({
40
- status: 'success',
41
- message: 'Response from MCP server!',
42
- })
8
+ const app = new Hono<{ Bindings: HttpBindings }>()
9
+
10
+ app.use(
11
+ '/*',
12
+ bearerAuth({
13
+ verifyToken: async (token, c) => {
14
+ const hash = crypto.createHash('sha256')
15
+
16
+ hash.update(token)
17
+
18
+ const digest = hash.digest().toString('base64')
19
+
20
+ const exists = db.database
21
+ .prepare(sql`
22
+ select
23
+ value
24
+ from
25
+ api_keys
26
+ where
27
+ value = ?
28
+ `)
29
+ .get(digest)
30
+
31
+ return !!exists
32
+ },
33
+ }),
34
+ )
35
+
36
+ app.get('/entry', async (c) => {
37
+ return c.json({
38
+ status: 'success',
39
+ message: 'Response from MCP server!',
43
40
  })
41
+ })
44
42
 
45
- app.post('/entry', async (c) => {
46
- return c.json({
47
- status: 'success',
48
- message: 'New entry created!',
49
- })
43
+ app.post('/entry', async (c) => {
44
+ return c.json({
45
+ status: 'success',
46
+ message: 'New entry created!',
50
47
  })
48
+ })
51
49
 
52
- return app
53
- }
50
+ export const mcpRoutes = app