/
nasya
/
SafeDrop
Обзор
Документация
Войти
/
nasya
/
SafeDrop
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
apps/web/src/lib/api/vault-api.ts
225 строк
6 KB
nasya
Initial commit
24 май 2026, 16:12
24 май 2026, 16:12
16bc032
Код
Авторство
О чём код?
import type { PublicKeyRecord, RotateKeyRequest, SaveVaultRequest } from "@safedrop/shared"; import type { EncryptedVault } from "@safedrop/shared"; export type VaultRecord = { encryptedVault: EncryptedVault; identityId: string; publicKeys: PublicKeyRecord[]; }; export type VaultLookup = | { status: "ready"; vault: VaultRecord; } | { status: "missing"; }; export class VaultAuthError extends Error { constructor() { super("Требуется вход в аккаунт."); } } function authHeaders(accessToken: string) { return { authorization: `Bearer ${accessToken}`, }; } export async function getVault( apiUrl: string, accessToken: string, fetchImpl: typeof fetch = fetch, ): Promise<VaultLookup> { const response = await fetchImpl(`${apiUrl}/api/keys/vault`, { headers: authHeaders(accessToken), }); if (response.status === 404) { return { status: "missing" }; } if (response.status === 401) { throw new VaultAuthError(); } if (!response.ok) { throw new Error(`Не удалось загрузить ключи: ${response.status}`); } return { status: "ready", vault: (await response.json()) as VaultRecord, }; } export async function saveVault( apiUrl: string, accessToken: string, request: SaveVaultRequest, fetchImpl: typeof fetch = fetch, ): Promise<VaultRecord> { const response = await fetchImpl(`${apiUrl}/api/keys/vault`, { body: JSON.stringify(request), headers: { ...authHeaders(accessToken), "content-type": "application/json", }, method: "PUT", }); if (response.status === 401) { throw new VaultAuthError(); } if (!response.ok) { throw new Error(`Не удалось сохранить ключи: ${response.status}`); } return (await response.json()) as VaultRecord; } export async function rotateVault( apiUrl: string, accessToken: string, request: RotateKeyRequest, fetchImpl: typeof fetch = fetch, ): Promise<VaultRecord> { const response = await fetchImpl(`${apiUrl}/api/keys/rotate`, { body: JSON.stringify(request), headers: { ...authHeaders(accessToken), "content-type": "application/json", }, method: "POST", }); if (response.status === 401) { throw new VaultAuthError(); } if (!response.ok) { throw new Error(`Не удалось создать новую версию ключей: ${response.status}`); } return (await response.json()) as VaultRecord; } if (import.meta.rstest) { const { describe, expect, it } = import.meta.rstest; describe("vault API client", () => { it("returns missing state for 404 vault responses", async () => { const fetchImpl = (async () => new Response(JSON.stringify({ error: "vault_not_found" }), { status: 404 })) as typeof fetch; const vault = await getVault("http://api.local", "token-1", fetchImpl); expect(vault.status).toBe("missing"); }); it("sends bearer token and DTO payload when saving vault", async () => { const request = { encryptedVault: { encryptedMasterKey: { alg: "AES-GCM", ciphertextB64: "0123456789abcdef", ivB64: "0123456789ab" }, encryptedPrivateKeys: [ { alg: "RSA-OAEP-256", ciphertextB64: "0123456789abcdef", ivB64: "0123456789ab", purpose: "encryption", version: 1, }, ], kdf: { iterations: 1, memoryKiB: 1, name: "argon2id", parallelism: 1, saltB64: "0123456789abcdef", }, vaultVersion: 1, }, publicKeys: [ { createdAt: "2026-05-21T00:00:00.000Z", encryptionPublicKeyJwk: { kty: "RSA" }, signingPublicKeyJwk: { kty: "RSA" }, status: "active", version: 1, }, ], } satisfies SaveVaultRequest; const fetchImpl = (async (_url: string | URL | Request, init?: RequestInit) => { expect(init?.method).toBe("PUT"); expect((init?.headers as Record<string, string>).authorization).toBe("Bearer token-1"); expect(JSON.parse(init?.body as string)).toEqual(request); return new Response(JSON.stringify({ identityId: "alice", ...request }), { headers: { "content-type": "application/json" }, }); }) as typeof fetch; const saved = await saveVault("http://api.local", "token-1", request, fetchImpl); expect(saved.identityId).toBe("alice"); }); it("maps 401 responses to auth errors", async () => { const fetchImpl = (async () => new Response(JSON.stringify({ error: "unauthorized" }), { status: 401 })) as typeof fetch; await expect(getVault("http://api.local", "bad-token", fetchImpl)).rejects.toBeInstanceOf(VaultAuthError); }); it("sends bearer token and DTO payload when rotating vault", async () => { const request = { encryptedVault: { encryptedMasterKey: { alg: "AES-GCM", ciphertextB64: "0123456789abcdef", ivB64: "0123456789ab" }, encryptedPrivateKeys: [ { alg: "RSA-OAEP-256", ciphertextB64: "0123456789abcdef", ivB64: "0123456789ab", purpose: "encryption", version: 1, }, ], kdf: { iterations: 1, memoryKiB: 1, name: "argon2id", parallelism: 1, saltB64: "0123456789abcdef", }, vaultVersion: 1, }, newActivePublicKey: { createdAt: "2026-05-21T00:00:00.000Z", encryptionPublicKeyJwk: { kty: "RSA" }, signingPublicKeyJwk: { kty: "RSA" }, status: "active", version: 2, }, } satisfies RotateKeyRequest; const fetchImpl = (async (_url: string | URL | Request, init?: RequestInit) => { expect(init?.method).toBe("POST"); expect((init?.headers as Record<string, string>).authorization).toBe("Bearer token-1"); expect(JSON.parse(init?.body as string)).toEqual(request); return new Response( JSON.stringify({ encryptedVault: request.encryptedVault, identityId: "alice", publicKeys: [{ ...request.newActivePublicKey }], }), { headers: { "content-type": "application/json" } }, ); }) as typeof fetch; const rotated = await rotateVault("http://api.local", "token-1", request, fetchImpl); expect(rotated.publicKeys[0].version).toBe(2); }); }); }