feat(pwa): add base
All checks were successful
/ build-web (push) Successful in 5m35s
/ build-desktop (linux) (push) Successful in 9m41s
/ build-mobile (push) Successful in 20m12s
/ release (push) Successful in 1m51s
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped

This commit is contained in:
Alois 2026-08-18 18:44:45 +02:00
commit 7b36218ffa
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
40 changed files with 4014 additions and 922 deletions

View file

@ -6,7 +6,9 @@
"exports": {
"./session": "./src/session.tsx",
"./context": "./src/context.tsx",
"./secure": "./src/secure.ts"
"./secure": "./src/secure.ts",
"./browserSecure": "./src/browserSecure.ts",
"./credentials": "./src/credentials.ts"
},
"scripts": {
"format": "pnpm exec prettier --write .",

View file

@ -0,0 +1,40 @@
import { getDatabaseEntry } from "@tensamin/shared/indexedDb";
type SecureEnvelope = {
__tensaminSecure: 1;
version: 1;
iv: string;
data: string;
};
function base64ToBytes(value: string) {
const binary = atob(value);
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
}
function isSecureEnvelope(value: unknown): value is SecureEnvelope {
if (!value || typeof value !== "object") return false;
const envelope = value as Partial<SecureEnvelope>;
return (
envelope.__tensaminSecure === 1 &&
envelope.version === 1 &&
typeof envelope.iv === "string" &&
typeof envelope.data === "string"
);
}
export async function loadSecureBrowserValue<T>(key: string) {
const stored = await getDatabaseEntry<unknown>("storage", key);
if (stored === undefined || !isSecureEnvelope(stored)) {
return stored as T | undefined;
}
const masterKey = await getDatabaseEntry<CryptoKey>("keys", "master-v1");
if (!masterKey) throw new Error("Secure storage key is unavailable.");
const plaintext = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: base64ToBytes(stored.iv) },
masterKey,
base64ToBytes(stored.data),
);
return JSON.parse(new TextDecoder().decode(plaintext)) as T;
}

View file

@ -30,7 +30,7 @@ import {
export type SaveOptions = { secure?: boolean };
interface StorageContextValue {
export interface StorageContextValue {
load<K extends keyof StorageSchema>(key: K): Promise<StorageSchema[K]>;
save<K extends keyof StorageSchema>(
key: K,

View file

@ -0,0 +1,68 @@
import { invoke, isTauri } from "@tauri-apps/api/core";
import type { StorageContextValue } from "./context";
export function parseTuFileContent(rawFileContent: string): {
userId: number;
privateKey: string;
domain: string | null;
} {
const content = rawFileContent.trim();
const separator = content.indexOf("::");
if (separator <= 0 || separator !== content.lastIndexOf("::")) {
throw new Error("Invalid file");
}
const identity = content.slice(0, separator);
const privateKey = content.slice(separator + 2).trim();
const [userIdValue, domain, ...extraDomainParts] = identity.split("@");
const userId = Number(userIdValue);
if (
!Number.isSafeInteger(userId) ||
userId <= 0 ||
!privateKey ||
extraDomainParts.length > 0 ||
(identity.includes("@") && !domain)
) {
throw new Error("Invalid file");
}
return { userId, privateKey, domain: domain ?? null };
}
export async function persistMtpCredentials({
storage,
userId,
keyring,
domain,
}: {
storage: Pick<StorageContextValue, "load" | "save">;
userId: number;
keyring: string;
domain?: string | null;
}) {
const omegaUrl = domain
? `https://${domain}/`
: await storage.load("omega_url");
if (domain) await storage.save("omega_url", omegaUrl);
if (isTauri()) {
const [forcedOmikronUrl, forcedOmikronPublicKey] = await Promise.all([
storage.load("forced_omikron_url"),
storage.load("forced_omikron_public_key"),
]);
await invoke("mtp_store_credentials", {
config: {
userId,
keyring,
omegaUrl,
forcedOmikronUrl,
forcedOmikronPublicKey,
},
});
}
await storage.save("mtp_keyring", keyring, { secure: true });
await storage.save("session_id", Date.now());
await storage.save("user_id", userId);
}

View file

@ -4,10 +4,7 @@ import { getDatabaseEntry, setDatabaseEntry } from "@tensamin/shared/indexedDb";
export type SecureStorageStatus = {
backend:
| "electron-keyring"
| "application-storage"
| "webcrypto"
| "indexeddb";
"electron-keyring" | "application-storage" | "webcrypto" | "indexeddb";
secure: boolean;
reason?: string;
};
@ -71,7 +68,12 @@ async function getKey() {
if (!globalThis.crypto?.subtle || typeof indexedDB === "undefined") {
return null;
}
if (window.tensaminDesktop?.secureStorage) return loadElectronKey();
if (
typeof window !== "undefined" &&
window.tensaminDesktop?.secureStorage
) {
return loadElectronKey();
}
return loadBrowserKey();
})().catch(() => null);
return keyPromise;
@ -120,7 +122,10 @@ export async function decodeSecureValue(value: unknown): Promise<unknown> {
}
export async function getSecureStorageStatus(): Promise<SecureStorageStatus> {
const desktop = window.tensaminDesktop?.secureStorage;
const desktop =
typeof window === "undefined"
? undefined
: window.tensaminDesktop?.secureStorage;
if (desktop?.getStatus) {
const status = await desktop.getStatus();
if (status.available) return { backend: "electron-keyring", secure: true };