53 Commits

Author SHA1 Message Date
3904449f45 the user and group system is basically finished, the fs needs progress on creating entries 2025-05-24 21:18:48 -04:00
4f6dc35569 users will have global permissions and fs entries will have their own 2025-05-23 02:12:44 -04:00
2a6a2656e0 split users and groups into their own modules 2025-05-23 01:02:26 -04:00
b6aa27bb08 remove bcrpytjs, overkill for this project right now 2025-05-23 01:01:23 -04:00
69dcd51b3c add bcryptjs and remove vite-plugin-html 2025-05-22 18:21:29 -04:00
8938709a1e working on the permissions system and groups structure 2025-05-21 22:38:10 -04:00
a5ee53a151 users system prototyping 2025-05-12 19:36:32 -04:00
8c2f1de028 huge file system rewrite, i lost a lot of track; permissions and user system 2025-05-07 23:36:34 -04:00
22b9e1f3d5 wip: root can add directories to itself 2025-04-24 01:23:11 -04:00
b79234a1f5 bitflags, fs is very broken 2025-04-16 15:51:15 -04:00
bb5d24884f switch to using bitflags for rfwfs permissions 2025-03-29 17:38:25 -04:00
56790cbe1d Binary file time 2025-03-22 21:29:44 -04:00
0483e2a0df change some to status in wrap 2025-03-20 19:39:10 -04:00
7714a08517 bun update 2025-03-20 19:04:48 -04:00
929b267f23 move library into rfwfs and rename root.ts to fs.ts, also place it outside of rfwfs 2025-03-17 17:46:55 -04:00
332e90d023 "noUnusedParameters": true 2025-03-17 00:10:28 -04:00
b964c911b2 Result -> Option 2025-03-16 23:10:00 -04:00
89b9320cc3 this should be an exported type 2025-03-16 15:01:16 -04:00
e04f2adae0 inner should be more explicit for directories
`inner` -> `directory`
2025-03-16 01:12:16 -04:00
b4c07873d0 library init 2025-03-15 21:31:51 -04:00
143ac35a99 fix binary search 2025-03-15 21:30:04 -04:00
e612f5762f not necessary bro 2025-03-15 20:16:13 -04:00
d4278d0d7f plumbing 2025-03-15 20:14:05 -04:00
26a7d4c21c File and Directory classes 2025-03-15 19:53:40 -04:00
305c2bd2cd vite update 2025-03-15 19:46:10 -04:00
c66a0eb4a9 prototyping with classes for Inner and Rfwfs directory 2025-03-14 17:39:07 -04:00
caedb7e8f0 push and find should use read_write_access since they read the names of inner 2025-03-12 16:36:05 -04:00
050c0f1aca (WIP) name and timestamp resepect permissions 2025-03-11 03:23:13 -04:00
d7abe20d1a PermissionsBinary 2025-03-11 03:13:28 -04:00
1d3b80515b var and etc 2025-03-08 18:31:05 -05:00
3f48a19b22 desktop dir 2025-03-08 18:26:18 -05:00
74e3d5df60 optional timestamp 2025-03-08 18:26:07 -05:00
9d0b12b47c replace tree with fs/root.ts 2025-03-08 17:41:14 -05:00
e8645d4f64 directory.clone(file_name) 2025-03-08 17:41:05 -05:00
fc0ef23bdb types/entry.d.ts not needed 2025-03-08 17:40:47 -05:00
7a457e5205 rewrite of types and names 2025-03-08 16:12:02 -05:00
6b129045e8 entry.d.ts 2025-03-08 16:11:18 -05:00
cec5642e70 branch out enums to an enum module 2025-03-08 16:09:54 -05:00
5b74400a10 wrap module, may branch this out to the entire codebase 2025-03-08 15:59:23 -05:00
384e70e286 sha256 hash module for file hashes 2025-03-08 02:10:31 -05:00
3aaedf1e8b vite update 2025-03-07 16:24:35 -05:00
17e89ef1c8 rename cloned_file_collection to entry_collection 2025-03-06 20:56:06 -05:00
aa6f8b6f8e rename new_entry and new_collection to entry and collection 2025-03-06 20:55:43 -05:00
d80887d281 PushStatus enum
i forgor about the permissions system here... maybe users soon for fun..
2025-03-06 20:47:38 -05:00
9eba512580 auto sort collections 2025-03-06 17:59:10 -05:00
8469c015f8 rename push() -> find() for entry_collection 2025-03-06 17:58:40 -05:00
b3fa561c76 typing is now correct for the file system 2025-03-06 17:29:30 -05:00
b7babb665e T extends Entry<T> -> T type error fix 2025-03-05 19:47:59 -05:00
9856b138df type fix 2025-03-05 02:58:02 -05:00
c2ac2ba28c type errors 2025-03-04 20:29:21 -05:00
c994698e2d update ts 2025-03-02 02:39:49 -05:00
fd1675f57a new structure and library structure 2025-03-02 02:39:46 -05:00
3865a79dbc new logo 2025-03-01 00:29:03 -05:00
52 changed files with 933 additions and 535 deletions

View File

