Restructure files

This commit is contained in:
aet 2021-11-11 23:56:06 -05:00
parent b4b21561ed
commit bdd46a530c
29 changed files with 271 additions and 179 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
drafts
node_modules
mochawesome-report
lib

View File

@ -39,6 +39,7 @@
"sass": "^1.43.2",
"sinon": "^11.1.2",
"sinon-chai": "^3.7.0",
"tslib": "^2.3.1",
"ts-node": "^10.2.1",
"tsconfig-paths": "^3.11.0",
"typescript": "^4.4.3"

View File

@ -0,0 +1,6 @@
{
"name": "opvault-adapters",
"dependencies": {
"opvault.js": "*"
}
}

View File

@ -1,5 +1,5 @@
import { Buffer } from "buffer"
import type { IAdapter, IFileSystem } from "./index"
import type { IAdapter, IFileSystem } from "opvault.js/src/adapter"
function normalize(path: string) {
return path.replace(/^\//, "")

View File

@ -1,5 +1,5 @@
import { Buffer } from "buffer"
import type { IAdapter, IFileSystem } from "./index"
import type { IAdapter, IFileSystem } from "opvault.js/src/adapter"
export class FileSystem implements IFileSystem {
private paths = new Set<string>()

View File

@ -4,7 +4,7 @@
"version": "0.0.1",
"license": "LGPL-3.0-or-later",
"scripts": {
"build": "rollup -c; cp src/adapters/index.d.ts lib/adapters/; prettier --write lib >/dev/null",
"build": "rollup -c; prettier --write lib >/dev/null",
"build:docs": "typedoc --out docs src/index.ts --excludePrivate"
},
"dependencies": {

View File

@ -8,7 +8,6 @@ import { dependencies } from "./package.json"
export default () => ({
input: {
index: "./src/index.ts",
"adapters/node": "./src/adapters/node.ts",
},
external: builtinModules.concat(Object.keys(dependencies)),
output: {

View File

@ -1,3 +1,6 @@
import { promises as fs, existsSync } from "fs"
import { webcrypto } from "crypto"
/**
* An object that implements basic file system functionalities.
*/
@ -53,3 +56,19 @@ export interface IAdapter {
*/
subtle: SubtleCrypto
}
/**
* Default Node.js adapter. This can be used while using `opvault.js`
* in a Node.js environment.
*/
export const nodeAdapter: IAdapter = {
fs: {
readFile: path => fs.readFile(path, "utf-8"),
readBuffer: path => fs.readFile(path),
writeFile: fs.writeFile,
readdir: fs.readdir,
isDirectory: async path => fs.stat(path).then(x => x.isDirectory()),
exists: async path => existsSync(path),
},
subtle: (webcrypto as any).subtle,
}

View File

@ -1,20 +0,0 @@
import { promises as fs, existsSync } from "fs"
import { webcrypto } from "crypto"
import type { IAdapter } from "./index"
/**
* Default Node.js adapter. This can be used while using `opvault.js`
* in a Node.js environment.
*/
export const nodeAdapter: IAdapter = {
fs: {
readFile: path => fs.readFile(path, "utf-8"),
readBuffer: path => fs.readFile(path),
writeFile: fs.writeFile,
readdir: fs.readdir,
isDirectory: async path => fs.stat(path).then(x => x.isDirectory()),
exists: async path => existsSync(path),
},
subtle: (webcrypto as any).subtle,
}

View File

@ -1,12 +1,12 @@
import { Buffer } from "buffer"
import { decryptData } from "../decipher"
import type { IAdapter } from "../adapters"
import { createEventEmitter } from "../ee"
import { HMACAssertionError } from "../errors"
import type { i18n } from "../i18n"
import type { ItemDetails, Overview, Profile } from "../types"
import { setIfAbsent } from "../util"
import type { EncryptedItem } from "./item"
import { decryptData } from "./decipher"
import type { IAdapter } from "./adapter"
import { createEventEmitter } from "./ee"
import { HMACAssertionError } from "./errors"
import type { i18n } from "./i18n"
import type { ItemDetails, Overview, Profile } from "./types"
import { setIfAbsent } from "./util"
import type { EncryptedItem } from "./models/item"
/** Encryption and MAC */
export interface Cipher {

View File

@ -1,6 +1,6 @@
import { resolve, extname, basename } from "path"
import invariant from "tiny-invariant"
import type { IFileSystem } from "./adapters"
import type { IFileSystem } from "./adapter"
import { once } from "./util"
export type OnePasswordFileManager = ReturnType<typeof OnePasswordFileManager>

View File

@ -1,6 +1,6 @@
import { resolve } from "path"
import { Vault } from "./models/vault"
import type { IAdapter } from "./adapters"
import type { IAdapter } from "./adapter"
import { asyncMap } from "./util"
export type { Vault } from "./models/vault"
@ -18,7 +18,7 @@ interface IOptions {
/**
* Adapter used to interact with the file system and cryptography modules
*/
adapter?: IAdapter
adapter?: IAdapter | Promise<IAdapter>
}
/**
@ -26,11 +26,11 @@ interface IOptions {
*/
export class OnePassword {
readonly #path: string
readonly #adapter: IAdapter
readonly #adapter: IAdapter | Promise<IAdapter>
constructor({
path,
adapter = process.browser ? null : require("./adapters/node").nodeAdapter,
adapter = process.browser ? null! : import("./adapter").then(x => x.nodeAdapter),
}: IOptions) {
this.#adapter = adapter
this.#path = path
@ -40,11 +40,11 @@ export class OnePassword {
* @returns A list of names of profiles of the current vault.
*/
async getProfileNames() {
const [fs, path] = [this.#adapter.fs, this.#path]
const children = await fs.readdir(path)
const { fs } = await this.#adapter
const children = await fs.readdir(this.#path)
const profiles: string[] = []
await asyncMap(children, async child => {
const fullPath = resolve(path, child)
const fullPath = resolve(this.#path, child)
if (
(await fs.isDirectory(fullPath)) &&
(await fs.exists(resolve(fullPath, "profile.js")))
@ -59,6 +59,6 @@ export class OnePassword {
* @returns A OnePassword Vault instance.
*/
async getProfile(profileName: string) {
return await Vault.of(this.#path, profileName, this.#adapter)
return await Vault.of(this.#path, profileName, await this.#adapter)
}
}

View File

@ -1,5 +1,5 @@
import { Buffer } from "buffer"
import type { Crypto } from "./crypto"
import type { Crypto } from "../crypto"
import { invariant } from "../errors"
type integer = number

View File

@ -1,5 +1,5 @@
import type { ItemDetails, Overview } from "../types"
import type { Crypto } from "./crypto"
import type { Crypto } from "../crypto"
import { Attachment } from "./attachment"
import { NotUnlockedError } from "../errors"
import type { Category } from "../models"
@ -8,7 +8,7 @@ export interface EncryptedItem {
category: string // "001"
/** Unix seconds */
created: integer
d: string // "b3BkYXRhMbt"
d: string // details, bass64
folder: string // 32 chars
hmac: string // base64
k: string // base64

View File

@ -1,9 +1,9 @@
import type { IAdapter } from "../adapters"
import type { IAdapter } from "../adapter"
import { HMACAssertionError, invariant } from "../errors"
import { OnePasswordFileManager } from "../fs"
import { i18n } from "../i18n"
import type { EncryptedItem } from "./item"
import { Crypto } from "./crypto"
import { Crypto } from "../crypto"
import { Item } from "./item"
import type { Profile } from "../types"
import { WeakValueMap } from "../weakMap"

View File

@ -130,6 +130,9 @@ export interface ItemDetails {
}[]
/** Web form fields */
fields?: ItemField[]
/** Plain password items */
backupKeys?: string[]
password?: string
}
export interface Folder {

View File

@ -12,8 +12,13 @@ fs.writeFileSync(
dtsPath,
`type Translation = Record<string, string>;
declare const exportee: {
${Object.keys(json)
.map(x => `${x}: Translation;`)
${Object.entries(json)
.map(
([category, value]) =>
`${category}: {\n${Object.keys(value)
.map(key => ` ${key}: Translation;`)
.join("\n")}\n };`
)
.join("\n ")}
};
export default exportee;

View File

@ -1,6 +1,7 @@
import styled from "@emotion/styled"
import type { Attachment, AttachmentMetadata, Item, ItemField } from "opvault.js"
import { useEffect, useState } from "react"
import type { ItemDetails } from "opvault.js/src/types"
import { memo, useEffect, useState } from "react"
import { useTranslate } from "../i18n"
import { CategoryIcon } from "./CategoryIcon"
import { ItemDates } from "./ItemDates"
@ -10,6 +11,7 @@ import {
FieldTitle,
ItemDetailsFieldView,
} from "./ItemField"
import { PasswordFieldView } from "./ItemFieldValue"
import { ItemWarning } from "./ItemWarning"
interface ItemViewProps {
@ -57,6 +59,22 @@ const AttachmentContainer = styled.div`
margin: 5px 0;
`
const SectionsView: React.FC<{ sections?: ItemDetails["sections"] }> = ({ sections }) =>
sections?.length ? (
<div style={{ marginBottom: 20 }}>
{sections
.filter(s => s.fields?.some(x => x.v != null))
.map((section, i) => (
<div key={i}>
{section.title != null && <SectionTitle>{section.title}</SectionTitle>}
{section.fields?.map((field, j) => (
<ItemFieldView key={j} field={field} />
))}
</div>
))}
</div>
) : null
const FieldsView: React.FC<{ fields?: ItemField[] }> = ({ fields }) =>
fields?.length ? (
<div style={{ marginBottom: 20 }}>
@ -71,7 +89,7 @@ const TagsView: React.FC<{ tags?: string[] }> = ({ tags }) => {
if (!tags?.length) return null
return (
<ExtraField>
<FieldTitle>{t.noun_tags}</FieldTitle>
<FieldTitle>{t.noun.tags}</FieldTitle>
<div>
{tags.map((tag, i) => (
<Tag key={i}>{tag}</Tag>
@ -81,65 +99,69 @@ const TagsView: React.FC<{ tags?: string[] }> = ({ tags }) => {
)
}
export const ItemView: React.FC<ItemViewProps> = ({ className, item }) => (
<Container className={className}>
<Inner>
<ItemWarning item={item} />
<Header>
{item.details.fields == null}
<Icon category={item.category} />
<ItemTitle>{item.overview.title}</ItemTitle>
</Header>
<details>
<summary>JSON</summary>
<pre>
{JSON.stringify({ overview: item.overview, details: item.details }, null, 2)}
</pre>
</details>
const JSONView = memo<{ item: Item }>(({ item }) => (
<details>
<summary>JSON</summary>
<pre>
{JSON.stringify({ overview: item.overview, details: item.details }, null, 2)}
</pre>
</details>
))
<div style={{ marginBottom: 20 }}>
{item.details.sections
?.filter(s => s.fields?.some(x => x.v != null))
.map((section, i) => (
<div key={i}>
{section.title != null && <SectionTitle>{section.title}</SectionTitle>}
{section.fields?.map((field, j) => (
<ItemFieldView key={j} field={field} />
export const ItemView: React.FC<ItemViewProps> = ({ className, item }) => {
const t = useTranslate()
return (
<Container className={className}>
<Inner>
<ItemWarning item={item} />
<Header>
{item.details.fields == null}
<Icon category={item.category} />
<ItemTitle>{item.overview.title}</ItemTitle>
</Header>
<JSONView item={item} />
<div style={{ height: 10 }}></div>
<SectionsView sections={item.details.sections} />
<FieldsView fields={item.details.fields} />
{item.details.notesPlain != null && (
<ExtraField>
<FieldTitle>notes</FieldTitle>
<div>
<p>{item.details.notesPlain}</p>
</div>
</ExtraField>
)}
{item.details.password != null && (
<ExtraField>
<FieldTitle>{t.label.password}</FieldTitle>
<PasswordFieldView field={{ v: item.details.password }} />
</ExtraField>
)}
<TagsView tags={item.overview.tags} />
{item.attachments.length > 0 && (
<ExtraField>
<FieldTitle>attachments</FieldTitle>
<div>
{item.attachments.map((file, i) => (
<AttachmentView key={i} file={file} />
))}
</div>
))}
</div>
</ExtraField>
)}
<FieldsView fields={item.details.fields} />
{item.details.notesPlain != null && (
<ExtraField>
<FieldTitle>notes</FieldTitle>
<div>
<p>{item.details.notesPlain}</p>
</div>
<ItemDates item={item} />
</ExtraField>
)}
<TagsView tags={item.overview.tags} />
{item.attachments.length > 0 && (
<ExtraField>
<FieldTitle>attachments</FieldTitle>
<div>
{item.attachments.map((file, i) => (
<AttachmentView key={i} file={file} />
))}
</div>
</ExtraField>
)}
<ExtraField>
<ItemDates item={item} />
</ExtraField>
</Inner>
</Container>
)
</Inner>
</Container>
)
}
function AttachmentView({ file }: { file: Attachment }) {
const [metadata, setMetadata] = useState<AttachmentMetadata>()

View File

@ -14,10 +14,10 @@ export const ItemDates: React.FC<{ item: Item }> = ({ item }) => {
return (
<Container>
<div>
{t.label_last_updated}: {new Date(item.updatedAt).toLocaleString()}
{t.label.last_updated}: {new Date(item.updatedAt).toLocaleString()}
</div>
<div>
{t.label_created_at}: {new Date(item.createdAt).toLocaleString()}
{t.label.created_at}: {new Date(item.createdAt).toLocaleString()}
</div>
</Container>
)

View File

@ -7,7 +7,7 @@ const Container = styled.menu`
box-shadow: #0004 0px 1px 4px;
left: 99%;
margin-block-start: 0;
min-width: 120px;
min-width: 150px;
padding-inline-start: 0;
position: absolute;
top: 0;
@ -39,6 +39,7 @@ const Item = styled.div`
height: 2.5em;
align-items: center;
padding-left: 1em;
padding-right: 5px;
position: relative;
&:hover {
background-color: #ddd;

View File

@ -9,8 +9,10 @@ import { useItemFieldContextMenu } from "./ItemFieldContextMenu"
const Container = styled.div``
export { Password as PasswordFieldView }
const Password: React.FC<{
field: ItemSection.Concealed
field: Pick<ItemSection.Concealed, "v">
}> = ({ field }) => {
const [show, setShow] = useState(false)
const [bigText, showBigText] = useState(false)

View File

@ -1,7 +1,7 @@
import { createContext, memo, useContext, useEffect, useMemo, useState } from "react"
import texts from "./texts.yml"
type Keys = keyof typeof texts
const categories = Object.keys(texts)
const ALLOWED = new Set(["en", "fr"])
const LOCALSTORAGE_KEY = "preferred-locale"
@ -40,21 +40,29 @@ export function useTranslate() {
const { locale } = useContext(LocaleContext)
const t = useMemo(
() =>
new Proxy(
{},
{
get(_, p: string) {
if (
process.env.NODE_ENV === "development" &&
!Object.prototype.hasOwnProperty.call(texts, p)
) {
throw new Error(`t.${p} does not exist.`)
Object.fromEntries(
categories.map(category => [
category,
new Proxy(
{},
{
get(_, p: string) {
const obj = (texts as any)[category]
if (
process.env.NODE_ENV === "development" &&
!Object.prototype.hasOwnProperty.call(obj, p)
) {
throw new Error(`t.${p} does not exist.`)
}
return obj[p][locale]
},
}
return (texts as any)[p][locale]
},
}
),
])
) as {
[key in Keys]: string
[category in keyof typeof texts]: {
[key in keyof typeof texts[category]]: string
}
},
[locale]
)

View File

@ -1,40 +1,51 @@
# /* spellchecker: disable */
label_choose_a_vault:
en: Pick a vault
fr: Choisir un coffre
label:
choose_a_vault:
en: Pick a vault
fr: Choisir un coffre
label_no_vault_selected:
en: No vault is selected.
fr: Aucun coffre nest sélectionné.
no_vault_selected:
en: No vault is selected.
fr: Aucun coffre nest sélectionné.
label_last_updated:
en: Last Updated
fr: Dernière modification
last_updated:
en: Last Updated
fr: Dernière modification
label_created_at:
en: Created At
fr: Créé
created_at:
en: Created At
fr: Créé
label_password_placeholder:
en: Master Password
fr: Mot de passe principal
password_placeholder:
en: Master Password
fr: Mot de passe principal
noun_vault:
en: vault
fr: coffre
username:
en: Username
fr: Nom dutilisateur
noun_tags:
en: tags
fr: mots-clés
password:
en: Password
fr: Mot de passe
action_lock:
en: Lock
fr: Vérouiller
noun:
vault:
en: vault
fr: coffre
action_unlock:
en: Unlock
fr: Déverouiller
tags:
en: tags
fr: mots-clés
action_go_back:
en: Back
fr: Revenir
action:
lock:
en: Lock
fr: Vérouiller
unlock:
en: Unlock
fr: Déverouiller
go_back:
en: Back
fr: Revenir

View File

@ -102,17 +102,17 @@ export const Unlock: React.FC<{
return (
<Container>
<div style={{ marginBottom: 10 }}>
<BackButton onClick={onReturn} title={t.action_go_back}>
<BackButton onClick={onReturn} title={t.action.go_back}>
<IoMdArrowRoundBack />
</BackButton>
<Select
title={t.noun_vault}
title={t.noun.vault}
value={profile}
onChange={e => setProfile(e.currentTarget.value)}
>
{profiles.map(p => (
<option key={p} value={p}>
{t.noun_vault}: {p}
{t.noun.vault}: {p}
</option>
))}
</Select>
@ -122,14 +122,14 @@ export const Unlock: React.FC<{
type="password"
value={password}
onChange={e => setPassword(e.currentTarget.value)}
placeholder={t.label_password_placeholder}
placeholder={t.label.password_placeholder}
onKeyUp={onKeyUp}
/>
<Submit
type="submit"
disabled={!profile || !password}
onClick={unlock}
title={t.action_unlock}
title={t.action.unlock}
>
<FaUnlock />
</Submit>

View File

@ -88,25 +88,36 @@ export const VaultView: React.FC<{ vault: Vault; onLock(): void }> = ({
arrayFrom(vault.values()).then(setItems)
}, [vault])
const filtered = useMemo(
() =>
sortedItem
.filter(x => x.category !== Category.Tombstone)
.filter(
search
? x =>
stringCompare(search, x.overview.title) ||
stringCompare(search, x.overview.ainfo)
: () => true
),
[sortedItem, search]
)
const filtered = useMemo(() => {
const items = sortedItem.filter(x => x.category !== Category.Tombstone)
let res: Item[] = items
if (search) {
res = []
for (const x of items) {
const compare = Math.max(
stringCompare(search, x.overview.title),
stringCompare(search, x.overview.ainfo)
) as CompareResult
switch (compare) {
case CompareResult.NoMatch:
continue
case CompareResult.Includes:
res.push(x)
break
case CompareResult.Equals:
res.unshift(x)
break
}
}
}
return res
}, [sortedItem, search])
return (
<Container>
<ListContainer className={scrollbar}>
<div style={{ margin: "10px 10px", display: "flex" }}>
<LockButton onClick={onLock} title={t.action_lock}>
<LockButton onClick={onLock} title={t.action.lock}>
<FiLock />
</LockButton>
<SearchContainer>
@ -147,8 +158,20 @@ async function arrayFrom<T>(generator: AsyncGenerator<T, void, unknown>) {
return list
}
function stringCompare(search: string, source?: string) {
if (!search) return true
if (!source) return false
return source.toLocaleLowerCase().includes(search.toLocaleLowerCase())
enum CompareResult {
NoMatch,
Includes,
Equals,
}
function stringCompare(search: string, source?: string) {
if (!search) return CompareResult.Includes
if (!source) return CompareResult.NoMatch
source = source.toLocaleLowerCase()
search = search.toLocaleUpperCase()
const includes = source.includes(search.toLocaleLowerCase())
if (includes) {
return source.length === search.length ? CompareResult.Equals : CompareResult.Includes
}
return CompareResult.NoMatch
}

View File

@ -87,8 +87,8 @@ const PickOPVault: React.FC<{
return (
<PickOPVaultContainer>
<button onClick={onClick}>{t.label_choose_a_vault}</button>
<PickOPVaultInfo>{t.label_no_vault_selected}</PickOPVaultInfo>
<button onClick={onClick}>{t.label.choose_a_vault}</button>
<PickOPVaultInfo>{t.label.no_vault_selected}</PickOPVaultInfo>
</PickOPVaultContainer>
)
}

11
pnpm-lock.yaml generated
View File

@ -35,6 +35,7 @@ importers:
sinon-chai: ^3.7.0
ts-node: ^10.2.1
tsconfig-paths: ^3.11.0
tslib: ^2.3.1
typescript: ^4.4.3
devDependencies:
'@types/chai': 4.2.22
@ -68,8 +69,15 @@ importers:
sinon-chai: 3.7.0_chai@4.3.4+sinon@11.1.2
ts-node: 10.2.1_8304ecd715830f7c190b4d1dea90b100
tsconfig-paths: 3.11.0
tslib: 2.3.1
typescript: 4.4.3
packages/adapters:
specifiers:
opvault.js: '*'
dependencies:
opvault.js: link:../opvault.js
packages/opvault.js:
specifiers:
'@rollup/plugin-json': ^4.1.0
@ -137,6 +145,9 @@ importers:
typescript: 4.4.3
vite: 2.6.13_sass@1.43.4
packages/web/dist:
specifiers: {}
packages:
/7zip-bin/5.1.1:

View File

@ -2,8 +2,8 @@ import { resolve } from "path";
import { describe, it, beforeEach } from "mocha";
import { expect } from "chai";
import type { Vault } from "../packages/opvault.js/index";
import { OnePassword } from "../packages/opvault.js/index";
import type { Vault } from "../packages/opvault.js/src/index";
import { OnePassword } from "../packages/opvault.js/src/index";
describe("OnePassword", () => {
const freddy = resolve(__dirname, "../freddy-2013-12-04.opvault");

View File

@ -1,7 +1,7 @@
import { describe, it } from "mocha";
import { expect } from "chai";
import { WeakValueMap } from "../packages/opvault.js/weakMap";
import { WeakValueMap } from "../packages/opvault.js/src/weakMap";
declare const gc: () => void;