@axium/storage 0.1.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/dist/server.js ADDED
@@ -0,0 +1,311 @@
1
+ import { getSessionAndUser } from '@axium/server/auth';
2
+ import { addConfigDefaults, config } from '@axium/server/config';
3
+ import { connect, database } from '@axium/server/database';
4
+ import { dirs } from '@axium/server/io';
5
+ import { checkAuth, getToken, parseBody, withError } from '@axium/server/requests';
6
+ import { addRoute } from '@axium/server/routes';
7
+ import { error } from '@sveltejs/kit';
8
+ import { createHash } from 'node:crypto';
9
+ import { linkSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
10
+ import { writeFile } from 'node:fs/promises';
11
+ import { join } from 'node:path/posix';
12
+ import * as z from 'zod';
13
+ import { StorageItemUpdate } from './common.js';
14
+ import './polyfills.js';
15
+ const defaultCASMime = [/video\/.*/, /audio\/.*/];
16
+ addConfigDefaults({
17
+ storage: {
18
+ enabled: true,
19
+ app_enabled: true,
20
+ data: dirs.at(-1) + '/storage',
21
+ trash_duration: 30,
22
+ limits: {
23
+ user_size: 1000,
24
+ item_size: 100,
25
+ user_items: 10_000,
26
+ },
27
+ cas: {
28
+ enabled: true,
29
+ include: [],
30
+ exclude: [],
31
+ },
32
+ },
33
+ });
34
+ function parseItem(item) {
35
+ return {
36
+ ...item,
37
+ hash: item.hash.toHex(),
38
+ dataURL: `/raw/storage/${item.id}`,
39
+ };
40
+ }
41
+ /**
42
+ * Returns the current usage of the storage for a user in bytes.
43
+ */
44
+ export async function currentUsage(userId) {
45
+ connect();
46
+ const result = await database
47
+ .selectFrom('storage')
48
+ .where('userId', '=', userId)
49
+ .select(database.fn.countAll().as('items'))
50
+ .select(eb => eb.fn.sum('size').as('bytes'))
51
+ .executeTakeFirstOrThrow();
52
+ return result;
53
+ }
54
+ export async function get(itemId) {
55
+ connect();
56
+ const result = await database.selectFrom('storage').where('id', '=', itemId).selectAll().executeTakeFirstOrThrow();
57
+ return parseItem(result);
58
+ }
59
+ let _getLimits = null;
60
+ /**
61
+ * Define the handler to get limits for a user externally.
62
+ */
63
+ export function useLimits(handler) {
64
+ _getLimits = handler;
65
+ }
66
+ export async function getLimits(userId) {
67
+ try {
68
+ return await _getLimits(userId);
69
+ }
70
+ catch {
71
+ return config.storage.limits;
72
+ }
73
+ }
74
+ addRoute({
75
+ path: '/api/storage/item/:id',
76
+ params: { id: z.uuid() },
77
+ async GET(event) {
78
+ if (!config.storage.enabled)
79
+ error(503, 'User storage is disabled');
80
+ const itemId = event.params.id;
81
+ const item = await get(itemId);
82
+ if (!item)
83
+ error(404, 'Item not found');
84
+ await checkAuth(event, item.userId);
85
+ return item;
86
+ },
87
+ async PATCH(event) {
88
+ if (!config.storage.enabled)
89
+ error(503, 'User storage is disabled');
90
+ const itemId = event.params.id;
91
+ const body = await parseBody(event, StorageItemUpdate);
92
+ const item = await get(itemId);
93
+ if (!item)
94
+ error(404, 'Item not found');
95
+ await checkAuth(event, item.userId);
96
+ const values = {};
97
+ if ('restrict' in body)
98
+ values.restricted = body.restrict;
99
+ if ('trash' in body)
100
+ values.trashedAt = body.trash ? new Date() : null;
101
+ if ('owner' in body)
102
+ values.userId = body.owner;
103
+ if ('name' in body)
104
+ values.name = body.name;
105
+ if (!Object.keys(values).length)
106
+ error(400, 'No valid fields to update');
107
+ return parseItem(await database
108
+ .updateTable('storage')
109
+ .where('id', '=', itemId)
110
+ .set(values)
111
+ .returningAll()
112
+ .executeTakeFirstOrThrow()
113
+ .catch(withError('Could not update item')));
114
+ },
115
+ async DELETE(event) {
116
+ if (!config.storage.enabled)
117
+ error(503, 'User storage is disabled');
118
+ const itemId = event.params.id;
119
+ const item = await get(itemId);
120
+ if (!item)
121
+ error(404, 'Item not found');
122
+ await checkAuth(event, item.userId);
123
+ await database
124
+ .deleteFrom('storage')
125
+ .where('id', '=', itemId)
126
+ .returningAll()
127
+ .executeTakeFirstOrThrow()
128
+ .catch(withError('Could not delete item'));
129
+ const { count } = await database
130
+ .selectFrom('storage')
131
+ .where('hash', '=', Uint8Array.fromHex(item.hash))
132
+ .select(eb => eb.fn.countAll().as('count'))
133
+ .executeTakeFirstOrThrow();
134
+ if (!Number(count))
135
+ unlinkSync(join(config.storage.data, item.hash));
136
+ return item;
137
+ },
138
+ });
139
+ addRoute({
140
+ path: '/api/storage/directory/:id',
141
+ params: { id: z.uuid() },
142
+ async GET(event) {
143
+ if (!config.storage.enabled)
144
+ error(503, 'User storage is disabled');
145
+ const itemId = event.params.id;
146
+ const item = await get(itemId);
147
+ if (!item)
148
+ error(404, 'Item not found');
149
+ await checkAuth(event, item.userId);
150
+ if (item.type != 'inode/directory')
151
+ error(409, 'Item is not a directory');
152
+ const items = await database
153
+ .selectFrom('storage')
154
+ .where('parentId', '=', itemId)
155
+ .where('trashedAt', '!=', null)
156
+ .selectAll()
157
+ .execute();
158
+ return items.map(parseItem);
159
+ },
160
+ });
161
+ addRoute({
162
+ path: '/raw/storage',
163
+ async PUT(event) {
164
+ if (!config.storage.enabled)
165
+ error(503, 'User storage is disabled');
166
+ const token = getToken(event);
167
+ if (!token)
168
+ error(401, 'Missing session token');
169
+ const { userId } = await getSessionAndUser(token).catch(withError('Invalid session token', 401));
170
+ const [usage, limits] = await Promise.all([currentUsage(userId), getLimits(userId)]).catch(withError('Could not fetch usage and/or limits'));
171
+ const name = event.request.headers.get('x-name');
172
+ if ((name?.length || 0) > 255)
173
+ error(400, 'Name is too long');
174
+ const maybeParentId = event.request.headers.get('x-parent');
175
+ const parentId = maybeParentId
176
+ ? await z
177
+ .uuid()
178
+ .parseAsync(maybeParentId)
179
+ .catch(() => error(400, 'Invalid parent ID'))
180
+ : null;
181
+ const size = Number(event.request.headers.get('content-length'));
182
+ if (Number.isNaN(size))
183
+ error(411, 'Missing or invalid content length header');
184
+ if (usage.items >= limits.user_items)
185
+ error(409, 'Too many items');
186
+ if ((usage.bytes + size) / 1_000_000 >= limits.user_size)
187
+ error(413, 'Not enough space');
188
+ if (size > limits.item_size * 1_000_000)
189
+ error(413, 'File size exceeds maximum size');
190
+ const content = await event.request.bytes();
191
+ // @todo: add this to the audit log
192
+ if (content.byteLength > size)
193
+ error(400, 'Content length does not match size header');
194
+ const type = event.request.headers.get('content-type') || 'application/octet-stream';
195
+ const useCAS = config.storage.cas.enabled &&
196
+ (defaultCASMime.some(pattern => pattern.test(type)) || config.storage.cas.include.some(mime => type.match(mime)));
197
+ const hash = createHash('BLAKE2b512').update(content).digest();
198
+ // @todo: make this atomic
199
+ const result = await database
200
+ .insertInto('storage')
201
+ .values({ userId: userId, hash, name, size, type, immutable: useCAS, parentId })
202
+ .returningAll()
203
+ .executeTakeFirstOrThrow()
204
+ .catch(withError('Could not create item'));
205
+ const path = join(config.storage.data, result.id);
206
+ const _noDupe = () => {
207
+ writeFileSync(path, content);
208
+ return parseItem(result);
209
+ };
210
+ if (!useCAS)
211
+ return _noDupe();
212
+ const existing = await database
213
+ .selectFrom('storage')
214
+ .where('hash', '=', hash)
215
+ .where('id', '!=', result.id)
216
+ .selectAll()
217
+ .executeTakeFirst();
218
+ if (!existing)
219
+ return _noDupe();
220
+ linkSync(join(config.storage.data, existing.id), path);
221
+ return parseItem(result);
222
+ },
223
+ });
224
+ addRoute({
225
+ path: '/raw/storage/:id',
226
+ params: { id: z.uuid() },
227
+ async GET(event) {
228
+ if (!config.storage.enabled)
229
+ error(503, 'User storage is disabled');
230
+ const itemId = event.params.id;
231
+ const item = await get(itemId);
232
+ if (!item)
233
+ error(404, 'Item not found');
234
+ await checkAuth(event, item.userId);
235
+ if (item.trashedAt)
236
+ error(410, 'Trashed items can not be downloaded');
237
+ const content = new Uint8Array(readFileSync(join(config.storage.data, item.id)));
238
+ return new Response(content, {
239
+ headers: {
240
+ 'Content-Type': item.type,
241
+ 'Content-Disposition': `attachment; filename="${item.name}"`,
242
+ },
243
+ });
244
+ },
245
+ async POST(event) {
246
+ if (!config.storage.enabled)
247
+ error(503, 'User storage is disabled');
248
+ const itemId = event.params.id;
249
+ const item = await get(itemId);
250
+ if (!item)
251
+ error(404, 'Item not found');
252
+ const { accessor } = await checkAuth(event, item.userId);
253
+ if (item.immutable)
254
+ error(403, 'Item is immutable');
255
+ if (item.trashedAt)
256
+ error(410, 'Trashed items can not be changed');
257
+ if (item.restricted && item.userId != accessor.id)
258
+ error(403, 'Item editing is restricted to the owner');
259
+ const type = event.request.headers.get('content-type') || 'application/octet-stream';
260
+ // @todo: add this to the audit log
261
+ if (type != item.type)
262
+ error(400, 'Content type does not match existing item type');
263
+ const size = Number(event.request.headers.get('content-length'));
264
+ if (Number.isNaN(size))
265
+ error(411, 'Missing or invalid content length header');
266
+ const [usage, limits] = await Promise.all([currentUsage(item.userId), getLimits(item.userId)]).catch(withError('Could not fetch usage and/or limits'));
267
+ if ((usage.bytes + size - item.size) / 1_000_000 >= limits.user_size)
268
+ error(413, 'Not enough space');
269
+ if (size > limits.item_size * 1_000_000)
270
+ error(413, 'File size exceeds maximum size');
271
+ const content = await event.request.bytes();
272
+ // @todo: add this to the audit log
273
+ if (content.byteLength > size)
274
+ error(400, 'Content length does not match size header');
275
+ const hash = createHash('BLAKE2b512').update(content).digest();
276
+ // @todo: make this atomic
277
+ const result = await database
278
+ .updateTable('storage')
279
+ .where('id', '=', itemId)
280
+ .set({ size, modifiedAt: new Date(), hash })
281
+ .returningAll()
282
+ .executeTakeFirstOrThrow()
283
+ .catch(withError('Could not update item'));
284
+ await writeFile(join(config.storage.data, result.id), content).catch(withError('Could not write'));
285
+ return parseItem(result);
286
+ },
287
+ });
288
+ addRoute({
289
+ path: '/api/users/:id/storage',
290
+ params: { id: z.uuid() },
291
+ async OPTIONS(event) {
292
+ if (!config.storage.enabled)
293
+ error(503, 'User storage is disabled');
294
+ const userId = event.params.id;
295
+ await checkAuth(event, userId);
296
+ const [usage, limits] = await Promise.all([currentUsage(userId), getLimits(userId)]).catch(withError('Could not fetch data'));
297
+ return { usage, limits };
298
+ },
299
+ async GET(event) {
300
+ if (!config.storage.enabled)
301
+ error(503, 'User storage is disabled');
302
+ const userId = event.params.id;
303
+ await checkAuth(event, userId);
304
+ const [items, usage, limits] = await Promise.all([
305
+ database.selectFrom('storage').where('userId', '=', userId).selectAll().execute(),
306
+ currentUsage(userId),
307
+ getLimits(userId),
308
+ ]).catch(withError('Could not fetch data'));
309
+ return { usage, limits, items: items.map(parseItem) };
310
+ },
311
+ });
@@ -0,0 +1,121 @@
1
+ <script lang="ts">
2
+ import { formatBytes } from '@axium/core/format';
3
+ import { forMime as iconForMime } from '@axium/core/icons';
4
+ import { FormDialog, Icon } from '@axium/server/lib';
5
+ import { deleteItem, getDirectoryMetadata, updateItem } from '@axium/storage/client';
6
+ import type { StorageItemMetadata } from '@axium/storage/common';
7
+
8
+ const { id }: { id: string } = $props();
9
+
10
+ let items = $state<StorageItemMetadata[]>([]);
11
+ let activeIndex = $state<number>(-1);
12
+ let activeItem = $derived(items[activeIndex]);
13
+ const dialogs = $state<Record<string, HTMLDialogElement>>({});
14
+ </script>
15
+
16
+ {#snippet action(name: string, icon: string, i: number)}
17
+ <Icon
18
+ i={icon}
19
+ --size="14px"
20
+ onclick={(e: Event) => {
21
+ e.stopPropagation();
22
+ e.preventDefault();
23
+ activeIndex = i;
24
+ dialogs[name].showModal();
25
+ }}
26
+ class="action"
27
+ />
28
+ {/snippet}
29
+
30
+ {#snippet _itemName()}
31
+ {#if activeItem.name}
32
+ <strong>{activeItem.name.length > 23 ? activeItem.name.slice(0, 20) + '...' : activeItem.name}</strong>
33
+ {:else}
34
+ this
35
+ {/if}
36
+ {/snippet}
37
+
38
+ <div class="FilesList">
39
+ {#await getDirectoryMetadata(id).then(data => (items = data)) then}
40
+ {#each items as item, i (item.id)}
41
+ <div class="FilesListItem">
42
+ <Icon i={iconForMime(item.type)} />
43
+ <span class="name">{item.name}</span>
44
+ <span>{item.modifiedAt.toLocaleString()}</span>
45
+ <span>{formatBytes(item.size)}</span>
46
+ {@render action('rename', 'edit', i)}
47
+ {@render action('download', 'download', i)}
48
+ {@render action('delete', 'delete', i)}
49
+ </div>
50
+ {:else}
51
+ <i>No items.</i>
52
+ {/each}
53
+ {:catch error}
54
+ <i style:color="#c44">{error.message}</i>
55
+ {/await}
56
+ </div>
57
+
58
+ <FormDialog
59
+ bind:dialog={dialogs.rename}
60
+ submitText="Rename"
61
+ submit={async (data: { name: string }) => {
62
+ await updateItem(activeItem.id, data);
63
+ activeItem.name = data.name;
64
+ }}
65
+ >
66
+ <div>
67
+ <label for="name">Name</label>
68
+ <input name="name" type="text" required value={activeItem.name} />
69
+ </div>
70
+ </FormDialog>
71
+ <FormDialog
72
+ bind:dialog={dialogs.delete}
73
+ submitText="Delete"
74
+ submitDanger
75
+ submit={async () => {
76
+ await deleteItem(activeItem.id);
77
+ if (activeIndex != -1) items.splice(activeIndex, 1);
78
+ }}
79
+ >
80
+ <p>Are you sure you want to delete {@render _itemName()}?</p>
81
+ </FormDialog>
82
+ <FormDialog
83
+ bind:dialog={dialogs.download}
84
+ submitText="Download"
85
+ submit={async () => {
86
+ open(activeItem.dataURL, '_blank');
87
+ }}
88
+ >
89
+ <p>
90
+ We are not responsible for the contents of this file. <br />
91
+ Are you sure you want to download {@render _itemName()}?
92
+ </p>
93
+ </FormDialog>
94
+
95
+ <style>
96
+ .FilesList {
97
+ display: flex;
98
+ flex-direction: column;
99
+ gap: 0.5em;
100
+ padding: 0.5em;
101
+ }
102
+
103
+ .FilesListItem {
104
+ display: grid;
105
+ grid-template-columns: 1em 4fr 15em 5em repeat(1em, 3);
106
+ align-items: center;
107
+ gap: 0.5em;
108
+ }
109
+
110
+ .action {
111
+ visibility: hidden;
112
+ }
113
+
114
+ .FilesListItem:hover .action {
115
+ visibility: visible;
116
+ }
117
+
118
+ .action:hover {
119
+ cursor: pointer;
120
+ }
121
+ </style>
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ import { getDirectoryMetadata, type _Sidebar } from '@axium/storage/client';
3
+ import type { StorageItemMetadata } from '@axium/storage/common';
4
+ import { setContext } from 'svelte';
5
+ import { ItemSelection } from '../src/selection.js';
6
+ import StorageSidebarItem from './StorageSidebarItem.svelte';
7
+
8
+ const { root }: { root: string } = $props();
9
+
10
+ let items = $state<StorageItemMetadata[]>([]);
11
+
12
+ const allItems: StorageItemMetadata[] = [];
13
+
14
+ const sidebar = $state<_Sidebar>({
15
+ selection: new ItemSelection(allItems),
16
+ items: allItems,
17
+ async getDirectory(id: string, assignTo?: StorageItemMetadata[]) {
18
+ const data = await getDirectoryMetadata(id);
19
+ this.items.push(...data);
20
+ assignTo = data;
21
+ return data;
22
+ },
23
+ });
24
+
25
+ setContext('files:sidebar', () => sidebar);
26
+ </script>
27
+
28
+ <div id="FilesSidebar">
29
+ {#await sidebar.getDirectory(root, items)}
30
+ <i>Loading...</i>
31
+ {:then}
32
+ {#each items as _, i (_.id)}
33
+ <StorageSidebarItem bind:item={items[i]} bind:items />
34
+ {/each}
35
+ {:catch error}
36
+ <i style:color="#c44">{error.message}</i>
37
+ {/await}
38
+ </div>
39
+
40
+ <style>
41
+ </style>
@@ -0,0 +1,170 @@
1
+ <script lang="ts">
2
+ import * as icon from '@axium/core/icons';
3
+ import { ClipboardCopy, FormDialog, Icon } from '@axium/server/lib';
4
+ import { deleteItem, updateItem, type _Sidebar } from '@axium/storage/client';
5
+ import type { StorageItemMetadata } from '@axium/storage/common';
6
+ import { getContext } from 'svelte';
7
+ import StorageSidebarItem from './StorageSidebarItem.svelte';
8
+
9
+ let {
10
+ item = $bindable(),
11
+ items = $bindable(),
12
+ debug = false,
13
+ }: {
14
+ item: StorageItemMetadata;
15
+ /** The items list for the parent directory */
16
+ items: StorageItemMetadata[];
17
+ debug?: boolean;
18
+ } = $props();
19
+
20
+ const sb = getContext<() => _Sidebar>('files:sidebar')();
21
+
22
+ const dialogs = $state<Record<string, HTMLDialogElement>>({});
23
+ let popover = $state<HTMLDivElement>();
24
+
25
+ function oncontextmenu(e: MouseEvent) {
26
+ e.preventDefault();
27
+ e.stopPropagation();
28
+ popover?.togglePopover();
29
+ }
30
+
31
+ function onclick(e: MouseEvent) {
32
+ if (e.shiftKey) sb.selection.toggleRange(item.id);
33
+ else if (e.ctrlKey) sb.selection.toggle(item.id);
34
+ else {
35
+ sb.selection.clear();
36
+ sb.selection.add(item.id);
37
+ }
38
+ }
39
+
40
+ let children = $state<StorageItemMetadata[]>([]);
41
+ </script>
42
+
43
+ {#snippet action(name: string, i: string, text: string)}
44
+ <div
45
+ onclick={e => {
46
+ e.stopPropagation();
47
+ e.preventDefault();
48
+ dialogs[name].showModal();
49
+ }}
50
+ >
51
+ <Icon {i} --size="14px" />
52
+ {text}
53
+ </div>
54
+ {/snippet}
55
+
56
+ {#snippet _itemName()}
57
+ {#if item.name}
58
+ <strong>{item.name.length > 23 ? item.name.slice(0, 20) + '...' : item.name}</strong>
59
+ {:else}
60
+ this
61
+ {/if}
62
+ {/snippet}
63
+
64
+ {#if item.type == 'inode/directory'}
65
+ <details>
66
+ <summary class={['StorageSidebarItem', sb.selection.has(item.id) && 'selected']} {onclick} {oncontextmenu}>
67
+ <Icon i={icon.forMime(item.type)} />
68
+ <span class="name">{item.name}</span>
69
+ </summary>
70
+ <div>
71
+ {#await sb.getDirectory(item.id, children)}
72
+ <i>Loading...</i>
73
+ {:then}
74
+ {#each children as _, i (_.id)}
75
+ <StorageSidebarItem bind:item={children[i]} bind:items={children} />
76
+ {/each}
77
+ {:catch error}
78
+ <i style:color="#c44">{error.message}</i>
79
+ {/await}
80
+ </div>
81
+ </details>
82
+ {:else}
83
+ <div class={['StorageSidebarItem', sb.selection.has(item.id) && 'selected']} {onclick} {oncontextmenu}>
84
+ <Icon i={icon.forMime(item.type)} />
85
+ <span class="name">{item.name}</span>
86
+ </div>
87
+ {/if}
88
+
89
+ <div popover bind:this={popover}>
90
+ {@render action('rename', 'pen', 'Rename')}
91
+ {@render action('delete', 'trash', 'Delete')}
92
+ {#if item.type == 'cas_item'}
93
+ {@render action('download', 'download', 'Download')}
94
+ {/if}
95
+ {#if debug}
96
+ <ClipboardCopy value={item.id} />
97
+ {/if}
98
+ </div>
99
+
100
+ <FormDialog
101
+ bind:dialog={dialogs.rename}
102
+ submitText="Rename"
103
+ submit={async (data: { name: string }) => {
104
+ await updateItem(item.id, data);
105
+ item.name = data.name;
106
+ }}
107
+ >
108
+ <div>
109
+ <label for="name">Name</label>
110
+ <input name="name" type="text" required value={item.name} />
111
+ </div>
112
+ </FormDialog>
113
+ <FormDialog
114
+ bind:dialog={dialogs.delete}
115
+ submitText="Delete"
116
+ submitDanger
117
+ submit={async () => {
118
+ await deleteItem(item.id);
119
+ const index = items.findIndex(r => r.id === item.id);
120
+ if (index !== -1) items.splice(index, 1);
121
+ }}
122
+ >
123
+ <p>Are you sure you want to delete {@render _itemName()}?</p>
124
+ </FormDialog>
125
+ <FormDialog
126
+ bind:dialog={dialogs.download}
127
+ submitText="Download"
128
+ submit={async () => {
129
+ open(item.dataURL, '_blank');
130
+ }}
131
+ >
132
+ <p>
133
+ We are not responsible for the contents of this file. <br />
134
+ Are you sure you want to download {@render _itemName()}?
135
+ </p>
136
+ </FormDialog>
137
+
138
+ <style>
139
+ .StorageSidebarItem {
140
+ display: grid;
141
+ grid-template-columns: 1em 1fr;
142
+ align-items: center;
143
+ text-align: left;
144
+ gap: 0.5em;
145
+ border-radius: 0.5em;
146
+ border: 1px solid transparent;
147
+ padding: 0.25em 0.75em 0.25em 0.5em;
148
+ font-size: 14px;
149
+ }
150
+
151
+ .name {
152
+ overflow: hidden;
153
+ text-overflow: ellipsis;
154
+ }
155
+
156
+ .StorageSidebarItem:hover {
157
+ background: #334;
158
+ cursor: pointer;
159
+ }
160
+
161
+ .selected {
162
+ border: 1px solid #555;
163
+ background: #334;
164
+ color: #fff;
165
+ }
166
+
167
+ details > div {
168
+ padding-left: 0.5em;
169
+ }
170
+ </style>
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "..",
5
+ "noEmit": true
6
+ },
7
+ "include": ["**/*.svelte"],
8
+ "exclude": [],
9
+ "references": [{ "path": ".." }]
10
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@axium/storage",
3
+ "version": "0.1.0",
4
+ "author": "James Prevett <axium@jamespre.dev> (https://jamespre.dev)",
5
+ "description": "User file storage for Axium",
6
+ "funding": {
7
+ "type": "individual",
8
+ "url": "https://github.com/sponsors/james-pre"
9
+ },
10
+ "license": "LGPL-3.0-or-later",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/james-pre/axium.git"
14
+ },
15
+ "homepage": "https://github.com/james-pre/axium#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/james-pre/axium/issues"
18
+ },
19
+ "type": "module",
20
+ "main": "dist/index.js",
21
+ "types": "dist/index.d.ts",
22
+ "exports": {
23
+ ".": "./dist/index.js",
24
+ "./*": "./dist/*.js"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "lib"
29
+ ],
30
+ "scripts": {
31
+ "build": "tsc"
32
+ },
33
+ "peerDependencies": {
34
+ "@axium/client": ">=0.1.0",
35
+ "@axium/core": ">=0.4.0",
36
+ "@axium/server": ">=0.16.0",
37
+ "@sveltejs/kit": "^2.23.0",
38
+ "utilium": "^2.3.8"
39
+ },
40
+ "dependencies": {
41
+ "blakejs": "^1.2.1",
42
+ "zod": "^4.0.5"
43
+ }
44
+ }