Release v1.0.2 - Fix Buffer polyfill race condition in prompt

- Fix race condition where permission prompts failed on first request
  due to Buffer polyfill not being initialized during module evaluation
- Replace Buffer.from() with native browser APIs (atob + TextDecoder)
  in prompt.ts for reliable base64 decoding
- Add debug logging to reckless mode approval checks
- Update permission encryption to support v2 vault key format
- Enhance LoggerService with warn/error/debug methods and log storage
- Add logs component for viewing extension activity
- Simplify deriving modal component
- Rename icon files from gooti to plebian-signer
- Update permissions component with improved styling

Files modified:
- projects/chrome/src/prompt.ts
- projects/firefox/src/prompt.ts
- projects/*/src/background-common.ts
- projects/common/src/lib/services/logger/logger.service.ts
- projects/*/src/app/components/home/logs/ (new)
- projects/*/public/*.svg, *.png (renamed)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-20 08:52:44 +01:00
parent 4b2d23e942
commit abd4a21f8f
36 changed files with 602 additions and 158 deletions

View File

@@ -3,8 +3,7 @@
<div class="deriving-modal">
<div class="deriving-spinner"></div>
<h3>{{ message }}</h3>
<div class="deriving-timer">{{ elapsed.toFixed(1) }}s</div>
<p class="deriving-note">This may take 3-6 seconds for security</p>
<p class="deriving-note">This may take a few seconds</p>
</div>
</div>
}

View File

@@ -30,14 +30,6 @@
}
}
.deriving-timer {
font-size: 2.5rem;
font-weight: bold;
color: #ff3eb5;
font-family: monospace;
margin: 0.5rem 0;
}
.deriving-note {
margin: 0.5rem 0 0;
color: #a1a1a1;

View File

@@ -1,23 +1,16 @@
import {
Component,
OnDestroy,
} from '@angular/core';
import { Component } from '@angular/core';
@Component({
selector: 'app-deriving-modal',
templateUrl: './deriving-modal.component.html',
styleUrl: './deriving-modal.component.scss',
})
export class DerivingModalComponent implements OnDestroy {
export class DerivingModalComponent {
visible = false;
elapsed = 0;
message = 'Deriving encryption key';
#startTime: number | null = null;
#animationFrame: number | null = null;
/**
* Show the deriving modal and start the timer
* Show the deriving modal
* @param message Optional custom message
*/
show(message?: string): void {
@@ -25,35 +18,12 @@ export class DerivingModalComponent implements OnDestroy {
this.message = message;
}
this.visible = true;
this.elapsed = 0;
this.#startTime = performance.now();
this.#updateTimer();
}
/**
* Hide the modal and stop the timer
* Hide the modal
*/
hide(): void {
this.visible = false;
this.#stopTimer();
}
ngOnDestroy(): void {
this.#stopTimer();
}
#updateTimer(): void {
if (this.#startTime !== null) {
this.elapsed = (performance.now() - this.#startTime) / 1000;
this.#animationFrame = requestAnimationFrame(() => this.#updateTimer());
}
}
#stopTimer(): void {
this.#startTime = null;
if (this.#animationFrame !== null) {
cancelAnimationFrame(this.#animationFrame);
this.#animationFrame = null;
}
}
}

View File

@@ -1,24 +1,76 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Injectable } from '@angular/core';
export interface LogEntry {
timestamp: Date;
level: 'log' | 'warn' | 'error' | 'debug';
message: string;
data?: any;
}
@Injectable({
providedIn: 'root',
})
export class LoggerService {
#namespace: string | undefined;
#logs: LogEntry[] = [];
#maxLogs = 500;
get logs(): LogEntry[] {
return this.#logs;
}
initialize(namespace: string): void {
this.#namespace = namespace;
}
log(value: any) {
log(value: any, data?: any) {
this.#assureInitialized();
this.#addLog('log', value, data);
const nowString = new Date().toLocaleString();
console.log(`[${this.#namespace} - ${nowString}]`, JSON.stringify(value));
}
warn(value: any, data?: any) {
this.#assureInitialized();
this.#addLog('warn', value, data);
const nowString = new Date().toLocaleString();
console.warn(`[${this.#namespace} - ${nowString}]`, JSON.stringify(value));
}
error(value: any, data?: any) {
this.#assureInitialized();
this.#addLog('error', value, data);
const nowString = new Date().toLocaleString();
console.error(`[${this.#namespace} - ${nowString}]`, JSON.stringify(value));
}
debug(value: any, data?: any) {
this.#assureInitialized();
this.#addLog('debug', value, data);
const nowString = new Date().toLocaleString();
console.debug(`[${this.#namespace} - ${nowString}]`, JSON.stringify(value));
}
clear() {
this.#logs = [];
}
#addLog(level: LogEntry['level'], message: any, data?: any) {
const entry: LogEntry = {
timestamp: new Date(),
level,
message: typeof message === 'string' ? message : JSON.stringify(message),
data,
};
this.#logs.unshift(entry);
// Limit stored logs
if (this.#logs.length > this.#maxLogs) {
this.#logs.pop();
}
}
#assureInitialized() {
if (!this.#namespace) {
throw new Error(

View File

@@ -146,12 +146,17 @@ export const decryptPermissions = async function (
const decryptedPermissions: Permission_DECRYPTED[] = [];
for (const permission of permissions) {
const decryptedPermission = await decryptPermission.call(
this,
permission,
withLockedVault
);
decryptedPermissions.push(decryptedPermission);
try {
const decryptedPermission = await decryptPermission.call(
this,
permission,
withLockedVault
);
decryptedPermissions.push(decryptedPermission);
} catch (error) {
// Skip corrupted permissions (e.g., encrypted with wrong key)
console.warn('[vault] Skipping corrupted permission:', error);
}
}
return decryptedPermissions;