Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
ddb74c61b2
|
@@ -1,6 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
version=$( cat package.json | jq '.custom.chrome.version' | tr -d '"')
|
||||
# Extract version and strip 'v' prefix if present (manifest requires bare semver)
|
||||
version=$( cat package.json | jq -r '.custom.chrome.version' | sed 's/^v//')
|
||||
|
||||
jq '.version = $newVersion' --arg newVersion $version ./projects/chrome/public/manifest.json > ./projects/chrome/public/tmp.manifest.json && mv ./projects/chrome/public/tmp.manifest.json ./projects/chrome/public/manifest.json
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
version=$( cat package.json | jq '.custom.firefox.version' | tr -d '"')
|
||||
# Extract version and strip 'v' prefix if present (manifest requires bare semver)
|
||||
version=$( cat package.json | jq -r '.custom.firefox.version' | sed 's/^v//')
|
||||
|
||||
jq '.version = $newVersion' --arg newVersion $version ./projects/firefox/public/manifest.json > ./projects/firefox/public/tmp.manifest.json && mv ./projects/firefox/public/tmp.manifest.json ./projects/firefox/public/manifest.json
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "plebeian-signer",
|
||||
"version": "v0.0.8",
|
||||
"version": "v0.0.9",
|
||||
"custom": {
|
||||
"chrome": {
|
||||
"version": "v0.0.8"
|
||||
"version": "v0.0.9"
|
||||
},
|
||||
"firefox": {
|
||||
"version": "v0.0.8"
|
||||
"version": "v0.0.9"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "Plebeian Signer - Nostr Identity Manager & Signer",
|
||||
"description": "Manage and switch between multiple identities while interacting with Nostr apps",
|
||||
"version": "v0.0.8",
|
||||
"version": "0.0.9",
|
||||
"homepage_url": "https://git.mleku.dev/mleku/plebeian-signer",
|
||||
"options_page": "options.html",
|
||||
"permissions": [
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="#ffffff" class="bi bi-person-fill" viewBox="0 0 16 16">
|
||||
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6"/>
|
||||
</svg>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 4C11.606 4 11.2159 4.0776 10.8519 4.22836C10.488 4.37913 10.1573 4.6001 9.87868 4.87868C9.6001 5.15726 9.37913 5.48797 9.22836 5.85195C9.0776 6.21593 9 6.60603 9 7C9 7.39397 9.0776 7.78407 9.22836 8.14805C9.37913 8.51203 9.6001 8.84274 9.87868 9.12132C10.1573 9.3999 10.488 9.62087 10.8519 9.77164C11.2159 9.9224 11.606 10 12 10C12.7956 10 13.5587 9.68393 14.1213 9.12132C14.6839 8.55871 15 7.79565 15 7C15 6.20435 14.6839 5.44129 14.1213 4.87868C13.5587 4.31607 12.7956 4 12 4ZM7 7C7 5.67392 7.52678 4.40215 8.46447 3.46447C9.40215 2.52678 10.6739 2 12 2C13.3261 2 14.5979 2.52678 15.5355 3.46447C16.4732 4.40215 17 5.67392 17 7C17 8.32608 16.4732 9.59785 15.5355 10.5355C14.5979 11.4732 13.3261 12 12 12C10.6739 12 9.40215 11.4732 8.46447 10.5355C7.52678 9.59785 7 8.32608 7 7ZM3.5 19C3.5 17.6739 4.02678 16.4021 4.96447 15.4645C5.90215 14.5268 7.17392 14 8.5 14H15.5C16.1566 14 16.8068 14.1293 17.4134 14.3806C18.02 14.6319 18.5712 15.0002 19.0355 15.4645C19.4998 15.9288 19.8681 16.48 20.1194 17.0866C20.3707 17.6932 20.5 18.3434 20.5 19V21H18.5V19C18.5 18.2044 18.1839 17.4413 17.6213 16.8787C17.0587 16.3161 16.2956 16 15.5 16H8.5C7.70435 16 6.94129 16.3161 6.37868 16.8787C5.81607 17.4413 5.5 18.2044 5.5 19V21H3.5V19Z" fill="#FAFAFA"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 219 B After Width: | Height: | Size: 1.3 KiB |
@@ -16,6 +16,7 @@ import { KeysComponent as EditIdentityKeysComponent } from './components/edit-id
|
||||
import { PermissionsComponent as EditIdentityPermissionsComponent } from './components/edit-identity/permissions/permissions.component';
|
||||
import { RelaysComponent as EditIdentityRelaysComponent } from './components/edit-identity/relays/relays.component';
|
||||
import { VaultImportComponent } from './components/vault-import/vault-import.component';
|
||||
import { WhitelistedAppsComponent } from './components/whitelisted-apps/whitelisted-apps.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -70,6 +71,10 @@ export const routes: Routes = [
|
||||
path: 'new-identity',
|
||||
component: NewIdentityComponent,
|
||||
},
|
||||
{
|
||||
path: 'whitelisted-apps',
|
||||
component: WhitelistedAppsComponent,
|
||||
},
|
||||
{
|
||||
path: 'edit-identity/:id',
|
||||
component: EditIdentityComponent,
|
||||
|
||||
@@ -11,6 +11,30 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="reckless-mode-row">
|
||||
<label class="reckless-label" (click)="onToggleRecklessMode()">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="isRecklessMode"
|
||||
(click)="$event.stopPropagation()"
|
||||
(change)="onToggleRecklessMode()"
|
||||
/>
|
||||
<span
|
||||
class="reckless-text"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
title="Auto-approve all actions. If whitelist has entries, only those apps are auto-approved."
|
||||
>Reckless mode</span>
|
||||
</label>
|
||||
<button
|
||||
class="gear-btn"
|
||||
title="Manage whitelisted apps"
|
||||
(click)="onClickWhitelistedApps()"
|
||||
>
|
||||
<i class="bi bi-gear"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@let sessionData = storage.getBrowserSessionHandler().browserSessionData;
|
||||
@let identities = sessionData?.identities ?? [];
|
||||
|
||||
@@ -34,7 +58,7 @@
|
||||
class="avatar"
|
||||
[src]="getAvatarUrl(identity)"
|
||||
alt=""
|
||||
(error)="$any($event.target).src = 'assets/person-fill.svg'"
|
||||
(error)="$any($event.target).src = 'person-fill.svg'"
|
||||
/>
|
||||
<span class="name">{{ getDisplayName(identity) }}</span>
|
||||
<lib-icon-button
|
||||
|
||||
@@ -28,13 +28,66 @@
|
||||
grid-column-end: 2;
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 2;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
font-family: var(--font-heading);
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1rem;
|
||||
justify-self: center;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.reckless-mode-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 12px;
|
||||
background: var(--background-light);
|
||||
border-radius: 8px;
|
||||
|
||||
.reckless-label {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reckless-text {
|
||||
font-size: 14px;
|
||||
color: var(--foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.gear-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--muted-foreground);
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: color 0.15s ease, background-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--foreground);
|
||||
background: var(--background-light-hover);
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
||||
@@ -24,6 +24,10 @@ export class IdentitiesComponent implements OnInit {
|
||||
// Cache of pubkey -> profile for quick lookup
|
||||
#profileCache = new Map<string, ProfileMetadata | null>();
|
||||
|
||||
get isRecklessMode(): boolean {
|
||||
return this.storage.getSignerMetaHandler().signerMetaData?.recklessMode ?? false;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.#profileMetadata.initialize();
|
||||
this.#loadProfiles();
|
||||
@@ -40,7 +44,7 @@ export class IdentitiesComponent implements OnInit {
|
||||
|
||||
getAvatarUrl(identity: Identity_DECRYPTED): string {
|
||||
const profile = this.#profileCache.get(identity.id);
|
||||
return profile?.picture || 'assets/person-fill.svg';
|
||||
return profile?.picture || 'person-fill.svg';
|
||||
}
|
||||
|
||||
getDisplayName(identity: Identity_DECRYPTED): string {
|
||||
@@ -60,4 +64,13 @@ export class IdentitiesComponent implements OnInit {
|
||||
async onClickSelectIdentity(identityId: string) {
|
||||
await this.storage.switchIdentity(identityId);
|
||||
}
|
||||
|
||||
async onToggleRecklessMode() {
|
||||
const newValue = !this.isRecklessMode;
|
||||
await this.storage.getSignerMetaHandler().setRecklessMode(newValue);
|
||||
}
|
||||
|
||||
onClickWhitelistedApps() {
|
||||
this.#router.navigateByUrl('/whitelisted-apps');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<div class="custom-header">
|
||||
<button class="back-btn" (click)="onClickBack()">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</button>
|
||||
<span class="text">Whitelisted Apps</span>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<button
|
||||
class="btn btn-primary whitelist-btn"
|
||||
(click)="onClickWhitelistCurrentTab()"
|
||||
>
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
<span>Whitelist current tab</span>
|
||||
</button>
|
||||
|
||||
<div class="hosts-list">
|
||||
@if (whitelistedHosts.length === 0) {
|
||||
<div class="empty-state">
|
||||
<span class="sam-text-muted">No whitelisted apps yet</span>
|
||||
</div>
|
||||
@if (isRecklessMode) {
|
||||
<div class="warning-note">
|
||||
<span>⚠ All sites will be auto-approved without prompting</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@for (host of whitelistedHosts; track host) {
|
||||
<div class="host-item">
|
||||
<span class="host-name">{{ host }}</span>
|
||||
<button
|
||||
class="remove-btn"
|
||||
title="Remove from whitelist"
|
||||
(click)="onClickRemoveHost(host)"
|
||||
>
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<lib-toast #toast></lib-toast>
|
||||
<lib-confirm #confirm></lib-confirm>
|
||||
@@ -0,0 +1,134 @@
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
|
||||
.custom-header {
|
||||
padding: var(--size);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto;
|
||||
align-items: center;
|
||||
background: var(--background);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
|
||||
.back-btn {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 2;
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 2;
|
||||
justify-self: start;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--foreground);
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
z-index: 1;
|
||||
|
||||
&:hover {
|
||||
background: var(--background-light);
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 2;
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 2;
|
||||
font-family: var(--font-heading);
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05rem;
|
||||
justify-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 var(--size) var(--size) var(--size);
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.whitelist-btn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-bottom: var(--size);
|
||||
}
|
||||
|
||||
.hosts-list {
|
||||
flex-grow: 1;
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.warning-note {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
background: rgba(255, 193, 7, 0.15);
|
||||
border: 1px solid rgba(255, 193, 7, 0.4);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
color: #ffc107;
|
||||
}
|
||||
}
|
||||
|
||||
.host-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
background: var(--background-light);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.host-name {
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--muted-foreground);
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: color 0.15s ease, background-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--destructive);
|
||||
background: var(--background-light-hover);
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Component, inject, ViewChild } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
ConfirmComponent,
|
||||
NavComponent,
|
||||
StorageService,
|
||||
ToastComponent,
|
||||
} from '@common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-whitelisted-apps',
|
||||
templateUrl: './whitelisted-apps.component.html',
|
||||
styleUrl: './whitelisted-apps.component.scss',
|
||||
imports: [ToastComponent, ConfirmComponent],
|
||||
})
|
||||
export class WhitelistedAppsComponent extends NavComponent {
|
||||
@ViewChild('toast') toast!: ToastComponent;
|
||||
@ViewChild('confirm') confirm!: ConfirmComponent;
|
||||
|
||||
readonly storage = inject(StorageService);
|
||||
readonly #router = inject(Router);
|
||||
|
||||
get whitelistedHosts(): string[] {
|
||||
return this.storage.getSignerMetaHandler().signerMetaData?.whitelistedHosts ?? [];
|
||||
}
|
||||
|
||||
get isRecklessMode(): boolean {
|
||||
return this.storage.getSignerMetaHandler().signerMetaData?.recklessMode ?? false;
|
||||
}
|
||||
|
||||
async onClickWhitelistCurrentTab() {
|
||||
try {
|
||||
// Get current active tab
|
||||
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
if (tabs.length === 0 || !tabs[0].url) {
|
||||
this.toast.show('No active tab found');
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(tabs[0].url);
|
||||
const host = url.host;
|
||||
|
||||
if (!host) {
|
||||
this.toast.show('Cannot get host from current tab');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already whitelisted
|
||||
if (this.whitelistedHosts.includes(host)) {
|
||||
this.toast.show(`${host} is already whitelisted`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.storage.getSignerMetaHandler().addWhitelistedHost(host);
|
||||
this.toast.show(`Added ${host} to whitelist`);
|
||||
} catch (error) {
|
||||
console.error('Error getting current tab:', error);
|
||||
this.toast.show('Error getting current tab');
|
||||
}
|
||||
}
|
||||
|
||||
onClickRemoveHost(host: string) {
|
||||
this.confirm.show(`Remove ${host} from whitelist?`, async () => {
|
||||
await this.storage.getSignerMetaHandler().removeWhitelistedHost(host);
|
||||
this.toast.show(`Removed ${host} from whitelist`);
|
||||
});
|
||||
}
|
||||
|
||||
onClickBack() {
|
||||
this.#router.navigateByUrl('/home/identities');
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,40 @@ export const getBrowserSessionData = async function (): Promise<
|
||||
return browserSessionData as BrowserSessionData;
|
||||
};
|
||||
|
||||
export const getSignerMetaData = async function (): Promise<SignerMetaData> {
|
||||
const signerMetaHandler = new ChromeMetaHandler();
|
||||
return (await signerMetaHandler.loadFullData()) as SignerMetaData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if reckless mode should auto-approve the request.
|
||||
* Returns true if should auto-approve, false if should use normal permission flow.
|
||||
*
|
||||
* Logic:
|
||||
* - If reckless mode is OFF → return false (use normal flow)
|
||||
* - If reckless mode is ON and whitelist is empty → return true (approve all)
|
||||
* - If reckless mode is ON and whitelist has entries → return true only if host is in whitelist
|
||||
*/
|
||||
export const shouldRecklessModeApprove = async function (
|
||||
host: string
|
||||
): Promise<boolean> {
|
||||
const signerMetaData = await getSignerMetaData();
|
||||
|
||||
if (!signerMetaData.recklessMode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const whitelistedHosts = signerMetaData.whitelistedHosts ?? [];
|
||||
|
||||
if (whitelistedHosts.length === 0) {
|
||||
// Reckless mode ON, no whitelist → approve all
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reckless mode ON, whitelist has entries → only approve if host is whitelisted
|
||||
return whitelistedHosts.includes(host);
|
||||
};
|
||||
|
||||
export const getBrowserSyncData = async function (): Promise<
|
||||
BrowserSyncData | undefined
|
||||
> {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
nip44Encrypt,
|
||||
PromptResponse,
|
||||
PromptResponseMessage,
|
||||
shouldRecklessModeApprove,
|
||||
signEvent,
|
||||
storePermission,
|
||||
} from './background-common';
|
||||
@@ -63,57 +64,65 @@ browser.runtime.onMessage.addListener(async (message /*, sender*/) => {
|
||||
}
|
||||
|
||||
const req = request as BackgroundRequestMessage;
|
||||
const permissionState = checkPermissions(
|
||||
browserSessionData,
|
||||
currentIdentity,
|
||||
req.host,
|
||||
req.method,
|
||||
req.params
|
||||
);
|
||||
|
||||
if (permissionState === false) {
|
||||
throw new Error('Permission denied');
|
||||
}
|
||||
// Check reckless mode first
|
||||
const recklessApprove = await shouldRecklessModeApprove(req.host);
|
||||
if (recklessApprove) {
|
||||
debug('Request auto-approved via reckless mode.');
|
||||
} else {
|
||||
// Normal permission flow
|
||||
const permissionState = checkPermissions(
|
||||
browserSessionData,
|
||||
currentIdentity,
|
||||
req.host,
|
||||
req.method,
|
||||
req.params
|
||||
);
|
||||
|
||||
if (permissionState === undefined) {
|
||||
// Ask user for permission.
|
||||
const width = 375;
|
||||
const height = 600;
|
||||
const { top, left } = await getPosition(width, height);
|
||||
|
||||
const base64Event = Buffer.from(
|
||||
JSON.stringify(req.params ?? {}, undefined, 2)
|
||||
).toString('base64');
|
||||
|
||||
const response = await new Promise<PromptResponse>((resolve, reject) => {
|
||||
const id = crypto.randomUUID();
|
||||
openPrompts.set(id, { resolve, reject });
|
||||
browser.windows.create({
|
||||
type: 'popup',
|
||||
url: `prompt.html?method=${req.method}&host=${req.host}&id=${id}&nick=${currentIdentity.nick}&event=${base64Event}`,
|
||||
height,
|
||||
width,
|
||||
top,
|
||||
left,
|
||||
});
|
||||
});
|
||||
debug(response);
|
||||
if (response === 'approve' || response === 'reject') {
|
||||
await storePermission(
|
||||
browserSessionData,
|
||||
currentIdentity,
|
||||
req.host,
|
||||
req.method,
|
||||
response === 'approve' ? 'allow' : 'deny',
|
||||
req.params?.kind
|
||||
);
|
||||
}
|
||||
|
||||
if (['reject', 'reject-once'].includes(response)) {
|
||||
if (permissionState === false) {
|
||||
throw new Error('Permission denied');
|
||||
}
|
||||
} else {
|
||||
debug('Request allowed (via saved permission).');
|
||||
|
||||
if (permissionState === undefined) {
|
||||
// Ask user for permission.
|
||||
const width = 375;
|
||||
const height = 600;
|
||||
const { top, left } = await getPosition(width, height);
|
||||
|
||||
const base64Event = Buffer.from(
|
||||
JSON.stringify(req.params ?? {}, undefined, 2)
|
||||
).toString('base64');
|
||||
|
||||
const response = await new Promise<PromptResponse>((resolve, reject) => {
|
||||
const id = crypto.randomUUID();
|
||||
openPrompts.set(id, { resolve, reject });
|
||||
browser.windows.create({
|
||||
type: 'popup',
|
||||
url: `prompt.html?method=${req.method}&host=${req.host}&id=${id}&nick=${currentIdentity.nick}&event=${base64Event}`,
|
||||
height,
|
||||
width,
|
||||
top,
|
||||
left,
|
||||
});
|
||||
});
|
||||
debug(response);
|
||||
if (response === 'approve' || response === 'reject') {
|
||||
await storePermission(
|
||||
browserSessionData,
|
||||
currentIdentity,
|
||||
req.host,
|
||||
req.method,
|
||||
response === 'approve' ? 'allow' : 'deny',
|
||||
req.params?.kind
|
||||
);
|
||||
}
|
||||
|
||||
if (['reject', 'reject-once'].includes(response)) {
|
||||
throw new Error('Permission denied');
|
||||
}
|
||||
} else {
|
||||
debug('Request allowed (via saved permission).');
|
||||
}
|
||||
}
|
||||
|
||||
const relays: Relays = {};
|
||||
|
||||
@@ -8,7 +8,7 @@ export abstract class SignerMetaHandler {
|
||||
|
||||
#signerMetaData?: SignerMetaData;
|
||||
|
||||
readonly metaProperties = ['syncFlow', 'vaultSnapshots'];
|
||||
readonly metaProperties = ['syncFlow', 'vaultSnapshots', 'recklessMode', 'whitelistedHosts'];
|
||||
/**
|
||||
* Load the full data from the storage. If the storage is used for storing
|
||||
* other data (e.g. browser sync data when the user decided to NOT sync),
|
||||
@@ -40,4 +40,53 @@ export abstract class SignerMetaHandler {
|
||||
}
|
||||
|
||||
abstract clearData(keep: string[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* Sets the reckless mode and immediately saves it.
|
||||
*/
|
||||
async setRecklessMode(enabled: boolean): Promise<void> {
|
||||
if (!this.#signerMetaData) {
|
||||
this.#signerMetaData = {
|
||||
recklessMode: enabled,
|
||||
};
|
||||
} else {
|
||||
this.#signerMetaData.recklessMode = enabled;
|
||||
}
|
||||
|
||||
await this.saveFullData(this.#signerMetaData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a host to the whitelist and immediately saves it.
|
||||
*/
|
||||
async addWhitelistedHost(host: string): Promise<void> {
|
||||
if (!this.#signerMetaData) {
|
||||
this.#signerMetaData = {
|
||||
whitelistedHosts: [host],
|
||||
};
|
||||
} else {
|
||||
const hosts = this.#signerMetaData.whitelistedHosts ?? [];
|
||||
if (!hosts.includes(host)) {
|
||||
hosts.push(host);
|
||||
this.#signerMetaData.whitelistedHosts = hosts;
|
||||
}
|
||||
}
|
||||
|
||||
await this.saveFullData(this.#signerMetaData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a host from the whitelist and immediately saves it.
|
||||
*/
|
||||
async removeWhitelistedHost(host: string): Promise<void> {
|
||||
if (!this.#signerMetaData?.whitelistedHosts) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#signerMetaData.whitelistedHosts = this.#signerMetaData.whitelistedHosts.filter(
|
||||
(h) => h !== host
|
||||
);
|
||||
|
||||
await this.saveFullData(this.#signerMetaData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +92,12 @@ export interface SignerMetaData {
|
||||
syncFlow?: number; // 0 = no sync, 1 = browser sync, (future: 2 = Signer sync, 3 = Custom sync (bring your own sync))
|
||||
|
||||
vaultSnapshots?: SignerMetaData_VaultSnapshot[];
|
||||
|
||||
// Reckless mode: auto-approve all actions without prompting
|
||||
recklessMode?: boolean;
|
||||
|
||||
// Whitelisted hosts: auto-approve all actions from these hosts
|
||||
whitelistedHosts?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "Plebeian Signer",
|
||||
"description": "Nostr Identity Manager & Signer",
|
||||
"version": "v0.0.8",
|
||||
"version": "0.0.9",
|
||||
"homepage_url": "https://git.mleku.dev/mleku/plebeian-signer",
|
||||
"options_page": "options.html",
|
||||
"permissions": [
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="#ffffff" class="bi bi-person-fill" viewBox="0 0 16 16">
|
||||
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6"/>
|
||||
</svg>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 4C11.606 4 11.2159 4.0776 10.8519 4.22836C10.488 4.37913 10.1573 4.6001 9.87868 4.87868C9.6001 5.15726 9.37913 5.48797 9.22836 5.85195C9.0776 6.21593 9 6.60603 9 7C9 7.39397 9.0776 7.78407 9.22836 8.14805C9.37913 8.51203 9.6001 8.84274 9.87868 9.12132C10.1573 9.3999 10.488 9.62087 10.8519 9.77164C11.2159 9.9224 11.606 10 12 10C12.7956 10 13.5587 9.68393 14.1213 9.12132C14.6839 8.55871 15 7.79565 15 7C15 6.20435 14.6839 5.44129 14.1213 4.87868C13.5587 4.31607 12.7956 4 12 4ZM7 7C7 5.67392 7.52678 4.40215 8.46447 3.46447C9.40215 2.52678 10.6739 2 12 2C13.3261 2 14.5979 2.52678 15.5355 3.46447C16.4732 4.40215 17 5.67392 17 7C17 8.32608 16.4732 9.59785 15.5355 10.5355C14.5979 11.4732 13.3261 12 12 12C10.6739 12 9.40215 11.4732 8.46447 10.5355C7.52678 9.59785 7 8.32608 7 7ZM3.5 19C3.5 17.6739 4.02678 16.4021 4.96447 15.4645C5.90215 14.5268 7.17392 14 8.5 14H15.5C16.1566 14 16.8068 14.1293 17.4134 14.3806C18.02 14.6319 18.5712 15.0002 19.0355 15.4645C19.4998 15.9288 19.8681 16.48 20.1194 17.0866C20.3707 17.6932 20.5 18.3434 20.5 19V21H18.5V19C18.5 18.2044 18.1839 17.4413 17.6213 16.8787C17.0587 16.3161 16.2956 16 15.5 16H8.5C7.70435 16 6.94129 16.3161 6.37868 16.8787C5.81607 17.4413 5.5 18.2044 5.5 19V21H3.5V19Z" fill="#FAFAFA"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 219 B After Width: | Height: | Size: 1.3 KiB |
@@ -16,6 +16,7 @@ import { WelcomeComponent } from './components/welcome/welcome.component';
|
||||
import { VaultLoginComponent } from './components/vault-login/vault-login.component';
|
||||
import { VaultCreateComponent } from './components/vault-create/vault-create.component';
|
||||
import { VaultImportComponent } from './components/vault-import/vault-import.component';
|
||||
import { WhitelistedAppsComponent } from './components/whitelisted-apps/whitelisted-apps.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -70,6 +71,10 @@ export const routes: Routes = [
|
||||
path: 'new-identity',
|
||||
component: NewIdentityComponent,
|
||||
},
|
||||
{
|
||||
path: 'whitelisted-apps',
|
||||
component: WhitelistedAppsComponent,
|
||||
},
|
||||
{
|
||||
path: 'edit-identity/:id',
|
||||
component: EditIdentityComponent,
|
||||
|
||||
@@ -11,6 +11,30 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="reckless-mode-row">
|
||||
<label class="reckless-label" (click)="onToggleRecklessMode()">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="isRecklessMode"
|
||||
(click)="$event.stopPropagation()"
|
||||
(change)="onToggleRecklessMode()"
|
||||
/>
|
||||
<span
|
||||
class="reckless-text"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
title="Auto-approve all actions. If whitelist has entries, only those apps are auto-approved."
|
||||
>Reckless mode</span>
|
||||
</label>
|
||||
<button
|
||||
class="gear-btn"
|
||||
title="Manage whitelisted apps"
|
||||
(click)="onClickWhitelistedApps()"
|
||||
>
|
||||
<i class="bi bi-gear"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@let sessionData = storage.getBrowserSessionHandler().browserSessionData;
|
||||
@let identities = sessionData?.identities ?? [];
|
||||
|
||||
@@ -34,7 +58,7 @@
|
||||
class="avatar"
|
||||
[src]="getAvatarUrl(identity)"
|
||||
alt=""
|
||||
(error)="$any($event.target).src = 'assets/person-fill.svg'"
|
||||
(error)="$any($event.target).src = 'person-fill.svg'"
|
||||
/>
|
||||
<span class="name">{{ getDisplayName(identity) }}</span>
|
||||
<lib-icon-button
|
||||
|
||||
@@ -28,13 +28,66 @@
|
||||
grid-column-end: 2;
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 2;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
font-family: var(--font-heading);
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1rem;
|
||||
justify-self: center;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.reckless-mode-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 12px;
|
||||
background: var(--background-light);
|
||||
border-radius: 8px;
|
||||
|
||||
.reckless-label {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reckless-text {
|
||||
font-size: 14px;
|
||||
color: var(--foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.gear-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--muted-foreground);
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: color 0.15s ease, background-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--foreground);
|
||||
background: var(--background-light-hover);
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
||||
@@ -24,6 +24,10 @@ export class IdentitiesComponent implements OnInit {
|
||||
// Cache of pubkey -> profile for quick lookup
|
||||
#profileCache = new Map<string, ProfileMetadata | null>();
|
||||
|
||||
get isRecklessMode(): boolean {
|
||||
return this.storage.getSignerMetaHandler().signerMetaData?.recklessMode ?? false;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.#profileMetadata.initialize();
|
||||
this.#loadProfiles();
|
||||
@@ -40,7 +44,7 @@ export class IdentitiesComponent implements OnInit {
|
||||
|
||||
getAvatarUrl(identity: Identity_DECRYPTED): string {
|
||||
const profile = this.#profileCache.get(identity.id);
|
||||
return profile?.picture || 'assets/person-fill.svg';
|
||||
return profile?.picture || 'person-fill.svg';
|
||||
}
|
||||
|
||||
getDisplayName(identity: Identity_DECRYPTED): string {
|
||||
@@ -60,4 +64,13 @@ export class IdentitiesComponent implements OnInit {
|
||||
async onClickSelectIdentity(identityId: string) {
|
||||
await this.storage.switchIdentity(identityId);
|
||||
}
|
||||
|
||||
async onToggleRecklessMode() {
|
||||
const newValue = !this.isRecklessMode;
|
||||
await this.storage.getSignerMetaHandler().setRecklessMode(newValue);
|
||||
}
|
||||
|
||||
onClickWhitelistedApps() {
|
||||
this.#router.navigateByUrl('/whitelisted-apps');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<div class="custom-header">
|
||||
<button class="back-btn" (click)="onClickBack()">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</button>
|
||||
<span class="text">Whitelisted Apps</span>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<button
|
||||
class="btn btn-primary whitelist-btn"
|
||||
(click)="onClickWhitelistCurrentTab()"
|
||||
>
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
<span>Whitelist current tab</span>
|
||||
</button>
|
||||
|
||||
<div class="hosts-list">
|
||||
@if (whitelistedHosts.length === 0) {
|
||||
<div class="empty-state">
|
||||
<span class="sam-text-muted">No whitelisted apps yet</span>
|
||||
</div>
|
||||
@if (isRecklessMode) {
|
||||
<div class="warning-note">
|
||||
<span>⚠ All sites will be auto-approved without prompting</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@for (host of whitelistedHosts; track host) {
|
||||
<div class="host-item">
|
||||
<span class="host-name">{{ host }}</span>
|
||||
<button
|
||||
class="remove-btn"
|
||||
title="Remove from whitelist"
|
||||
(click)="onClickRemoveHost(host)"
|
||||
>
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<lib-toast #toast></lib-toast>
|
||||
<lib-confirm #confirm></lib-confirm>
|
||||
@@ -0,0 +1,134 @@
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
|
||||
.custom-header {
|
||||
padding: var(--size);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto;
|
||||
align-items: center;
|
||||
background: var(--background);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
|
||||
.back-btn {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 2;
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 2;
|
||||
justify-self: start;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--foreground);
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
z-index: 1;
|
||||
|
||||
&:hover {
|
||||
background: var(--background-light);
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 2;
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 2;
|
||||
font-family: var(--font-heading);
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05rem;
|
||||
justify-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 var(--size) var(--size) var(--size);
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.whitelist-btn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-bottom: var(--size);
|
||||
}
|
||||
|
||||
.hosts-list {
|
||||
flex-grow: 1;
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.warning-note {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
background: rgba(255, 193, 7, 0.15);
|
||||
border: 1px solid rgba(255, 193, 7, 0.4);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
color: #ffc107;
|
||||
}
|
||||
}
|
||||
|
||||
.host-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
background: var(--background-light);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.host-name {
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--muted-foreground);
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: color 0.15s ease, background-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--destructive);
|
||||
background: var(--background-light-hover);
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Component, inject, ViewChild } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
ConfirmComponent,
|
||||
NavComponent,
|
||||
StorageService,
|
||||
ToastComponent,
|
||||
} from '@common';
|
||||
import browser from 'webextension-polyfill';
|
||||
|
||||
@Component({
|
||||
selector: 'app-whitelisted-apps',
|
||||
templateUrl: './whitelisted-apps.component.html',
|
||||
styleUrl: './whitelisted-apps.component.scss',
|
||||
imports: [ToastComponent, ConfirmComponent],
|
||||
})
|
||||
export class WhitelistedAppsComponent extends NavComponent {
|
||||
@ViewChild('toast') toast!: ToastComponent;
|
||||
@ViewChild('confirm') confirm!: ConfirmComponent;
|
||||
|
||||
readonly storage = inject(StorageService);
|
||||
readonly #router = inject(Router);
|
||||
|
||||
get whitelistedHosts(): string[] {
|
||||
return this.storage.getSignerMetaHandler().signerMetaData?.whitelistedHosts ?? [];
|
||||
}
|
||||
|
||||
get isRecklessMode(): boolean {
|
||||
return this.storage.getSignerMetaHandler().signerMetaData?.recklessMode ?? false;
|
||||
}
|
||||
|
||||
async onClickWhitelistCurrentTab() {
|
||||
try {
|
||||
// Get current active tab
|
||||
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
||||
if (tabs.length === 0 || !tabs[0].url) {
|
||||
this.toast.show('No active tab found');
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(tabs[0].url);
|
||||
const host = url.host;
|
||||
|
||||
if (!host) {
|
||||
this.toast.show('Cannot get host from current tab');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already whitelisted
|
||||
if (this.whitelistedHosts.includes(host)) {
|
||||
this.toast.show(`${host} is already whitelisted`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.storage.getSignerMetaHandler().addWhitelistedHost(host);
|
||||
this.toast.show(`Added ${host} to whitelist`);
|
||||
} catch (error) {
|
||||
console.error('Error getting current tab:', error);
|
||||
this.toast.show('Error getting current tab');
|
||||
}
|
||||
}
|
||||
|
||||
onClickRemoveHost(host: string) {
|
||||
this.confirm.show(`Remove ${host} from whitelist?`, async () => {
|
||||
await this.storage.getSignerMetaHandler().removeWhitelistedHost(host);
|
||||
this.toast.show(`Removed ${host} from whitelist`);
|
||||
});
|
||||
}
|
||||
|
||||
onClickBack() {
|
||||
this.#router.navigateByUrl('/home/identities');
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,40 @@ export const getBrowserSessionData = async function (): Promise<
|
||||
return browserSessionData as unknown as BrowserSessionData;
|
||||
};
|
||||
|
||||
export const getSignerMetaData = async function (): Promise<SignerMetaData> {
|
||||
const signerMetaHandler = new FirefoxMetaHandler();
|
||||
return (await signerMetaHandler.loadFullData()) as SignerMetaData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if reckless mode should auto-approve the request.
|
||||
* Returns true if should auto-approve, false if should use normal permission flow.
|
||||
*
|
||||
* Logic:
|
||||
* - If reckless mode is OFF → return false (use normal flow)
|
||||
* - If reckless mode is ON and whitelist is empty → return true (approve all)
|
||||
* - If reckless mode is ON and whitelist has entries → return true only if host is in whitelist
|
||||
*/
|
||||
export const shouldRecklessModeApprove = async function (
|
||||
host: string
|
||||
): Promise<boolean> {
|
||||
const signerMetaData = await getSignerMetaData();
|
||||
|
||||
if (!signerMetaData.recklessMode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const whitelistedHosts = signerMetaData.whitelistedHosts ?? [];
|
||||
|
||||
if (whitelistedHosts.length === 0) {
|
||||
// Reckless mode ON, no whitelist → approve all
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reckless mode ON, whitelist has entries → only approve if host is whitelisted
|
||||
return whitelistedHosts.includes(host);
|
||||
};
|
||||
|
||||
export const getBrowserSyncData = async function (): Promise<
|
||||
BrowserSyncData | undefined
|
||||
> {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
nip44Encrypt,
|
||||
PromptResponse,
|
||||
PromptResponseMessage,
|
||||
shouldRecklessModeApprove,
|
||||
signEvent,
|
||||
storePermission,
|
||||
} from './background-common';
|
||||
@@ -63,57 +64,65 @@ browser.runtime.onMessage.addListener(async (message /*, sender*/) => {
|
||||
}
|
||||
|
||||
const req = request as BackgroundRequestMessage;
|
||||
const permissionState = checkPermissions(
|
||||
browserSessionData,
|
||||
currentIdentity,
|
||||
req.host,
|
||||
req.method,
|
||||
req.params
|
||||
);
|
||||
|
||||
if (permissionState === false) {
|
||||
throw new Error('Permission denied');
|
||||
}
|
||||
// Check reckless mode first
|
||||
const recklessApprove = await shouldRecklessModeApprove(req.host);
|
||||
if (recklessApprove) {
|
||||
debug('Request auto-approved via reckless mode.');
|
||||
} else {
|
||||
// Normal permission flow
|
||||
const permissionState = checkPermissions(
|
||||
browserSessionData,
|
||||
currentIdentity,
|
||||
req.host,
|
||||
req.method,
|
||||
req.params
|
||||
);
|
||||
|
||||
if (permissionState === undefined) {
|
||||
// Ask user for permission.
|
||||
const width = 375;
|
||||
const height = 600;
|
||||
const { top, left } = await getPosition(width, height);
|
||||
|
||||
const base64Event = Buffer.from(
|
||||
JSON.stringify(req.params ?? {}, undefined, 2)
|
||||
).toString('base64');
|
||||
|
||||
const response = await new Promise<PromptResponse>((resolve, reject) => {
|
||||
const id = crypto.randomUUID();
|
||||
openPrompts.set(id, { resolve, reject });
|
||||
browser.windows.create({
|
||||
type: 'popup',
|
||||
url: `prompt.html?method=${req.method}&host=${req.host}&id=${id}&nick=${currentIdentity.nick}&event=${base64Event}`,
|
||||
height,
|
||||
width,
|
||||
top,
|
||||
left,
|
||||
});
|
||||
});
|
||||
debug(response);
|
||||
if (response === 'approve' || response === 'reject') {
|
||||
await storePermission(
|
||||
browserSessionData,
|
||||
currentIdentity,
|
||||
req.host,
|
||||
req.method,
|
||||
response === 'approve' ? 'allow' : 'deny',
|
||||
req.params?.kind
|
||||
);
|
||||
}
|
||||
|
||||
if (['reject', 'reject-once'].includes(response)) {
|
||||
if (permissionState === false) {
|
||||
throw new Error('Permission denied');
|
||||
}
|
||||
} else {
|
||||
debug('Request allowed (via saved permission).');
|
||||
|
||||
if (permissionState === undefined) {
|
||||
// Ask user for permission.
|
||||
const width = 375;
|
||||
const height = 600;
|
||||
const { top, left } = await getPosition(width, height);
|
||||
|
||||
const base64Event = Buffer.from(
|
||||
JSON.stringify(req.params ?? {}, undefined, 2)
|
||||
).toString('base64');
|
||||
|
||||
const response = await new Promise<PromptResponse>((resolve, reject) => {
|
||||
const id = crypto.randomUUID();
|
||||
openPrompts.set(id, { resolve, reject });
|
||||
browser.windows.create({
|
||||
type: 'popup',
|
||||
url: `prompt.html?method=${req.method}&host=${req.host}&id=${id}&nick=${currentIdentity.nick}&event=${base64Event}`,
|
||||
height,
|
||||
width,
|
||||
top,
|
||||
left,
|
||||
});
|
||||
});
|
||||
debug(response);
|
||||
if (response === 'approve' || response === 'reject') {
|
||||
await storePermission(
|
||||
browserSessionData,
|
||||
currentIdentity,
|
||||
req.host,
|
||||
req.method,
|
||||
response === 'approve' ? 'allow' : 'deny',
|
||||
req.params?.kind
|
||||
);
|
||||
}
|
||||
|
||||
if (['reject', 'reject-once'].includes(response)) {
|
||||
throw new Error('Permission denied');
|
||||
}
|
||||
} else {
|
||||
debug('Request allowed (via saved permission).');
|
||||
}
|
||||
}
|
||||
|
||||
const relays: Relays = {};
|
||||
|
||||
Reference in New Issue
Block a user