This commit is contained in:
2026-07-12 11:36:01 +02:00
parent 1f180a2e53
commit 062054f450
46 changed files with 976 additions and 1691 deletions
+58
View File
@@ -0,0 +1,58 @@
import { encode, decode } from "@msgpack/msgpack";
import type { ServerNote } from "./types.js";
let server = "";
export function setServer(url: string) {
server = url.replace(/\/+$/, "");
}
export function getServer() {
return server;
}
function api(path: string) {
return `${server}/api/v3/${path}`;
}
export async function create(note: ServerNote): Promise<{ id: string }> {
const res = await fetch(api("notes"), {
method: "POST",
headers: { "content-type": "application/msgpack" },
body: encode(note),
});
if (!res.ok) throw new Error("create failed");
const buf = await res.arrayBuffer();
const data = decode(new Uint8Array(buf)) as any;
if (typeof data?.id !== "string") throw new Error("invalid response");
return { id: data.id };
}
export async function info(id: string): Promise<ServerNote["meta"] | null> {
const res = await fetch(api(`notes/${id}`));
if (!res.ok) return null;
const buf = await res.arrayBuffer();
const data = decode(new Uint8Array(buf)) as any;
const meta = data?.meta as ServerNote["meta"] | undefined;
if (!meta) return null;
if (meta.extra && !(meta.extra instanceof Uint8Array)) meta.extra = new Uint8Array(meta.extra as any);
return meta;
}
export async function get(id: string): Promise<ServerNote | null> {
const res = await fetch(api(`notes/${id}`), { method: "DELETE" });
if (!res.ok) return null;
const buf = await res.arrayBuffer();
const data = decode(new Uint8Array(buf)) as any;
const meta = data.meta as ServerNote["meta"];
if (meta?.extra && !(meta.extra instanceof Uint8Array)) meta.extra = new Uint8Array(meta.extra as any);
const d = data.data;
return { meta, data: d instanceof Uint8Array ? d : new Uint8Array(d) } satisfies ServerNote;
}
export async function status(): Promise<Record<string, unknown>> {
const res = await fetch(api("status"));
if (!res.ok) throw new Error("status failed");
return res.json();
}
+26
View File
@@ -0,0 +1,26 @@
import { describe, expect, it } from "vitest";
import { deriveKey, encrypt, decrypt, generateKey, utf8ToBytes, randomBytes } from "./crypto";
describe("crypto", () => {
it("encrypts and decrypts with generated key", () => {
const data = utf8ToBytes("hello world");
const key = generateKey();
const enc = encrypt(data, key);
const dec = decrypt(enc, key);
expect(dec).toEqual(data);
});
it("encrypts and decrypts with derived key", () => {
const data = utf8ToBytes("secret message");
const salt = randomBytes(16);
const key = deriveKey("password123", salt);
const enc = encrypt(data, key);
const dec = decrypt(enc, key);
expect(dec).toEqual(data);
});
it("derived key has same length as generated", () => {
const salt = randomBytes(16);
expect(deriveKey("test", salt).length).toBe(generateKey().length);
});
});
+41
View File
@@ -0,0 +1,41 @@
import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
import { managedNonce, randomBytes, utf8ToBytes } from "@noble/ciphers/utils.js";
import { scrypt } from "@noble/hashes/scrypt.js";
export { bytesToUtf8, utf8ToBytes } from "@noble/ciphers/utils.js";
export { randomBytes } from "@noble/ciphers/utils.js";
const N = 2 ** 15;
const KEY_SIZE = 32;
export function generateKey(): Uint8Array {
return randomBytes(KEY_SIZE);
}
export function deriveKey(password: string, salt: Uint8Array): Uint8Array {
return scrypt(password, salt, { N, r: 8, p: 1, dkLen: KEY_SIZE });
}
export function encrypt(data: Uint8Array, key: Uint8Array): Uint8Array {
const chacha = managedNonce(xchacha20poly1305)(key);
return chacha.encrypt(data);
}
export function decrypt(data: Uint8Array, key: Uint8Array): Uint8Array {
const chacha = managedNonce(xchacha20poly1305)(key);
return chacha.decrypt(data);
}
export function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
export function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
}
return bytes;
}
+4
View File
@@ -0,0 +1,4 @@
export * from "./crypto.js";
export * from "./types.js";
export * from "./api.js";
export { encode, decode } from "@msgpack/msgpack";
+21
View File
@@ -0,0 +1,21 @@
export type NoteMeta = {
expiration?: number;
views?: number;
extra?: Uint8Array;
};
export type ServerNote = {
meta: NoteMeta;
data: Uint8Array;
};
export type NoteContent =
| { type: "text"; data: string }
| { type: "files"; data: FileDTO[] };
export type FileDTO = {
name: string;
mime: string;
size: number;
data: Uint8Array;
};