@authlocal/authlocal
Version:
User-sovereign Logins For All
112 lines (91 loc) • 2.97 kB
text/typescript
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}`
}
}