feat(extension): update devices.ts for revoked.json + deploy keys
- Add createDeployKey/deleteDeployKey to GiteaHost - Add RevokedEntry interface and readRevoked() to devices.ts - Update revokeDevice() to write revoked.json alongside devices.json - Update router to use new register_device WASM API (private keys internal) - Pass revokedBy device name when revoking Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,22 @@
|
||||
/// Device management — reads/writes .relicario/devices.json
|
||||
/// Device management — reads/writes .relicario/devices.json and revoked.json
|
||||
|
||||
import type { GitHost } from './git-host';
|
||||
import type { Device } from '../shared/types';
|
||||
|
||||
const DEVICES_PATH = '.relicario/devices.json';
|
||||
const REVOKED_PATH = '.relicario/revoked.json';
|
||||
|
||||
interface DevicesFile {
|
||||
devices: Device[];
|
||||
}
|
||||
|
||||
export interface RevokedEntry {
|
||||
name: string;
|
||||
public_key: string;
|
||||
revoked_at: number; // unix timestamp
|
||||
revoked_by: string; // name of device that performed the revocation
|
||||
}
|
||||
|
||||
export async function readDevices(gitHost: GitHost): Promise<Device[]> {
|
||||
try {
|
||||
const raw = await gitHost.readFile(DEVICES_PATH);
|
||||
@@ -30,6 +38,25 @@ export async function writeDevices(
|
||||
await gitHost.writeFile(DEVICES_PATH, bytes, message);
|
||||
}
|
||||
|
||||
export async function readRevoked(gitHost: GitHost): Promise<RevokedEntry[]> {
|
||||
try {
|
||||
const raw = await gitHost.readFile(REVOKED_PATH);
|
||||
const text = new TextDecoder().decode(raw);
|
||||
return JSON.parse(text) as RevokedEntry[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function writeRevoked(
|
||||
gitHost: GitHost,
|
||||
revoked: RevokedEntry[],
|
||||
message: string,
|
||||
): Promise<void> {
|
||||
const bytes = new TextEncoder().encode(JSON.stringify(revoked, null, 2));
|
||||
await gitHost.writeFile(REVOKED_PATH, bytes, message);
|
||||
}
|
||||
|
||||
export async function addDevice(
|
||||
gitHost: GitHost,
|
||||
device: Device,
|
||||
@@ -45,11 +72,25 @@ export async function addDevice(
|
||||
export async function revokeDevice(
|
||||
gitHost: GitHost,
|
||||
name: string,
|
||||
revokedBy?: string,
|
||||
): Promise<void> {
|
||||
const existing = await readDevices(gitHost);
|
||||
const filtered = existing.filter((d) => d.name !== name);
|
||||
if (filtered.length === existing.length) {
|
||||
const device = existing.find((d) => d.name === name);
|
||||
if (!device) {
|
||||
throw new Error(`device '${name}' not found`);
|
||||
}
|
||||
|
||||
// Remove from devices.json
|
||||
const filtered = existing.filter((d) => d.name !== name);
|
||||
await writeDevices(gitHost, filtered, `device: revoke ${name}`);
|
||||
|
||||
// Add to revoked.json
|
||||
const revoked = await readRevoked(gitHost);
|
||||
revoked.push({
|
||||
name,
|
||||
public_key: device.public_key,
|
||||
revoked_at: Math.floor(Date.now() / 1000),
|
||||
revoked_by: revokedBy ?? 'unknown',
|
||||
});
|
||||
await writeRevoked(gitHost, revoked, `device: revoke ${name} (revoked log)`);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export class GiteaHost implements GitHost {
|
||||
private baseUrl: string;
|
||||
private gitApiBase: string;
|
||||
private commitsUrl: string;
|
||||
private keysUrl: string;
|
||||
private branch: string = 'main';
|
||||
private headers: Record<string, string>;
|
||||
|
||||
@@ -27,6 +28,7 @@ export class GiteaHost implements GitHost {
|
||||
this.baseUrl = `${apiUrl}/repos/${repoPath}/contents`;
|
||||
this.gitApiBase = `${apiUrl}/repos/${repoPath}/git`;
|
||||
this.commitsUrl = `${apiUrl}/repos/${repoPath}/commits`;
|
||||
this.keysUrl = `${apiUrl}/repos/${repoPath}/keys`;
|
||||
this.headers = {
|
||||
'Authorization': `token ${apiToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
@@ -244,4 +246,31 @@ export class GiteaHost implements GitHost {
|
||||
async deleteBlob(path: string, message: string): Promise<void> {
|
||||
return this.deleteFile(path, message);
|
||||
}
|
||||
|
||||
/// Create a deploy key for this repo, returning its numeric ID.
|
||||
async createDeployKey(title: string, publicKey: string): Promise<number> {
|
||||
const resp = await fetch(this.keysUrl, {
|
||||
method: 'POST',
|
||||
headers: this.headers,
|
||||
body: JSON.stringify({ title, key: publicKey, read_only: false }),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const text = await resp.text();
|
||||
throw new Error(`createDeployKey: ${resp.status} ${text}`);
|
||||
}
|
||||
const json = await resp.json() as { id: number };
|
||||
return json.id;
|
||||
}
|
||||
|
||||
/// Delete a deploy key by numeric ID. Ignores 404 (already gone).
|
||||
async deleteDeployKey(keyId: number): Promise<void> {
|
||||
const resp = await fetch(`${this.keysUrl}/${keyId}`, {
|
||||
method: 'DELETE',
|
||||
headers: this.headers,
|
||||
});
|
||||
if (!resp.ok && resp.status !== 404) {
|
||||
const text = await resp.text();
|
||||
throw new Error(`deleteDeployKey: ${resp.status} ${text}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,17 +359,15 @@ export async function handle(
|
||||
|
||||
case 'register_this_device': {
|
||||
if (!state.gitHost) return { ok: false, error: 'vault_locked' };
|
||||
const keypair = state.wasm.generate_device_keypair() as {
|
||||
public_key_hex: string;
|
||||
private_key_base64: string;
|
||||
// register_device keeps private keys internal — only public keys cross to JS
|
||||
const keys = state.wasm.register_device(msg.name) as {
|
||||
signing_public_key: string;
|
||||
deploy_public_key: string;
|
||||
};
|
||||
await chrome.storage.local.set({
|
||||
device_name: msg.name,
|
||||
device_private_key: keypair.private_key_base64,
|
||||
});
|
||||
await chrome.storage.local.set({ device_name: msg.name });
|
||||
await devices.addDevice(state.gitHost, {
|
||||
name: msg.name,
|
||||
public_key: keypair.public_key_hex,
|
||||
public_key: keys.signing_public_key,
|
||||
added_at: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
return { ok: true };
|
||||
@@ -377,7 +375,9 @@ export async function handle(
|
||||
|
||||
case 'revoke_device': {
|
||||
if (!state.gitHost) return { ok: false, error: 'vault_locked' };
|
||||
await devices.revokeDevice(state.gitHost, msg.name);
|
||||
const stored = await chrome.storage.local.get(['device_name']);
|
||||
const revokedBy = stored.device_name as string | undefined;
|
||||
await devices.revokeDevice(state.gitHost, msg.name, revokedBy);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user