diff --git a/.dockerignore b/.dockerignore index d6dc5af..e280464 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,15 +1,15 @@ * -!/packages/backend/src -!/packages/backend/Cargo.lock -!/packages/backend/Cargo.toml +!/packages +!/package.json +!/pnpm-lock.yaml +!/pnpm-workspace.yaml -!/packages/frontend/locales -!/packages/frontend/src -!/packages/frontend/static -!/packages/frontend/.npmrc -!/packages/frontend/package.json -!/packages/frontend/pnpm-lock.yaml -!/packages/frontend/svelte.config.js -!/packages/frontend/tsconfig.json -!/packages/frontend/vite.config.js +**/target +**/node_modules +**/dist +**/bin +**/*.tsbuildinfo +**/build +**/.svelte +**/.svelte-kit diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d6d27b2..ff1fe16 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -13,10 +13,10 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: - node-version: "16" + node-version-file: '.nvmrc' - - uses: docker/setup-qemu-action@v1 - - uses: docker/setup-buildx-action@v1 + - uses: docker/setup-qemu-action@v2 + - uses: docker/setup-buildx-action@v2 with: install: true - name: Build docker image @@ -30,7 +30,6 @@ jobs: - name: Run your tests run: npm test - uses: actions/upload-artifact@v2 - if: always() with: name: test-results path: test-results diff --git a/.gitignore b/.gitignore index f3198e7..4c85f1f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,10 @@ +.env +*.tsbuildinfo +node_modules +dist +bin -# Backend target -# Client -.DS_Store -node_modules -/.svelte -/build -/functions -.env - -General +# Testing test-results diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..d3e33db --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v18.16 diff --git a/Dockerfile b/Dockerfile index a1681a8..5e8cb58 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,29 @@ # FRONTEND FROM node:16-alpine as client WORKDIR /tmp -RUN npm install -g pnpm@7 -COPY ./packages/frontend ./ -RUN pnpm install -RUN pnpm exec svelte-kit sync +RUN npm install -g pnpm@8 +COPY . . +RUN pnpm install --frozen-lockfile +# WORKDIR /tmp/packages/frontend +# RUN pnpm exec svelte-kit sync RUN pnpm run build # BACKEND -FROM rust:1.64-alpine as backend +FROM rust:1.69-alpine as backend WORKDIR /tmp RUN apk add libc-dev openssl-dev alpine-sdk COPY ./packages/backend/Cargo.* ./ -# https://blog.rust-lang.org/2022/06/22/sparse-registry-testing.html -RUN rustup update nightly -ENV CARGO_UNSTABLE_SPARSE_REGISTRY=true -RUN cargo +nightly fetch +RUN cargo fetch COPY ./packages/backend ./ -RUN cargo +nightly build --release +RUN cargo build --release # RUNNER FROM alpine WORKDIR /app COPY --from=backend /tmp/target/release/cryptgeon . -COPY --from=client /tmp/build ./frontend +COPY --from=client /tmp/packages/frontend/build ./frontend ENV FRONTEND_PATH="./frontend" ENV REDIS="redis://redis/" EXPOSE 8000 diff --git a/cryptgeon.code-workspace b/cryptgeon.code-workspace index 83ba629..cc6da4b 100644 --- a/cryptgeon.code-workspace +++ b/cryptgeon.code-workspace @@ -1,16 +1,17 @@ { "folders": [ - { - "path": "." - }, - { - "path": "packages/backend" - } - ], + { + "path": "." + }, + { + "path": "packages/backend" + }, + { + "path": "packages/frontend" + } +], "settings": { - "cSpell.words": ["ciphertext", "cryptgeon"], - "i18n-ally.enabledFrameworks": ["svelte"], - "i18n-ally.keystyle": "nested", - "i18n-ally.localesPaths": ["packages/frontend/locales"] + "i18n-ally.localesPaths": ["packages/frontend/locales"], + "cSpell.words": ["cryptgeon"] } } diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index 2e05170..9a6f0e7 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -10,7 +10,6 @@ services: - 6379:6379 app: - image: cupcakearmy/cryptgeon:test build: . env_file: .dev.env depends_on: diff --git a/package.json b/package.json index 55f8537..66a6e3d 100644 --- a/package.json +++ b/package.json @@ -2,17 +2,16 @@ "scripts": { "dev:docker": "docker-compose -f docker-compose.dev.yaml up redis", "dev:packages": "pnpm --parallel run dev", - "dev:proxy": "node proxy.mjs", "dev": "run-p dev:*", "test": "playwright test --project chrome firefox safari", "test:local": "playwright test --project local", "test:server": "docker compose -f docker-compose.dev.yaml up", - "test:prepare": "docker compose -f docker-compose.dev.yaml build" + "test:prepare": "docker compose -f docker-compose.dev.yaml build", + "build": "pnpm run --recursive --filter=!@cryptgeon/backend build" }, "devDependencies": { - "@playwright/test": "^1.29.2", - "@types/node": "^16.18.11", - "http-proxy": "^1.18.1", + "@playwright/test": "^1.32.3", + "@types/node": "^16.18.24", "npm-run-all": "^4.1.5" } } diff --git a/packages/backend/Cargo.toml b/packages/backend/Cargo.toml index 6de3029..73166cf 100644 --- a/packages/backend/Cargo.toml +++ b/packages/backend/Cargo.toml @@ -8,6 +8,9 @@ edition = "2021" name = "cryptgeon" path = "src/main.rs" +[registries.crates-io] +protocol = "sparse" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/packages/backend/package.json b/packages/backend/package.json index d5efad1..bcb0667 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { - "name": "backend", "private": true, + "name": "@cryptgeon/backend", "scripts": { "dev": "cargo watch -x 'run --bin cryptgeon'", "build": "cargo build --release", diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..77271ce --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,43 @@ +{ + "name": "@cryptgeon/cli", + "type": "module", + "scripts": { + "dev": "esbuild ./src/index.ts --bundle --platform=node --outfile=dist/index.cjs --watch", + "build": "esbuild ./src/index.ts --bundle --platform=node --outfile=dist/index.cjs", + "bin": "pkg ." + }, + "bin": { + "cryptgeon": "./dist/index.cjs" + }, + "files": [ + "dist" + ], + "pkg": { + "scripts": "dist/**/*.js", + "targets": [ + "node18-macos-arm64", + "node18-macos-x64", + "node18-linux-arm64", + "node18-linux-x64", + "node18-win-arm64", + "node18-win-x64" + ], + "outputPath": "bin" + }, + "devDependencies": { + "@types/inquirer": "^9.0.3", + "@types/mime": "^3.0.1", + "esbuild": "^0.17.18", + "pkg": "^5.8.1", + "typescript": "^4.9.5" + }, + "dependencies": { + "@commander-js/extra-typings": "^9.5.0", + "@cryptgeon/shared": "workspace:*", + "commander": "^9.5.0", + "inquirer": "^9.2.0", + "mime": "^3.0.0", + "occulto": "^2.0.1", + "pretty-bytes": "^6.1.0" + } +} diff --git a/packages/cli/src/download.ts b/packages/cli/src/download.ts new file mode 100644 index 0000000..eb3a356 --- /dev/null +++ b/packages/cli/src/download.ts @@ -0,0 +1,63 @@ +import { Adapters, get, info, setBase } from '@cryptgeon/shared' +import inquirer from 'inquirer' +import { access, constants, writeFile } from 'node:fs/promises' +import { basename, resolve } from 'node:path' +import { Hex } from 'occulto' +import pretty from 'pretty-bytes' + +import { exit } from './utils' + +export async function download(url: URL) { + setBase(url.origin) + const id = url.pathname.split('/')[2] + await info(id).catch(() => exit('Note does not exist or is expired')) + const note = await get(id) + + const password = url.hash.slice(1) + const key = Hex.decode(password) + + const couldNotDecrypt = () => exit('Could not decrypt note. Probably an invalid password') + switch (note.meta.type) { + case 'file': + const files = await Adapters.Files.decrypt(note.contents, key).catch(couldNotDecrypt) + if (!files) { + exit('No files found in note') + return + } + console.log(files) + const { names } = await inquirer.prompt([ + { + type: 'checkbox', + message: 'What files should be saved?', + name: 'names', + choices: files.map((file) => ({ + value: file.name, + name: `${file.name} - ${file.type} - ${pretty(file.size, { binary: true })}`, + checked: true, + })), + }, + ]) + + const selected = files.filter((file) => names.includes(file.name)) + + if (!selected.length) exit('No files selected') + + await Promise.all( + files.map(async (file) => { + let filename = resolve(file.name) + try { + // If exists -> prepend timestamp to not overwrite the current file + await access(filename, constants.R_OK) + filename = resolve(`${Date.now()}-${file.name}`) + } catch {} + await writeFile(filename, new Uint8Array(await file.contents.arrayBuffer())) + console.log(`Saved: ${basename(filename)}`) + }) + ) + break + case 'text': + const plaintext = await Adapters.Text.decrypt(note.contents, key).catch(couldNotDecrypt) + console.log(plaintext) + break + } +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 0000000..b6c5a61 --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,82 @@ +#!/usr/bin/env node + +import { Argument, Option, program } from '@commander-js/extra-typings' +import { setBase, status } from '@cryptgeon/shared' + +import { download } from './download.js' +import { parseFile, parseNumber } from './parsers.js' +import { uploadFiles, uploadText } from './upload.js' +import { exit } from './utils.js' + +const defaultServer = process.env['CRYPTGEON_SERVER'] || 'https://cryptgeon.org' +const server = new Option('-s --server ', 'the cryptgeon server to use').default(defaultServer) +const files = new Argument('', 'Files to be sent').argParser(parseFile) +const text = new Argument('', 'Text content of the note') +const password = new Option('-p --password ', 'manually set a password') +const url = new Argument('', 'The url to open') +const views = new Option('-v --views ', 'Amount of views before getting destroyed').argParser(parseNumber) +const minutes = new Option('-m --minutes ', 'Minutes before the note expires').argParser(parseNumber) + +async function checkConstrains(constrains: { views?: number; minutes?: number }) { + const { views, minutes } = constrains + if (views && minutes) exit('cannot set view and minutes constrains simultaneously') + if (!views && !minutes) constrains.views = 1 + + const response = await status() + if (views && views > response.max_views) + exit(`Only a maximum of ${response.max_views} views allowed. ${views} given.`) + if (minutes && minutes > response.max_expiration) + exit(`Only a maximum of ${response.max_expiration} minutes allowed. ${minutes} given.`) +} + +program.name('cryptgeon').version('1.0.0').configureHelp({ showGlobalOptions: true }) + +program + .command('info') + .addOption(server) + .action(async (options) => { + setBase(options.server) + const response = await status() + for (const key of Object.keys(response)) { + if (key.startsWith('theme_')) delete response[key as keyof typeof response] + } + console.table(response) + }) + +const send = program.command('send') +send + .command('file') + .addArgument(files) + .addOption(server) + .addOption(views) + .addOption(minutes) + .action(async (files, options) => { + setBase(options.server!) + await checkConstrains(options) + await uploadFiles(files, { views: options.views, expiration: options.minutes }) + }) +send + .command('text') + .addArgument(text) + .addOption(server) + .addOption(views) + .addOption(minutes) + .action(async (text, options) => { + setBase(options.server!) + await checkConstrains(options) + await uploadText(text, { views: options.views, expiration: options.minutes }) + }) + +program + .command('open') + .addArgument(url) + .action(async (note, options) => { + try { + const url = new URL(note) + await download(url) + } catch { + exit('Invalid URL') + } + }) + +program.parse() diff --git a/packages/cli/src/parsers.ts b/packages/cli/src/parsers.ts new file mode 100644 index 0000000..6cbda70 --- /dev/null +++ b/packages/cli/src/parsers.ts @@ -0,0 +1,27 @@ +import { InvalidArgumentError, InvalidOptionArgumentError } from '@commander-js/extra-typings' +import { accessSync, constants } from 'node:fs' +import path from 'node:path' + +export function parseFile(value: string, before: string[] = []) { + try { + const file = path.resolve(value) + accessSync(file, constants.R_OK) + return [...before, file] + } catch { + throw new InvalidArgumentError('cannot access file') + } +} + +export function parseURL(value: string, _: URL): URL { + try { + return new URL(value) + } catch { + throw new InvalidArgumentError('is not a valid url') + } +} + +export function parseNumber(value: string, _: number): number { + const n = parseInt(value, 10) + if (isNaN(n)) throw new InvalidOptionArgumentError('invalid number') + return n +} diff --git a/packages/cli/src/upload.ts b/packages/cli/src/upload.ts new file mode 100644 index 0000000..f22dec1 --- /dev/null +++ b/packages/cli/src/upload.ts @@ -0,0 +1,49 @@ +import { Blob } from 'node:buffer' +import { readFile, stat } from 'node:fs/promises' +import { basename } from 'node:path' + +import { Adapters, BASE, create, FileDTO, Note } from '@cryptgeon/shared' +import mime from 'mime' +import { AES, Hex, TypedArray } from 'occulto' + +import { exit } from './utils.js' + +type UploadOptions = Pick + +export async function upload(key: TypedArray, note: Note) { + try { + const result = await create(note) + const password = Hex.encode(key) + const url = `${BASE}/note/${result.id}#${password}` + console.log(`Note created under:\n\n${url}`) + } catch { + exit('Could not create note') + } +} + +export async function uploadFiles(paths: string[], options: UploadOptions) { + const key = await AES.generateKey() + const files: FileDTO[] = await Promise.all( + paths.map(async (path) => { + const data = new Uint8Array(await readFile(path)) + const stats = await stat(path) + const extension = path.substring(path.indexOf('.') + 1) + const type = mime.getType(extension) ?? 'application/octet-stream' + return { + name: basename(path), + size: stats.size, + contents: new Blob([data]) as FileDTO['contents'], + type, + } + }) + ) + + const contents = await Adapters.Files.encrypt(files, key) + await upload(key, { ...options, contents, meta: { type: 'file' } }) +} + +export async function uploadText(text: string, options: UploadOptions) { + const key = await AES.generateKey() + const contents = await Adapters.Text.encrypt(text, key) + await upload(key, { ...options, contents, meta: { type: 'text' } }) +} diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts new file mode 100644 index 0000000..a52b4cb --- /dev/null +++ b/packages/cli/src/utils.ts @@ -0,0 +1,6 @@ +import process from 'node:process' + +export function exit(message: string) { + console.error(message) + process.exit(1) +} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 0000000..6bac29d --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,110 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "es2022" /* Specify what module code is generated. */, + // "rootDir": "./src" /* Specify the root folder within your source files. */, + "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./" /* Specify the base directory to resolve non-relative module names. */, + // "paths": { + // "@shared/*": ["../shared/*"], + // "@shared": ["../shared"] + // } /* Specify a set of entries that re-map imports to additional lookup locations. */, + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [ + // "../shared/types.ts", + // "node" + // ] /* Specify type package names to be included without being referenced in a source file. */, + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } + // "references": [{ "path": "../shared" }] +} diff --git a/packages/frontend/.npmrc b/packages/frontend/.npmrc deleted file mode 100644 index b6f27f1..0000000 --- a/packages/frontend/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 45fb968..114b094 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -1,5 +1,6 @@ { "private": true, + "name": "@cryptgeon/web", "scripts": { "dev": "vite dev", "build": "vite build", @@ -11,29 +12,30 @@ }, "type": "module", "devDependencies": { - "@lokalise/node-api": "^9.5.0", - "@sveltejs/adapter-static": "^1.0.2", - "@sveltejs/kit": "^1.0.13", + "@lokalise/node-api": "^9.8.0", + "@sveltejs/adapter-static": "^1.0.6", + "@sveltejs/kit": "^1.15.8", "@types/dompurify": "^2.4.0", "@types/file-saver": "^2.0.5", "@zerodevx/svelte-toast": "^0.7.2", "adm-zip": "^0.5.10", "dotenv": "^16.0.3", - "svelte": "^3.55.1", + "svelte": "^3.58.0", "svelte-check": "^2.10.3", "svelte-intl-precompile": "^0.10.1", "svelte-preprocess": "^4.10.7", - "tslib": "^2.4.1", - "typescript": "^4.9.4", - "vite": "^4.0.4" + "tslib": "^2.5.0", + "typescript": "^4.9.5", + "vite": "^4.3.1" }, "dependencies": { + "@cryptgeon/shared": "workspace:*", "@fontsource/fira-mono": "^4.5.10", "copy-to-clipboard": "^3.3.3", - "dompurify": "^2.4.3", + "dompurify": "^2.4.5", "file-saver": "^2.0.5", - "occulto": "2.0.0-rc.10", - "pretty-bytes": "^6.0.0", + "occulto": "^2.0.0", + "pretty-bytes": "^6.1.0", "qrious": "^4.0.2" } } diff --git a/packages/frontend/src/app.d.ts b/packages/frontend/src/app.d.ts new file mode 100644 index 0000000..26a9569 --- /dev/null +++ b/packages/frontend/src/app.d.ts @@ -0,0 +1,9 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +// and what to do when importing types +declare namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface Platform {} +} diff --git a/packages/frontend/src/global.d.ts b/packages/frontend/src/global.d.ts deleted file mode 100644 index 79d7d7f..0000000 --- a/packages/frontend/src/global.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -/// -/// -/// diff --git a/packages/frontend/src/lib/adapters.ts b/packages/frontend/src/lib/adapters.ts deleted file mode 100644 index 2bf965a..0000000 --- a/packages/frontend/src/lib/adapters.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { AES, Bytes, type TypedArray } from 'occulto' -import type { EncryptedFileDTO, FileDTO } from './api' - -abstract class CryptAdapter { - abstract encrypt(plaintext: T, key: TypedArray): Promise - abstract decrypt(ciphertext: string, key: TypedArray): Promise -} - -class CryptTextAdapter implements CryptAdapter { - async encrypt(plaintext: string, key: TypedArray) { - return await AES.encrypt(Bytes.encode(plaintext), key) - } - async decrypt(ciphertext: string, key: TypedArray) { - return Bytes.decode(await AES.decrypt(ciphertext, key)) - } -} - -class CryptBlobAdapter implements CryptAdapter { - async encrypt(plaintext: Blob, key: TypedArray) { - return await AES.encrypt(new Uint8Array(await plaintext.arrayBuffer()), key) - } - - async decrypt(ciphertext: string, key: TypedArray) { - const plaintext = await AES.decrypt(ciphertext, key) - return new Blob([plaintext], { type: 'application/octet-stream' }) - } -} - -class CryptFilesAdapter implements CryptAdapter { - async encrypt(plaintext: FileDTO[], key: TypedArray) { - const adapter = new CryptBlobAdapter() - const data: Promise[] = plaintext.map(async (file) => ({ - name: file.name, - size: file.size, - type: file.type, - contents: await adapter.encrypt(file.contents, key), - })) - return JSON.stringify(await Promise.all(data)) - } - - async decrypt(ciphertext: string, key: TypedArray) { - const adapter = new CryptBlobAdapter() - const data: EncryptedFileDTO[] = JSON.parse(ciphertext) - const files: FileDTO[] = await Promise.all( - data.map(async (file) => ({ - name: file.name, - size: file.size, - type: file.type, - contents: await adapter.decrypt(file.contents, key), - })) - ) - return files - } -} - -export const Adapters = { - Text: new CryptTextAdapter(), - Blob: new CryptBlobAdapter(), - Files: new CryptFilesAdapter(), -} diff --git a/packages/frontend/src/lib/api.ts b/packages/frontend/src/lib/api.ts deleted file mode 100644 index 793753d..0000000 --- a/packages/frontend/src/lib/api.ts +++ /dev/null @@ -1,78 +0,0 @@ -export type NoteMeta = { type: 'text' | 'file' } - -export type Note = { - contents: string - meta: NoteMeta - views?: number - expiration?: number -} -export type NoteInfo = {} -export type NotePublic = Pick -export type NoteCreate = Omit & { meta: string } - -export type FileDTO = Pick & { - contents: Blob -} - -export type EncryptedFileDTO = Omit & { - contents: string -} - -type CallOptions = { - url: string - method: string - body?: any -} - -export class PayloadToLargeError extends Error {} - -export async function call(options: CallOptions) { - const response = await fetch('/api/' + options.url, { - method: options.method, - body: options.body === undefined ? undefined : JSON.stringify(options.body), - mode: 'cors', - headers: { - 'Content-Type': 'application/json', - }, - }) - - if (!response.ok) { - if (response.status === 413) throw new PayloadToLargeError() - else throw new Error('API call failed') - } - return response.json() -} - -export async function create(note: Note) { - const { meta, ...rest } = note - const body: NoteCreate = { - ...rest, - meta: JSON.stringify(meta), - } - const data = await call({ - url: 'notes/', - method: 'post', - body, - }) - return data as { id: string } -} - -export async function get(id: string): Promise { - const data = await call({ - url: `notes/${id}`, - method: 'delete', - }) - const { contents, meta } = data - return { - contents, - meta: JSON.parse(meta) as NoteMeta, - } -} - -export async function info(id: string): Promise { - const data = await call({ - url: `notes/${id}`, - method: 'get', - }) - return data -} diff --git a/packages/frontend/src/lib/stores/status.ts b/packages/frontend/src/lib/stores/status.ts index 627bb3f..4cc347c 100644 --- a/packages/frontend/src/lib/stores/status.ts +++ b/packages/frontend/src/lib/stores/status.ts @@ -1,24 +1,8 @@ -import { call } from '$lib/api' +import { status as getStatus, type Status } from '@cryptgeon/shared' import { writable } from 'svelte/store' -export type Status = { - version: string - max_size: number - max_views: number - max_expiration: number - allow_advanced: boolean - theme_image: string - theme_text: string - theme_favicon: string - theme_page_title: string -} - export const status = writable(null) export async function init() { - const data = await call({ - url: 'status/', - method: 'get', - }) - status.set(data) + status.set(await getStatus()) } diff --git a/packages/frontend/src/lib/ui/AdvancedParameters.svelte b/packages/frontend/src/lib/ui/AdvancedParameters.svelte index 8db3701..442e38a 100644 --- a/packages/frontend/src/lib/ui/AdvancedParameters.svelte +++ b/packages/frontend/src/lib/ui/AdvancedParameters.svelte @@ -1,10 +1,10 @@