@ -2,7 +2,7 @@ FROM oven/bun AS builder
WORKDIR /rhpidfyre.io WORKDIR /rhpidfyre.io
COPY ../packages/web/src ../packages/web/package.json ../packages/web/vite.config.js ../packages/web/tsconfig.json ./ COPY src package.json vite.config.js tsconfig.json ./
RUN bun run install RUN bun run install
RUN bun run build --emptyOutDir RUN bun run build --emptyOutDir

View File

@ -1,7 +1,16 @@
{ {
"name": "rhpidfyre.io", "name": "rhpidfyre.io",
"private": true, "version": "0.0.1",
"workspace": [ "private": true,
"packages/*" "type": "module",
] "devDependencies": {
"sass": "^1.89.0",
"typescript": "^5.8.3",
"vite": "^6.3.5"
},
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
}
} }

View File

@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2025 rhpidfyre
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,9 +0,0 @@
{
"name": "@rhpidfyre.io/rfwfs",
"private": true,
"version": "0.0.1",
"type": "module",
"devDependencies": {
"typescript": "^5.7.3",
}
}

View File

@ -1,20 +0,0 @@
import rfwfs_search from "./index"
import rfwfs_entry_trait, {EntryType, type Entry, type EntryTree} from "./entry"
function Entry<T = File>(name: string, inner: T, permissions: Permissions): Entry<T> {
return {
type: typeof inner == "object" ? EntryType.Directory : EntryType.File,
inner: inner,
name: name,
permissions: permissions,
timestamp: Math.floor(Date.now()/1000)
}
}
export {
EntryType,
Permissions,
Entry
}

View File

@ -1,89 +0,0 @@
import rfwfs_search from "./index"
const enum EntryType {
Directory,
File
}
const enum Permissions {
r,
w,
rw,
none
}
type Files<T> = Entry<T>[]
type File = string
interface EntryListManipulate {
push: <T extends Entry<T>>(entry: Entry<T>) => boolean,
pop: <T extends Entry<T>>(file_name: string) => Entry<T> | undefined,
sort: () => void,
}
interface EntryList<T> extends EntryListManipulate {
readonly tree: Files<T>
}
interface Entry<T = File> {
readonly inner: EntryList<T>,
readonly name: string,
readonly type: EntryType,
readonly timestamp: number,
readonly permissions: Permissions,
}
function rfwfs_entry_trait<T extends Entry<T>>(): EntryList<T> {
const trait = {} as EntryList<T>
trait.sort = function() {
this.tree.sort((a,z) => a.name.localeCompare(z.name))
}
trait.push = function(entry) {
const no_duplicates = rfwfs_search.binary_fs_name(this.tree, entry.name)
if (!no_duplicates) {
this.tree.push(entry)
this.sort()
return true
}
return false
}
trait.pop = function(file_name) {
const file_search = rfwfs_search.binary_fs_name(this.tree, file_name)
if (file_search) {
this.tree.splice(file_search.binary_index, 1)
return file_search.item
}
return
}
return trait
}
function rfwfs_new_entry_tree<T>(files: Files<T>) {
}
function rfwfs_new_entry<T extends EntryList<T>>(
name: string,
inner: T,
permissions: Permissions,
timestamp: number = Math.floor(Date.now()/1000)
): Entry<T> {
return {
type: typeof inner == "object" ? EntryType.Directory : EntryType.File,
timestamp: timestamp,
name: name,
permissions: permissions,
inner: inner,
}
}
export {
rfwfs_entry_trait,
rfwfs_new_entry,
EntryType,
Permissions,
type EntryList,
type Entry,
type Files,
type File,
}

View File

@ -1,77 +0,0 @@
import { type Entry, type Files } from "./entry"
interface SearchResult<T> {
item: T,
binary_index: number
}
interface Search {
binary_fs_name: <T extends Entry<T>>(cloned_list: Files<T>, file_name: string) => SearchResult<Entry<T>> | undefined,
binary_nsort: <T>(list: T[], find: T, start?: number, end?: number) => SearchResult<T> | undefined,
binary: <T>(list: T[], find: T) => SearchResult<T> | undefined,
linear: <T>(list: T[], find: T) => SearchResult<T> | undefined,
}
function wrap_result<T>(item: T, binary_index: number): SearchResult<T> {
return { item: item, binary_index: binary_index }
}
const rfwfs_search = {} as Search
rfwfs_search.binary = function(list, find) {
list.sort()
let start = 0
let end = list.length-1
while (start<=end) {
const median = (start+end)>>1
if (list[median] === find) {
return wrap_result(list[median], median)
} else if (list[median]<find) {
start = median+1
} else {
end = median-1
}
}
return
}
rfwfs_search.binary_nsort = function(list, find, start = 0, end = list.length-1) {
if (start>end) { return }
const median = (start+end)>>1
if (list[median] === find) {
return wrap_result(list[median], median)
}
if (list[median]>find) {
return this.binary_nsort(list, find, start, median-1)
} else {
return this.binary_nsort(list, find, median+1, end)
}
}
rfwfs_search.binary_fs_name = function(cloned_entry_list, file_name) {
let start = 0
let end = cloned_entry_list.length-1
while (start<=end) {
const median = (start+end)>>1
const median_name = cloned_entry_list[median].name
if (median_name === file_name) {
return wrap_result(cloned_entry_list[median], median)
} else if (median_name<file_name) {
start = median+1
} else {
end = median-1
}
}
return
}
rfwfs_search.linear = function(list, find) {
for (let ind = 0; ind<list.length; ind++) {
if (list[ind] === find) {
return wrap_result(list[ind], ind)
}
}
return
}
export default rfwfs_search

