UNPKG

@authlocal/authlocal

Version:

User-sovereign Logins For All

112 lines (91 loc) 2.97 kB
import {Base64, Text} from "@benev/slate" import {Passport} from "./passport.js" import {ensure} from "./utils/ensure.js" import {maxNameLength} from "./utils/validation.js" import {crushUsername} from "./utils/crush-username.js" import {PassportsFileData, PassportData} from "./types.js" export class PassportsFile { static readonly format = "authlocal.org passports" static readonly extension = "passport" static readonly version = 4 static #ingestData(raw: any): PassportsFileData { let data: PassportsFileData | null = null const legacyFormat = "authduo.org passports" if ( !("format" in raw && (raw.format === PassportsFile.format || raw.format === legacyFormat)) || !("version" in raw && typeof raw.version === "number")) throw new Error(`invalid format`) // v2 and lower is invalid if (raw.version <= 2) { throw new Error(`invalid version ${raw.version}`) } // v3 gets migrated to v4 if (raw.version === 3) { // we simply renamed the format, so the migration is just to use the new format name raw.format = PassportsFile.format raw.version = 4 } // v4 is the current version if (raw.version === 4) { data = raw } if (!data) throw new Error(`unknown version ${raw.version}`) return { format: ensure.string("format", data.format), version: ensure.number("version", data.version), passports: ensure.array("array", data.passports.map((p): PassportData => ({ name: ensure.string("name", p.name).slice(0, maxNameLength), created: ensure.number("created", p.created), keypair: { thumbprint: ensure.string("thumbprint", p.keypair.thumbprint), publicKey: ensure.string("public", p.keypair.publicKey), privateKey: ensure.string("private", p.keypair.privateKey), }, }))) } } static fromData(raw: any) { const data = PassportsFile.#ingestData(raw) const passports = new this() passports.add(...data.passports.map(d => Passport.fromData(d))) return passports } #map = new Map<string, Passport>() list() { return [...this.#map.values()] } add(...additions: Passport[]) { for (const passport of additions) this.#map.set(passport.thumbprint, passport) return this } delete(...deletions: Passport[]) { for (const passport of deletions) this.#map.delete(passport.thumbprint) return this } deleteAll() { this.#map.clear() return this } toData(): PassportsFileData { return { format: PassportsFile.format, version: PassportsFile.version, passports: [...this.#map.values()] .map(passport => passport.toData()), } } filename() { const passports = this.list() return passports.length === 1 ? `${crushUsername(passports.at(0)!.name)}.${PassportsFile.extension}` : `passports.${PassportsFile.extension}` } href() { const text = JSON.stringify(this.toData(), undefined, "\t") const encoded = Base64.string(Text.bytes(text)) return `data:application/octet-stream;base64,${encoded}` } }