Compare commits
No commits in common. "dda72bf67d4bb8a88ea018c4f4d0bc86408faf73" and "f9016ffc1089fa20c1a9d00af6dc65a3b7870077" have entirely different histories.
dda72bf67d
...
f9016ffc10
55
deno.lock
55
deno.lock
|
@ -1,55 +0,0 @@
|
|||
{
|
||||
"version": "3",
|
||||
"packages": {
|
||||
"specifiers": {
|
||||
"jsr:@std/json@^0.223.0": "jsr:@std/json@0.223.0",
|
||||
"jsr:@std/streams@^0.223.0": "jsr:@std/streams@0.223.0",
|
||||
"npm:superjson@1.13.3": "npm:superjson@1.13.3"
|
||||
},
|
||||
"jsr": {
|
||||
"@std/json@0.223.0": {
|
||||
"integrity": "9a4a255931dd0397924c6b10bb6a72fe3e28ddd876b981ada2e3b8dd0764163f",
|
||||
"dependencies": [
|
||||
"jsr:@std/streams@^0.223.0"
|
||||
]
|
||||
},
|
||||
"@std/streams@0.223.0": {
|
||||
"integrity": "d6b28e498ced3960b04dc5d251f2dcfc1df244b5ec5a48dc23a8f9b490be3b99"
|
||||
}
|
||||
},
|
||||
"npm": {
|
||||
"copy-anything@3.0.5": {
|
||||
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
|
||||
"dependencies": {
|
||||
"is-what": "is-what@4.1.16"
|
||||
}
|
||||
},
|
||||
"is-what@4.1.16": {
|
||||
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"superjson@1.13.3": {
|
||||
"integrity": "sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==",
|
||||
"dependencies": {
|
||||
"copy-anything": "copy-anything@3.0.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"remote": {
|
||||
"https://deno.land/std@0.203.0/dotenv/load.ts": "0636983549b98f29ab75c9a22a42d9723f0a389ece5498fe971e7bb2556a12e2",
|
||||
"https://deno.land/std@0.203.0/dotenv/mod.ts": "1da8c6d0e7f7d8a5c2b19400b763bc11739df24acec235dda7ea2cfd3d300057",
|
||||
"https://deno.land/std@0.216.0/json/_common.ts": "6444c6ea166514eb379f778d9216c1d8eb159f97c753aeeb1fc44cd091e30544",
|
||||
"https://deno.land/std@0.216.0/json/common.ts": "33f1a4f39a45e2f9f357823fd0b5cf88b63fb4784b8c9a28f8120f70a20b23e9",
|
||||
"https://deno.land/std@0.216.0/json/concatenated_json_parse_stream.ts": "13a707615e03e5ea93ac81e5f00420e3b7764c4e3fa88043e63bbac87ebe62ce",
|
||||
"https://deno.land/std@0.216.0/json/json_parse_stream.ts": "2740652ea73726cd0f2edc89188b35d64a1ec15dc8cf7fd87db52a0170bc182c",
|
||||
"https://deno.land/std@0.216.0/json/json_stringify_stream.ts": "269633e63d4e38ab3ba31e76a4292d11b9eb3151e26ff4f49dee1c264b4878fa",
|
||||
"https://deno.land/std@0.216.0/json/mod.ts": "acd3e39a6c68c70ee7fa93991a8a8f02d799f7394db3cef09594e2dd8fd69814",
|
||||
"https://deno.land/std@0.216.0/streams/to_transform_stream.ts": "4c4836455ef89bab9ece55975ee3a819f07d3d8b0e43101ec7f4ed033c8a2b61"
|
||||
},
|
||||
"workspace": {
|
||||
"dependencies": [
|
||||
"jsr:@std/json@^0.223.0"
|
||||
]
|
||||
}
|
||||
}
|
128
src/db/mod.ts
128
src/db/mod.ts
|
@ -1,128 +0,0 @@
|
|||
import { Credential, Group, Machine, Ressource, Service, User } from '@models'
|
||||
|
||||
//!TODO link ressources (get, list)
|
||||
//!TODO Purge unused ressources (delete)
|
||||
|
||||
export class Db {
|
||||
#kv: Deno.Kv
|
||||
static async init(path?: string) {
|
||||
const kv = await Deno.openKv(path ?? Deno.env.get('DB_PATH'))
|
||||
return new Db(kv)
|
||||
}
|
||||
|
||||
private constructor(kv: Deno.Kv) {
|
||||
this.#kv = kv
|
||||
}
|
||||
|
||||
get storage() {
|
||||
return this.#kv
|
||||
}
|
||||
|
||||
get prefix() {
|
||||
return {
|
||||
ressource: 'ressource',
|
||||
}
|
||||
}
|
||||
|
||||
get ressource() {
|
||||
return {
|
||||
credential: this.#ressourceStorage<Credential>('credential', Credential),
|
||||
group: this.#ressourceStorage<Group>('group', Group),
|
||||
machine: this.#ressourceStorage<Machine>('machine', Machine),
|
||||
service: this.#ressourceStorage<Service>('service', Service),
|
||||
user: this.#ressourceStorage<User>('user', User),
|
||||
}
|
||||
}
|
||||
|
||||
#ressourceStorage<T extends Ressource>(
|
||||
type: RessourceType<T>,
|
||||
Builder: RessourceBuilder<T>,
|
||||
) {
|
||||
return {
|
||||
get: (ressource: Pick<T, 'uuid'>) =>
|
||||
this.#get<T>(type, Builder, ressource),
|
||||
set: (ressources: T[]) => this.#set<T>(type, ressources),
|
||||
delete: (ressources: Pick<T, 'uuid'>[]) =>
|
||||
this.#delete<T>(type, ressources),
|
||||
list: (filter: (ressource: T) => boolean = () => true) =>
|
||||
this.#list<T>(type, Builder, filter),
|
||||
}
|
||||
}
|
||||
|
||||
async #get<T extends Ressource>(
|
||||
type: RessourceType<T>,
|
||||
Builder: RessourceBuilder<T>,
|
||||
entry: Pick<T, 'uuid'>,
|
||||
): Promise<T> {
|
||||
const json = await this.#kv.get<RessourceJson<T>>([
|
||||
this.prefix.ressource,
|
||||
type,
|
||||
entry.uuid,
|
||||
])
|
||||
if (json.value) {
|
||||
//@ts-expect-error Type union of Ressource types for Builder
|
||||
return Builder.fromJSON(json.value)
|
||||
}
|
||||
throw new ReferenceError(
|
||||
`no ressource.${type} was found with uuid: "${entry.uuid}"`,
|
||||
)
|
||||
}
|
||||
#set<T extends Ressource>(type: RessourceType<T>, entries: T[]) {
|
||||
const atomic = this.#kv.atomic()
|
||||
|
||||
for (const entry of entries) {
|
||||
//! TODO check if `refs` exists
|
||||
const key = [this.prefix.ressource, type, entry.uuid]
|
||||
atomic.set(key, entry.toJSON())
|
||||
}
|
||||
|
||||
return atomic.commit()
|
||||
}
|
||||
#delete<T extends Ressource>(
|
||||
type: RessourceType<T>,
|
||||
entries: Pick<T, 'uuid'>[],
|
||||
): Promise<Deno.KvCommitResult | Deno.KvCommitError> {
|
||||
const atomic = this.#kv.atomic()
|
||||
|
||||
for (const entry of entries) {
|
||||
//! TODO check if `refs` exists
|
||||
atomic.delete([this.prefix.ressource, type, entry.uuid])
|
||||
}
|
||||
|
||||
return atomic.commit()
|
||||
}
|
||||
async *#list<T extends Ressource>(
|
||||
type: RessourceType<T>,
|
||||
Builder: RessourceBuilder<T>,
|
||||
filter: (entry: T) => boolean,
|
||||
): AsyncGenerator<T, void, void> {
|
||||
const list = this.#kv.list<RessourceJson<T>>({
|
||||
prefix: [this.prefix.ressource, type],
|
||||
})
|
||||
for await (const entry of list) {
|
||||
const value = entry.value
|
||||
//@ts-expect-error Type union of Ressource types for Builder
|
||||
const ressource = Builder.fromJSON(value) as T
|
||||
if (filter(ressource)) {
|
||||
yield ressource
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type RessourceType<T extends Ressource> = T extends Credential ? 'credential'
|
||||
: T extends Group ? 'group'
|
||||
: T extends Machine ? 'machine'
|
||||
: T extends Service ? 'service'
|
||||
: T extends User ? 'user'
|
||||
: never
|
||||
|
||||
type RessourceBuilder<T extends Ressource> = T extends Credential
|
||||
? typeof Credential
|
||||
: T extends Group ? typeof Group
|
||||
: T extends Machine ? typeof Machine
|
||||
: T extends Service ? typeof Service
|
||||
: T extends User ? typeof User
|
||||
: never
|
||||
|
||||
type RessourceJson<T extends Ressource> = ReturnType<T['toJSON']>
|
|
@ -1,11 +0,0 @@
|
|||
export {
|
||||
Ref,
|
||||
type RefResolver,
|
||||
type RefString,
|
||||
} from '@/src/models/utils/ref.ts'
|
||||
export { Credential } from '@models/credential.ts'
|
||||
export { Group } from '@models/group.ts'
|
||||
export { Machine } from '@models/machine.ts'
|
||||
export type { Ressource } from '@models/ressource.ts'
|
||||
export { Service } from '@models/service.ts'
|
||||
export { User } from '@models/user.ts'
|
|
@ -1,105 +0,0 @@
|
|||
import { ToJson } from '@/types.ts'
|
||||
import { Ressource } from '@models/ressource.ts'
|
||||
|
||||
export class Credential extends Ressource {
|
||||
static fromJSON(
|
||||
json: ToJson<Credential>,
|
||||
): Credential {
|
||||
return new Credential(json)
|
||||
}
|
||||
|
||||
static create(
|
||||
{ category, store, name }: Pick<Credential, 'category' | 'store' | 'name'>,
|
||||
): Credential {
|
||||
const { uuid, createdAt, updatedAt } = super.create({ name })
|
||||
return new Credential({ uuid, createdAt, updatedAt, name, category, store })
|
||||
}
|
||||
|
||||
#category: 'password' | 'ssh' | 'passkey'
|
||||
#store: CredentialCategory<Credential['category']>
|
||||
|
||||
private constructor(
|
||||
{ category, store, ...props }:
|
||||
& Pick<Credential, 'category' | 'store'>
|
||||
& Pick<Ressource, 'name' | 'uuid' | 'createdAt' | 'updatedAt'>,
|
||||
) {
|
||||
super(props)
|
||||
|
||||
if (!['password', 'ssh', 'passkey'].includes(category)) {
|
||||
throw new TypeError(
|
||||
`category is "${category}" but ('password' | 'ssh' | 'passkey') is required`,
|
||||
)
|
||||
}
|
||||
this.#category = category
|
||||
this.#store = Object.freeze(store)
|
||||
}
|
||||
|
||||
get type(): 'credential' {
|
||||
return 'credential'
|
||||
}
|
||||
|
||||
get category() {
|
||||
return this.#category
|
||||
}
|
||||
|
||||
get store() {
|
||||
return Object.freeze(this.#store)
|
||||
}
|
||||
|
||||
update(
|
||||
props: Partial<Omit<Credential, 'type' | 'uuid' | 'createdAt'>>,
|
||||
): Credential {
|
||||
const { updatedAt } = super.update(props)
|
||||
const credential = new Credential({ ...this, ...props, updatedAt })
|
||||
return credential
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
type: this.type,
|
||||
category: this.category,
|
||||
store: Object.freeze(this.store),
|
||||
} as const
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `Credential (${JSON.stringify(this)})`
|
||||
}
|
||||
}
|
||||
|
||||
export interface Credential extends Ressource {
|
||||
type: 'credential'
|
||||
category: 'password' | 'ssh' | 'passkey'
|
||||
store: Readonly<CredentialCategory<Credential['category']>>
|
||||
}
|
||||
|
||||
type CredentialCategory<T extends 'password' | 'ssh' | 'passkey'> = T extends
|
||||
'password' ? {
|
||||
store: {
|
||||
hash: string //hex or b64 of Uint8Array
|
||||
alg: string
|
||||
salt: string //hex or b64 of Uint8Array
|
||||
}
|
||||
}
|
||||
: T extends 'ssh' ? {
|
||||
store: {
|
||||
publicKey: string
|
||||
}
|
||||
}
|
||||
: T extends 'passkey' ? {
|
||||
store: Record<string, unknown>
|
||||
}
|
||||
: never
|
||||
|
||||
/*
|
||||
PassKey store:
|
||||
{
|
||||
publicKey: Uint8Array
|
||||
keyId: string
|
||||
transport: string
|
||||
counter: number
|
||||
}
|
||||
*/
|
||||
|
||||
//new Uint8Array(Object.values(JSON.parse(JSON.stringify(new Uint8Array([1, 2, 3])))))
|
|
@ -1,113 +0,0 @@
|
|||
import { ToJson } from '@/types.ts'
|
||||
import { Group } from '@models/group.ts'
|
||||
import { Ressource } from '@models/ressource.ts'
|
||||
import { Ref } from '@/src/models/utils/ref.ts'
|
||||
|
||||
export class Machine extends Ressource {
|
||||
static fromJSON(
|
||||
json: ToJson<Machine>,
|
||||
): Machine {
|
||||
const url = new URL(json.url)
|
||||
const avatar = new URL(json.avatar)
|
||||
const groups = json.groups.map((group) => Ref.fromString<Group>(group))
|
||||
|
||||
return new Machine({ ...json, url, avatar, groups })
|
||||
}
|
||||
|
||||
static create(
|
||||
{ tags, url, status, avatar, groups, ...props }:
|
||||
& Pick<Machine, 'tags' | 'url' | 'status' | 'avatar' | 'groups'>
|
||||
& Pick<Ressource, 'name' | 'uuid' | 'createdAt' | 'updatedAt'>,
|
||||
): Machine {
|
||||
return new Machine({ ...props, tags, url, status, avatar, groups })
|
||||
}
|
||||
|
||||
#tags: readonly string[]
|
||||
#url: URL
|
||||
#status:
|
||||
| 'ready'
|
||||
| 'busy'
|
||||
| 'unavailable'
|
||||
| 'discontinued'
|
||||
| 'error'
|
||||
| 'unknown'
|
||||
#avatar: URL
|
||||
#groups: readonly Ref<Group>[]
|
||||
|
||||
private constructor(
|
||||
{ tags, url, status, avatar, groups, ...props }:
|
||||
& Pick<Machine, 'tags' | 'url' | 'status' | 'avatar' | 'groups'>
|
||||
& Pick<Ressource, 'name' | 'uuid' | 'createdAt' | 'updatedAt'>,
|
||||
) {
|
||||
super(props)
|
||||
|
||||
this.#tags = Object.freeze(tags)
|
||||
this.#url = new URL(url)
|
||||
if (!['working', 'pending', 'ready', 'unavailable'].includes(status)) {
|
||||
throw new TypeError(
|
||||
`status is "${status}" but ('ready' | 'busy' | 'unavailable' | 'discontinued' | 'error' | 'unknown') is required`,
|
||||
)
|
||||
}
|
||||
this.#status = status
|
||||
this.#avatar = new URL(avatar)
|
||||
this.#groups = Object.freeze(groups)
|
||||
}
|
||||
|
||||
get type(): 'machine' {
|
||||
return 'machine'
|
||||
}
|
||||
get tags() {
|
||||
return this.#tags.slice()
|
||||
}
|
||||
get url() {
|
||||
return new URL(this.#url)
|
||||
}
|
||||
get status() {
|
||||
return this.#status
|
||||
}
|
||||
get avatar() {
|
||||
return new URL(this.#avatar)
|
||||
}
|
||||
get groups() {
|
||||
return this.#groups
|
||||
}
|
||||
|
||||
update(
|
||||
props: Partial<Omit<Machine, 'type' | 'uuid' | 'createdAt'>>,
|
||||
): Machine {
|
||||
const { updatedAt } = super.update(props)
|
||||
const machine = new Machine({ ...this, ...props, updatedAt })
|
||||
return machine
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
type: this.type,
|
||||
tags: this.tags.slice(),
|
||||
url: this.url.toJSON(),
|
||||
status: this.status,
|
||||
avatar: this.avatar.toJSON(),
|
||||
groups: Object.freeze(this.groups.map((group) => group.toJSON())),
|
||||
} as const
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `Machine (${JSON.stringify(this)})`
|
||||
}
|
||||
}
|
||||
|
||||
export interface Machine extends Ressource {
|
||||
type: 'machine'
|
||||
tags: string[]
|
||||
url: URL
|
||||
status:
|
||||
| 'ready'
|
||||
| 'busy'
|
||||
| 'unavailable'
|
||||
| 'discontinued'
|
||||
| 'error'
|
||||
| 'unknown'
|
||||
avatar: URL
|
||||
groups: readonly Ref<Group>[]
|
||||
}
|
|
@ -37,40 +37,6 @@ export class Ref<T extends Ressource> extends String {
|
|||
return new Ref<T>(Ref.parse(string))
|
||||
}
|
||||
|
||||
static dbResolver(db: Db) {
|
||||
return <T extends Ressource>(ref: RefString<T>) => {
|
||||
const { type, uuid } = Ref.parse(ref)
|
||||
return db.ressource[type].get({ uuid })
|
||||
}
|
||||
}
|
||||
|
||||
static restResolver(endpoint: string | URL) {
|
||||
return async <T extends Ressource>(ref: RefString<T>) => {
|
||||
const { type, uuid } = Ref.parse(ref)
|
||||
const url = new URL(`${type}s/${uuid}`, endpoint)
|
||||
const response = await fetch(url)
|
||||
const json = await response.json()
|
||||
|
||||
if (type === 'user') {
|
||||
return User.fromJSON(json)
|
||||
}
|
||||
if (type === 'machine') {
|
||||
return Machine.fromJSON(json)
|
||||
}
|
||||
if (type === 'service') {
|
||||
return Service.fromJSON(json)
|
||||
}
|
||||
if (type === 'group') {
|
||||
return Group.fromJSON(json)
|
||||
}
|
||||
if (type === 'credential') {
|
||||
return Credential.fromJSON(json)
|
||||
}
|
||||
|
||||
throw new TypeError(`unknown ref type "${type}"`)
|
||||
}
|
||||
}
|
||||
|
||||
private constructor({ uuid, type }: { uuid: UUID; type: T['type'] }) {
|
||||
super(Ref.#toString({ uuid, type }))
|
||||
this.#type = type
|
||||
|
@ -100,3 +66,37 @@ export class Ref<T extends Ressource> extends String {
|
|||
return this.toString()
|
||||
}
|
||||
}
|
||||
|
||||
export function RefDbResolver(db: Db) {
|
||||
return <T extends Ressource>(ref: RefString<T>) => {
|
||||
const { type, uuid } = Ref.parse(ref)
|
||||
return db.ressource[type].get({ uuid })
|
||||
}
|
||||
}
|
||||
|
||||
export function RefRestResolver(endpoint: string) {
|
||||
return async <T extends Ressource>(ref: RefString<T>) => {
|
||||
const { type, uuid } = Ref.parse(ref)
|
||||
const url = new URL(`${type}/${uuid}`, endpoint)
|
||||
const response = await fetch(url)
|
||||
const json = await response.json()
|
||||
|
||||
if (type === 'user') {
|
||||
return User.fromJSON(json)
|
||||
}
|
||||
if (type === 'machine') {
|
||||
return Machine.fromJSON(json)
|
||||
}
|
||||
if (type === 'service') {
|
||||
return Service.fromJSON(json)
|
||||
}
|
||||
if (type === 'group') {
|
||||
return Group.fromJSON(json)
|
||||
}
|
||||
if (type === 'credential') {
|
||||
return Credential.fromJSON(json)
|
||||
}
|
||||
|
||||
throw new TypeError(`unknown ref type "${type}"`)
|
||||
}
|
||||
}
|
||||
|
|
28
utils.ts
28
utils.ts
|
@ -1,28 +0,0 @@
|
|||
import { Login } from '@/types.ts'
|
||||
|
||||
export function toLogin({
|
||||
firstname,
|
||||
lastname,
|
||||
}: {
|
||||
firstname: string
|
||||
lastname: string
|
||||
}): Login {
|
||||
return `${sanitizeString(firstname)}.${sanitizeString(lastname)}`
|
||||
}
|
||||
|
||||
export function sanitizeString(str: string): string {
|
||||
return str.toLocaleLowerCase().split('').map(sanitizeChar).join('')
|
||||
}
|
||||
|
||||
function sanitizeChar(char: string): string {
|
||||
//decompose unicode and remove diacritical marks
|
||||
char = char.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
|
||||
if (char.match(/[a-zA-Z0-9]|-|_/)) return char
|
||||
return '_'
|
||||
}
|
||||
|
||||
export const regex = {
|
||||
uuidV4: /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/,
|
||||
isoDateString: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z$/,
|
||||
mailAddress: /\S+@\S+\.\S+/,
|
||||
}
|
Loading…
Reference in a new issue