View File

@ -1,85 +0,0 @@
import { type FsDirectory, type FsEntry } from "./core"
import fstree from "./tree"
import index from "./index"
let cached_dir = fstree[0] //start at root
let working_path = ["/", "home", "user"]
const clone_working_path = () => [...working_path]
function get_working_dir_name() {
return working_path[working_path.length-1]
}
function get_working_dir_name_full(): string {
const w_dir_clone = clone_working_path()
const root = w_dir_clone.shift()
if (root) {
return root+w_dir_clone.join("/")
}
return "shift-error"
}
const enum SetDirStatus {
Valid,
NotFound,
NotADirectory,
Invalid
}
interface FsIterEntry {
readonly entry: FsDirectory | null,
readonly status: SetDirStatus
}
function find_fs_dir(working_dir_path_clone: string[], find_dir_name: string): FsIterEntry {
let cached_dir_clone = cached_dir.inner
for (let path_i = 0; path_i<working_dir_path_clone.length; path_i++) {
if (cached_dir_clone) {
let cached_dir_file_names: string[] = []
cached_dir_clone.forEach((file, file_i) => cached_dir_file_names.push(file.name))
const search_result = index.binary(cached_dir_clone, fstree[0])
if (working_dir_path_clone[path_i] === find_dir_name) {
cached_dir_clone = cached_dir_clone
if (path_i === working_dir_path_clone.length) {
const search_result = index.binary(cached_dir_file_names, find_dir_name)
if (search_result) {
}
} else {
continue
}
}
}
return { entry: null, status: SetDirStatus.Invalid }
}
return { entry: null, status: SetDirStatus.NotFound }
}
function set_working_dir(name: string): SetDirStatus {
if (name === ".") { return SetDirStatus.Valid }
const w_dir_clone = clone_working_path()
if (name === "..") {
w_dir_clone.pop()
} else {
w_dir_clone.push(name)
}
}
function get_working_dir_entries(): FsEntry[] {
}
export {
get_working_dir_name,
get_working_dir_name_full,
get_working_dir_entries,
set_working_dir,
SetDirStatus
}

View File

@ -1,19 +0,0 @@
import { Entry, Permissions } from "./core"
const user = [
Entry("about_me.txt", "about me inside", Permissions.rw),
Entry("services.txt", "services inside", Permissions.rw),
Entry("hi", [], Permissions.rw)
]
const home = [
Entry("user", user, Permissions.rw)
]
const root = [
Entry("home", home, Permissions.r),
Entry("bin", {}, Permissions.r),
]
const fstree = [
Entry("/", root, Permissions.r)
]
export default fstree

View File

