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 { GitHost } from './git-host';
|
||||||
import type { Device } from '../shared/types';
|
import type { Device } from '../shared/types';
|
||||||
|
|
||||||
const DEVICES_PATH = '.relicario/devices.json';
|
const DEVICES_PATH = '.relicario/devices.json';
|
||||||
|
const REVOKED_PATH = '.relicario/revoked.json';
|
||||||
|
|
||||||
interface DevicesFile {
|
interface DevicesFile {
|
||||||
devices: Device[];
|
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[]> {
|
export async function readDevices(gitHost: GitHost): Promise<Device[]> {
|
||||||
try {
|
try {
|
||||||
const raw = await gitHost.readFile(DEVICES_PATH);
|
const raw = await gitHost.readFile(DEVICES_PATH);
|
||||||
@@ -30,6 +38,25 @@ export async function writeDevices(
|
|||||||
await gitHost.writeFile(DEVICES_PATH, bytes, message);
|
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(
|
export async function addDevice(
|
||||||
gitHost: GitHost,
|
gitHost: GitHost,
|
||||||
device: Device,
|
device: Device,
|
||||||
@@ -45,11 +72,25 @@ export async function addDevice(
|
|||||||
export async function revokeDevice(
|
export async function revokeDevice(
|
||||||
gitHost: GitHost,
|
gitHost: GitHost,
|
||||||
name: string,
|
name: string,
|
||||||
|
revokedBy?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const existing = await readDevices(gitHost);
|
const existing = await readDevices(gitHost);
|
||||||
const filtered = existing.filter((d) => d.name !== name);
|
const device = existing.find((d) => d.name === name);
|
||||||
if (filtered.length === existing.length) {
|
if (!device) {
|
||||||
throw new Error(`device '${name}' not found`);
|
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}`);
|
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 baseUrl: string;
|
||||||
private gitApiBase: string;
|
private gitApiBase: string;
|
||||||
private commitsUrl: string;
|
private commitsUrl: string;
|
||||||
|
private keysUrl: string;
|
||||||
private branch: string = 'main';
|
private branch: string = 'main';
|
||||||
private headers: Record<string, string>;
|
private headers: Record<string, string>;
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ export class GiteaHost implements GitHost {
|
|||||||
this.baseUrl = `${apiUrl}/repos/${repoPath}/contents`;
|
this.baseUrl = `${apiUrl}/repos/${repoPath}/contents`;
|
||||||
this.gitApiBase = `${apiUrl}/repos/${repoPath}/git`;
|
this.gitApiBase = `${apiUrl}/repos/${repoPath}/git`;
|
||||||
this.commitsUrl = `${apiUrl}/repos/${repoPath}/commits`;
|
this.commitsUrl = `${apiUrl}/repos/${repoPath}/commits`;
|
||||||
|
this.keysUrl = `${apiUrl}/repos/${repoPath}/keys`;
|
||||||
this.headers = {
|
this.headers = {
|
||||||
'Authorization': `token ${apiToken}`,
|
'Authorization': `token ${apiToken}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -244,4 +246,31 @@ export class GiteaHost implements GitHost {
|
|||||||
async deleteBlob(path: string, message: string): Promise<void> {
|
async deleteBlob(path: string, message: string): Promise<void> {
|
||||||
return this.deleteFile(path, message);
|
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': {
|
case 'register_this_device': {
|
||||||
if (!state.gitHost) return { ok: false, error: 'vault_locked' };
|
if (!state.gitHost) return { ok: false, error: 'vault_locked' };
|
||||||
const keypair = state.wasm.generate_device_keypair() as {
|
// register_device keeps private keys internal — only public keys cross to JS
|
||||||
public_key_hex: string;
|
const keys = state.wasm.register_device(msg.name) as {
|
||||||
private_key_base64: string;
|
signing_public_key: string;
|
||||||
|
deploy_public_key: string;
|
||||||
};
|
};
|
||||||
await chrome.storage.local.set({
|
await chrome.storage.local.set({ device_name: msg.name });
|
||||||
device_name: msg.name,
|
|
||||||
device_private_key: keypair.private_key_base64,
|
|
||||||
});
|
|
||||||
await devices.addDevice(state.gitHost, {
|
await devices.addDevice(state.gitHost, {
|
||||||
name: msg.name,
|
name: msg.name,
|
||||||
public_key: keypair.public_key_hex,
|
public_key: keys.signing_public_key,
|
||||||
added_at: Math.floor(Date.now() / 1000),
|
added_at: Math.floor(Date.now() / 1000),
|
||||||
});
|
});
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
@@ -377,7 +375,9 @@ export async function handle(
|
|||||||
|
|
||||||
case 'revoke_device': {
|
case 'revoke_device': {
|
||||||
if (!state.gitHost) return { ok: false, error: 'vault_locked' };
|
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 };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user