@ -1,17 +0,0 @@
{
"name": "@rhpidfyre.io/web",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"sass": "^1.85.1",
"typescript": "^5.7.3",
"vite": "^6.2.0",
"vite-plugin-html": "^3.2.2"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

BIN
src/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

24
src/rt/crypto/generate.ts Normal file
View File

@ -0,0 +1,24 @@
interface SHA256 {
readonly secret: string
}
class Crypto {
protected inner: string
constructor(inner: string) {
this.inner = inner
}
public async sha256_hash(): Promise<SHA256> {
const encoder = new TextEncoder()
const hash = await crypto.subtle.digest("SHA-256", encoder.encode(this.inner))
const hash_as_uint8 = new Uint8Array(hash)
return { secret: Array.from(hash_as_uint8).map(byte => byte.toString(16).padStart(2, "0")).join("") }
}
}
export default Crypto
export {
type SHA256
}

View File

@ -1,6 +1,6 @@
import { cyan, green } from "../shell/color" import { cyan, green } from "../shell/color"
import { get_working_dir_name } from "../rfwfs/library"
import librfwfs, { username } from "../rfwfs/library"
import create from "./create" import create from "./create"
interface Ps1Prompt { interface Ps1Prompt {
@ -43,14 +43,20 @@ function ps1_element(user: HTMLSpanElement, dir: HTMLSpanElement) {
return display return display
} }
function working_dir() { function working_dir_name() {
const dir_name = get_working_dir_name() const dir = librfwfs.home.dir()
return dir_name === "user" ? "~" : dir_name if (dir) {
const dir_name = dir.name.read()
if (dir_name) {
return dir_name === username ? "~" : dir_name
}
}
return "?"
} }
function working_dir_element() { function working_dir_element() {
const user = cyan("user") const user = cyan("user")
const dir = green(" "+working_dir()) const dir = green(" "+working_dir_name())
return ps1_element(user, dir) return ps1_element(user, dir)
} }

15
src/rt/fs.ts Normal file
View File

@ -0,0 +1,15 @@
import rfwfs, { PERMISSION_FLAGS } from "./rfwfs/main"
const time_now = (Date.now()/1000) | 0
const fs = new rfwfs()
const root = fs.push_bulk_unsafe([
rfwfs.directory_in_root({
permissions: {wheel: PERMISSION_FLAGS.RWX, users: PERMISSION_FLAGS.NONE},
timestamp: time_now,
metadata: {},
name: "bin"
})
])
export default fs

179
src/rt/rfwfs/groups.ts Normal file
View File

@ -0,0 +1,179 @@
import { ROOT_ID } from "./main";
import wrap, { type WrapResult } from "./wrap";
import User from "./users";
type User_Index = [User, number]
type WrapUserSearch = WrapResult<User_Index | undefined, GroupSearch>
type SysGroupsNames = "wheel" | "users"
const enum SysGroups {
Wheel,
Users,
}
const enum UserMoveStatus {
Ok,
RootBlocked,
MovingNonExistentUser,
AlreadyInWheel,
AlreadyInUsers,
}
const enum GroupRemoveStatus {
Ok,
RootBlocked,
RemovingNonExistentUser,
}
const enum GroupSearch {
NotFound,
WheelResult,
UsersResult,
}
interface Groups {
wheel: Group,
users: Group,
together: () => User[]
}
class Group {
protected inner: User[];
private group_type: SysGroups;
constructor(type: SysGroups) {
this.group_type = type
this.inner = []
}
public users(): User[] {
return [...this.inner]
}
public type(): SysGroups {
return this.group_type
}
public type_as_name(): SysGroupsNames {
return this.type() === SysGroups.Wheel ? "wheel" : "users"
}
public add_user(user: User): boolean {
const duplicate = this.inner.find(user_in_group => user_in_group.uname() === user.uname())
if (!duplicate) {
this.inner.push(user)
return true
}
return false
}
public remove_user(user: User): User | undefined {
for (let i = 0; i<this.inner.length; i++) {
if (this.inner[i].uname() === user.uname()) {
this.inner.splice(i, 1)
return this.inner[i]
}
}
return undefined
}
}
const groups: Groups = {
wheel: new Group(SysGroups.Wheel),
users: new Group(SysGroups.Users),
together: function() {
return [...this.wheel.users(), ...this.users.users()]
}
}
function wrap_user_search(status: GroupSearch, result?: User_Index): WrapUserSearch {
return wrap(result, status)
}
function group_iter_for_user(uname: string, group_t: Group): User_Index | undefined {
const group_t_users = group_t.users()
for (let i = 0; i<group_t_users.length; i++) {
if (group_t_users[i].uname() === uname) {
return [group_t_users[i], i]
}
}
return undefined
}
function groups_find_user(uname: string): WrapUserSearch {
const exist_in_wheel = group_iter_for_user(uname, groups.wheel)
if (exist_in_wheel) {
return wrap_user_search(GroupSearch.WheelResult, exist_in_wheel)
}
const exist_in_users = group_iter_for_user(uname, groups.users)
if (exist_in_users) {
return wrap_user_search(GroupSearch.UsersResult, exist_in_users)
}
return wrap_user_search(GroupSearch.NotFound)
}
function group_add(new_user: User, group_t: Group): GroupSearch {
const dups = groups_find_user(new_user.uname())
if (dups.status === GroupSearch.NotFound) {
group_t.add_user(new_user)
}
return dups.status
}
function group_remove(uname: string, group_t: Group): GroupRemoveStatus {
if (uname !== ROOT_ID.NAME) {
const found_user = group_t.users().find(user => user.uname() === uname)
if (found_user) {
group_t.remove_user(found_user)
return GroupRemoveStatus.Ok
}
return GroupRemoveStatus.RemovingNonExistentUser
}
return GroupRemoveStatus.RootBlocked
}
function group_user_move(uname: string, new_group: SysGroups): UserMoveStatus {
if (uname === ROOT_ID.NAME) { return UserMoveStatus.RootBlocked }
const find_in_group = groups_find_user(uname)
if (find_in_group.status === GroupSearch.NotFound) { return UserMoveStatus.MovingNonExistentUser }
if (new_group === SysGroups.Wheel) {
if (find_in_group.status === GroupSearch.WheelResult) { return UserMoveStatus.AlreadyInWheel }
groups.wheel.add_user(groups.users.remove_user((find_in_group.result as User_Index)[0]) as User)
} else if (new_group === SysGroups.Users) {
if (find_in_group.status === GroupSearch.UsersResult) { return UserMoveStatus.AlreadyInUsers }
groups.users.add_user(groups.wheel.remove_user((find_in_group.result as User_Index)[0]) as User)
}
return UserMoveStatus.Ok
}
function group_wheel_add(new_user: User): GroupSearch {
return group_add(new_user, groups.wheel)
}
function group_wheel_remove(uname: string): GroupRemoveStatus {
return group_remove(uname, groups.wheel)
}
function group_users_add(new_user: User): GroupSearch {
return group_add(new_user, groups.users)
}
function group_users_remove(uname: string): GroupRemoveStatus {
return group_remove(uname, groups.users)
}
export default groups
export {
group_wheel_remove,
group_users_remove,
groups_find_user,
group_wheel_add,
group_users_add,
group_user_move,
type SysGroupsNames,
GroupRemoveStatus,
GroupSearch,
SysGroups,
Group,
}

24
src/rt/rfwfs/index.ts Normal file
View File

@ -0,0 +1,24 @@
import { type Entry } from "./main"
import wrap, { WrapResult } from "./wrap"
function wrap_bsearch<T extends Entry>(index: number, result: T): WrapResult<T, number> {
return wrap(result, index)
}
export default function directory_search<T extends Entry>(dir_files: T[], file_name: string): WrapResult<T, number> | undefined {
let start = 0
let end = dir_files.length-1
while (start<=end) {
const median = (start+end)>>1
const median_name = dir_files[median].name.__inner()
if (median_name === file_name) {
return wrap_bsearch(median, dir_files[median])
} else if (median_name<file_name) {
start = median+1
} else {
end = median-1
}
}
return undefined
}

60
src/rt/rfwfs/library.ts Normal file
View File

@ -0,0 +1,60 @@
import { wrap_entry, type WrapResultEntry } from "./wrap"
import { ReadStatus } from "./enum/status"
import rfwfs, { type DirectoryAny, type EntryCollection, type DirectoryAnyDepth } from "./main"
import fs from "../fs"
type Path = string[]
interface Home {
path: () => Path,
dir: () => DirectoryAny | undefined,
}
interface Librfwfs {
home: Home,
traverse_to: (path: Path) => WrapResultEntry<DirectoryAny, ReadStatus>
pwd_entry: <T extends EntryCollection<T>>(working_dir: T) => Path | undefined
}
let username: string = "user"
const librfwfs = {} as Librfwfs
librfwfs.traverse_to = function(path) {
let traverse = fs
for (const path_name of path) {
const find = traverse.inner.find(path_name)
if (find.status === ReadStatus.Ok) {
if (find.result && rfwfs.is_dir(find.result)) {
traverse = find.result as DirectoryAnyDepth
} else {
return wrap_entry(ReadStatus.Denied)
}
} else {
return wrap_entry(find.status)
}
}
return wrap_entry(ReadStatus.Ok, traverse)
}
librfwfs.pwd_entry = function(working_dir) {
}
librfwfs.home = {} as Home
librfwfs.home.path = function() {
return ["home", username]
}
librfwfs.home.dir = function() {
const traverse = librfwfs.traverse_to(this.path())
return traverse.status === ReadStatus.Ok ? traverse.result : undefined
}
export default librfwfs
export {
username
}

395
src/rt/rfwfs/main.ts Normal file
View File

@ -0,0 +1,395 @@
import wrap, { type WrapResult, ConstEnum, Option } from "./wrap"
import { SysGroups } from "./groups"
import directory_search from "./index"
import User, { LibUser } from "./users"
const enum EntryType {
Root,
File,
Directory,
Binary,
SymLink,
}
const enum PushStatus {
Ok,
Duplicate,
Denied,
}
const enum ReadStatus {
Ok,
NotFound,
NotInGroup,
Denied,
}
const enum ModifyStatus {
Ok,
NotInGroup,
Denied,
}
const enum ModifyAccessType {
Read,
Write,
}
const enum ROOT_ID {
TRUNK = "/",
NAME = "root",
UID = 0,
}
const enum PERMISSION_FLAGS {
NONE = -1,
R = 1 << 0,
W = 1 << 1,
X = 1 << 2,
RWX = PERMISSION_FLAGS.R | PERMISSION_FLAGS.W | PERMISSION_FLAGS.X
}
interface Permissions<W = Gate<PERMISSION_FLAGS>, U = Gate<PERMISSION_FLAGS>> {
wheel: W,
users: U,
}
type GroupPermissionsRoot = Permissions<Gate<PERMISSION_FLAGS>, Gate<PERMISSION_FLAGS.NONE>>
interface Metadata {
[index: string]: string
}
interface Entry<
T extends EntryType = EntryType,
P extends Permissions = Permissions,
N = Gate<string>
> {
readonly type: T,
permissions: P,
timestamp: Gate<number>,
metadata: Gate<Metadata>,
group: Gate<SysGroups>,
owner: Gate<User>,
name: N,
}
type Directory<T extends Entry> = DirectoryContainer<RfwfsDirectory<T>>
interface DirectoryContainer<T> extends Entry {
files: Gate<Entry[]>,
parent: Gate<T> | null,
}
interface Root extends Entry<EntryType.Root, GroupPermissionsRoot, ROOT_ID.TRUNK> {
parent: null,
files: Gate<Entry[]>,
}
interface DirectoryInRoot extends Entry<EntryType.Root, Permissions {
}
interface DirectoryInRootProperties {
permissions: Permissions<PERMISSION_FLAGS, PERMISSION_FLAGS.NONE>,
timestamp: number,
metadata: Metadata,
name: string,
}
/** Other directory types that can be treated as a single arbitrary directory.
Do not cast.
*/
type DirectoryAssociates<T extends Entry> = Directory<T> | DirectoryInRoot | Root
/** Other entry types that can be treated as a single arbitrary entry.
Do not cast.
*/
type EntryAssociates = Entry | Root
type WrapResultEntry<T extends Entry, U> = WrapResult<T | undefined, U>
type WrapResultNone<T> = WrapResult<Option.None, T>
type WrapEntryRead<V> = WrapResult<V | undefined, ModifyStatus>
function wrap_entry<T extends ConstEnum, U extends Entry>(status: T, result?: U): WrapResultEntry<U, T> {
return wrap(result, status)
}
function wrap_none<T extends ConstEnum>(status: T): WrapResultNone<T> {
return wrap(Option.None, status)
}
function wrap_entry_read<V>(status: ModifyStatus, result?: V): WrapEntryRead<V> {
return wrap(result, status)
}
function fs_dir_sort<T extends Entry>(dir: DirectoryAssociates<T>) {
dir.files.__inner().sort((a,z) => a.name.__inner().localeCompare(z.name.__inner()))
}
function fs_dir_clone<T extends Entry>(dir: DirectoryAssociates<T>, file_name: string): WrapResultEntry<T, ReadStatus> {
const clone_find = directory_search(dir.files.__inner(), file_name)
if (clone_find) {
return wrap_entry(ReadStatus.Ok, { ...clone_find.result as T })
}
return wrap_entry(ReadStatus.NotFound)
}
function fs_dir_find<T extends Entry>(dir: DirectoryAssociates<T>, file_name: string): WrapResultEntry<T, ReadStatus> {
const file_search = directory_search(dir.files.__inner(), file_name)
if (file_search) {
return wrap_entry(ReadStatus.Ok, file_search.result as T)
}
return wrap_entry(ReadStatus.NotFound)
}
function fs_dir_push<T extends Entry>(dir: DirectoryAssociates<T>, entry: Entry) {
const no_duplicates = directory_search(dir.files.__inner(), entry.name.__inner())
if (!no_duplicates) {
dir.files.__inner().push(entry)
fs_dir_sort(dir)
return wrap_none(PushStatus.Ok)
}
return wrap_none(PushStatus.Duplicate)
}
function fs_dir_pop<T extends Entry>(dir: DirectoryAssociates<T>, file_name: string): WrapResultEntry<T, ReadStatus> {
const pop_find = directory_search(dir.files.__inner(), file_name)
if (pop_find) {
dir.files.__inner().splice(pop_find.status, 1)
return wrap_entry(ReadStatus.Ok, pop_find.result as T)
}
return wrap_entry(ReadStatus.NotFound)
}
function user_group_perms(entry: EntryAssociates): PERMISSION_FLAGS | undefined {
const user = LibUser.current_sys_user
const current_user_group = user.group()
if (user.is_root() || current_user_group.type() === entry.group.__inner()) {
return entry.permissions[current_user_group.type_as_name()].__inner()
}
return undefined
}
function user_group_read_write<T extends Entry>(entry: DirectoryAssociates<T>): ModifyStatus {
if (LibUser.current_sys_user.is_root()) {
return ModifyStatus.Ok
}
const group_perms = user_group_perms(entry)
if (group_perms) {
return LibRfwfs.read_write_access(group_perms) ? ModifyStatus.Ok : ModifyStatus.Denied
}
return ModifyStatus.NotInGroup
}
class Gate<V> {
private inner: V;
protected entry: EntryAssociates;
constructor(entry: EntryAssociates, value: V) {
this.inner = value
this.entry = entry
}
private access_read_write(accessType: ModifyAccessType): ModifyStatus {
const group_perms = user_group_perms(this.entry)
if (group_perms) {
switch (accessType) {
case ModifyAccessType.Read:
return LibRfwfs.read_access(group_perms) ? ModifyStatus.Ok : ModifyStatus.Denied
case ModifyAccessType.Write:
return LibRfwfs.write_access(group_perms) ? ModifyStatus.Ok : ModifyStatus.Denied
}
}
return ModifyStatus.NotInGroup
}
public __inner(): V {
return this.inner
}
public read(): WrapEntryRead<V> {
switch (this.access_read_write(ModifyAccessType.Read)) {
case ModifyStatus.Ok:
return wrap_entry_read(ModifyStatus.Ok, this.inner)
case ModifyStatus.NotInGroup:
return wrap_entry_read(ModifyStatus.NotInGroup)
case ModifyStatus.Denied:
return wrap_entry_read(ModifyStatus.Denied)
}
}
public write<T extends V>(new_value: T): ModifyStatus {
switch (this.access_read_write(ModifyAccessType.Read)) {
case ModifyStatus.Ok:
this.inner = new_value
return ModifyStatus.Ok
case ModifyStatus.NotInGroup:
return ModifyStatus.NotInGroup
case ModifyStatus.Denied:
return ModifyStatus.Denied
}
}
}
class RfwfsDirectory<T extends Entry> {
public dir: DirectoryAssociates<T>;
constructor(dir: DirectoryAssociates<T>) {
this.dir = dir
}
public sort() {
fs_dir_sort(this.dir)
}
public clone(file_name: string): WrapResultEntry<Entry, ReadStatus> {
if (user_group_read_write(this.dir)) {
return fs_dir_clone(this.dir, file_name)
}
return wrap_entry(ReadStatus.Denied)
}
public find(file_name: string): WrapResultEntry<Entry, ReadStatus> {
if (user_group_read_write(this.dir)) {
return fs_dir_find(this.dir, file_name)
}
return wrap_entry(ReadStatus.Denied)
}
public push<E extends Entry>(entry: E): WrapResultNone<PushStatus> {
if (user_group_read_write(this.dir)) {
return fs_dir_push(this.dir, entry)
}
return wrap_none(PushStatus.Denied)
}
public pop(file_name: string): WrapResultEntry<Entry, ReadStatus> {
if (user_group_read_write(this.dir)) {
fs_dir_pop(this.dir, file_name)
}
return wrap_entry(ReadStatus.Denied)
}
public push_bulk_unsafe(dirs: T[]) {
dirs.forEach(dir => this.dir.files.__inner().push(dir))
this.sort()
}
public push_unsafe(dir: T) {
this.dir.files.__inner().push(dir)
this.sort()
}
}
class LibRfwfs {
public static is_root<T extends Entry>(entry: T): boolean {
return entry.type === EntryType.Root
}
public static is_dir<T extends Entry>(entry: T): boolean {
return entry.type === EntryType.Directory
}
public static is_file<T extends Entry>(entry: T): boolean {
return entry.type === EntryType.File
}
public static is_binary<T extends Entry>(entry: T): boolean {
return entry.type === EntryType.Binary
}
public static is_symlink<T extends Entry>(entry: T): boolean {
return entry.type === EntryType.SymLink
}
public static read_access(permissions: PERMISSION_FLAGS): boolean {
return (permissions & PERMISSION_FLAGS.R) !== 0
}
public static write_access(permissions: PERMISSION_FLAGS): boolean {
return (permissions & PERMISSION_FLAGS.W) !== 0
}
public static execute_access(permissions: PERMISSION_FLAGS): boolean {
return (permissions & PERMISSION_FLAGS.X) !== 0
}
public static read_write_access(permissions: PERMISSION_FLAGS): boolean {
return LibRfwfs.read_access(permissions) && LibRfwfs.write_access(permissions)
}
public static directory_in_root(properties: DirectoryInRootProperties): RfwfsDirectory<DirectoryInRoot> {
const dir_o = { type: EntryType.Directory } as DirectoryInRoot
dir_o.permissions = {
wheel: new Gate(dir_o, properties.permissions.wheel),
users: new Gate(dir_o, properties.permissions.users),
}
dir_o.metadata = new Gate(dir_o, properties.metadata)
dir_o.timestamp = new Gate(dir_o, properties.timestamp)
dir_o.files = new Gate(dir_o, [])
dir_o.name = new Gate(dir_o, properties.name)
dir_o.parent = null
return new RfwfsDirectory(dir_o)
}
}
class Rfwfs extends LibRfwfs {
public root: Root;
constructor() {
super()
this.root = { type: EntryType.Root } as Root
this.root.permissions = {
wheel: new Gate(this.root, PERMISSION_FLAGS.RWX),
users: new Gate(this.root, PERMISSION_FLAGS.NONE)
}
this.root.timestamp = new Gate(this.root, (Date.now()/1000) | 0)
this.root.parent = null
this.root.files = new Gate(this.root, [])
this.root.name = ROOT_ID.TRUNK
}
public sort() {
fs_dir_sort(this.root)
}
public clone(file_name: string): WrapResultEntry<Entry, ReadStatus> {
if (user_group_read_write(this.root)) {
return fs_dir_clone(this.root, file_name)
}
return wrap_entry(ReadStatus.Denied)
}
public find(file_name: string): WrapResultEntry<Entry, ReadStatus> {
if (user_group_read_write(this.root)) {
return fs_dir_find(this.root, file_name)
}
return wrap_entry(ReadStatus.Denied)
}
public push<T extends Entry>(entry: T): WrapResultNone<PushStatus> {
if (user_group_read_write(this.root)) {
return fs_dir_push(this.root, entry)
}
return wrap_none(PushStatus.Denied)
}
public pop(file_name: string): WrapResultEntry<Entry, ReadStatus> {
if (user_group_read_write(this.root)) {
fs_dir_pop(this.root, file_name)
}
return wrap_entry(ReadStatus.Denied)
}
public push_bulk_unsafe(dirs: DirectoryInRoot[]) {
dirs.forEach(dir => this.root.files.__inner().push(dir))
this.sort()
}
public push_unsafe(dir: DirectoryInRoot) {
this.root.files.__inner().push(dir)
this.sort()
}
}
export default Rfwfs
export {
type DirectoryInRoot,
type RfwfsDirectory,
type Directory,
type Entry,
PERMISSION_FLAGS,
EntryType,
ROOT_ID,
}

182
src/rt/rfwfs/users.ts Normal file
View File

@ -0,0 +1,182 @@
import { ROOT_ID } from "./main";
import Crypto, { type SHA256 } from "../crypto/generate";
import groups, { groups_find_user, GroupSearch, SysGroups, Group } from "./groups";
const enum UserSet {
Ok,
AlreadyLoggedIn,
UserDoesNotExist,
}
const enum PasswordCheckStatus {
Ok,
MinBound,
MaxBound,
}
const enum PasswordSetStatus {
Ok,
RootRequiresPassword,
MinBound,
MaxBound,
Incorrect,
}
const enum SetUnameStatus {
Ok,
CantChangeRootName,
NotFound,
WheelResult,
UsersResult,
}
const enum PASS_BOUNDS {
MIN = 4,
MAX = 1 << 12, //64 ^ 2
}
let uid_count: number = 0
class LibUser {
public static current_sys_user: User;
public static get_sys_user(): User {
return groups.together().find(user => user.is_logged_in()) as User
}
public static set_sys_user(uname: string): UserSet {
const found_user = groups_find_user(uname)
const result_user_i = found_user.result
if (!result_user_i) { return UserSet.UserDoesNotExist }
if (result_user_i[0].is_logged_in()) { return UserSet.AlreadyLoggedIn }
LibUser.current_sys_user = result_user_i[0]
return UserSet.Ok
}
public static in_password_bounds(password: string): PasswordCheckStatus {
//Math.min(Math.max(PASS_BOUNDS.MIN, password.length), PASS_BOUNDS.MAX) < PASS_BOUNDS.MAX
if (password.length > PASS_BOUNDS.MIN) {
if (password.length < PASS_BOUNDS.MAX) {
return PasswordCheckStatus.Ok
}
return PasswordCheckStatus.MaxBound
}
return PasswordCheckStatus.MinBound
}
}
class User {
private inner_password?: SHA256;
private inner_group: Group;
private inner_name: string;
private inner_uid: number;
private current: boolean;
constructor(name: string, group: Group, password?: SHA256) {
const root_creation = name === ROOT_ID.NAME
if (root_creation) {
this.inner_uid = 0
this.inner_group = group
} else {
uid_count += 1
this.inner_uid = uid_count
this.inner_group = group
}
this.inner_name = name
this.current = root_creation
this.inner_password = password
}
private set_as_current(): boolean {
LibUser.get_sys_user().current = false
LibUser.current_sys_user = this
this.current = true
return this.current
}
public is_logged_in(): boolean {
return this.current
}
public in_wheel(): boolean {
return this.inner_group.type() === SysGroups.Wheel
}
public password(): SHA256 | undefined {
return this.inner_password
}
public is_root(): boolean {
return this.inner_name === ROOT_ID.NAME && this.inner_uid === ROOT_ID.UID
}
public group(): Group {
return this.inner_group
}
public uname(): string {
return this.inner_name
}
public uid(): number {
return this.inner_uid
}
public async check_password(password?: string): Promise<boolean> {
if (!(password && this.inner_password) || (await new Crypto(password).sha256_hash()).secret === this.inner_password.secret) {
return true
}
return false
}
public async login(password?: string): Promise<boolean> {
if (!this.inner_password || (password && await this.check_password(password))) {
return this.set_as_current()
}
return false
}
public set_uname(new_uname: string): SetUnameStatus {
if (this.is_root()) { return SetUnameStatus.CantChangeRootName }
const search = groups_find_user(new_uname)
switch (search.status) {
case GroupSearch.NotFound:
this.inner_name = new_uname
break
case GroupSearch.UsersResult:
return SetUnameStatus.UsersResult
case GroupSearch.WheelResult:
return SetUnameStatus.WheelResult
}
return SetUnameStatus.Ok
}
public async set_password(current_password: string, new_password?: string): Promise<PasswordSetStatus> {
if (await this.check_password(current_password)) {
if (new_password) {
switch (LibUser.in_password_bounds(new_password)) {
case PasswordCheckStatus.Ok:
this.inner_password = await new Crypto(new_password).sha256_hash()
break
case PasswordCheckStatus.MinBound:
return PasswordSetStatus.MinBound
case PasswordCheckStatus.MaxBound:
return PasswordSetStatus.MaxBound
}
} else {
if (this.is_root()) { return PasswordSetStatus.RootRequiresPassword }
//This user has no password
this.inner_password = undefined
}
return PasswordSetStatus.Ok
}
return PasswordSetStatus.Incorrect
}
}
groups.wheel.add_user(
new User(ROOT_ID.NAME, groups.wheel, { secret: "90a956efae97cca5ec584977d96a236aa76b0a07def9fcafab87fd221a1d2cfe" })
)
groups.users.add_user(
new User("user", groups.users)
)
export default User
export {
LibUser
}

24
src/rt/rfwfs/wrap.ts Normal file
View File

@ -0,0 +1,24 @@
const enum Option {
None,
Some,
}
type ConstEnum = number
interface WrapResult<T, U> {
/** The resulting value if `U` is a success status */
readonly result: T,
/** Represents some arbitrary extra value, usually a status */
readonly status: U,
}
function wrap<T, U>(result: T, some: U): WrapResult<T, U> {
return { result: result, status: some }
}
export default wrap
export {
type WrapResult,
type ConstEnum,
Option,
}

View File

@ -1,10 +1,11 @@
import { set_working_dir, SetDirStatus } from "../../../rfwfs/library" import { ReadStatus } from "../../../rfwfs/enum"
import type { Args, Term } from "../list" import type { Args, Term } from "../list"
import lib from "../../../rfwfs/library"
import stdout from "../../../elements/stdout" import stdout from "../../../elements/stdout"
export default function cd(term: Term, args: Args): boolean { export default function cd(term: Term, args: Args): boolean {
const new_dir_status = set_working_dir(args[1]) const new_dir_status = lib.traverse_to(args)
if (new_dir_status === SetDirStatus.NotADirectory) { if (new_dir_status === SetDirStatus.NotADirectory) {
term.appendChild(stdout(`cd: "${args[1]}" is not a directory`)) term.appendChild(stdout(`cd: "${args[1]}" is not a directory`))

View File

@ -16,6 +16,7 @@
/* Linting */ /* Linting */
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true, "noUncheckedSideEffectImports": true,
"noImplicitAny": true, "noImplicitAny": true,

View File

@ -1,10 +1,6 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import { createHtmlPlugin } from 'vite-plugin-html'
export default defineConfig({ export default defineConfig({
plugins: [
createHtmlPlugin({minify: true}),
],
root: "src", root: "src",
build: { build: {
outDir: "../dist" outDir: "../dist"