Browse Source

don't display nsec in GUI

master
Silberengel 1 week ago
parent
commit
e2c06c9c7f
  1. 30
      README.md
  2. 6
      manifest.json
  3. 24
      src/eventStorage.ts
  4. 32
      src/main.ts
  5. 3
      src/metadataManager.ts
  6. 9
      src/nostr/authHandler.ts
  7. 24
      src/nostr/eventBuilder.ts
  8. 98
      src/nostr/profileFetcher.ts
  9. 9
      src/nostr/relayClient.ts
  10. 3
      src/relayManager.ts
  11. 85
      src/ui/settingsTab.ts
  12. 174
      src/utils/security.ts

30
README.md

@ -1,23 +1,35 @@ @@ -1,23 +1,35 @@
# Scriptorium Obsidian Plugin
# Scriptorium Nostr
An Obsidian plugin for creating, editing, and publishing Nostr document events directly from your vault.
**Author**: Silberengel
**Homepage**: https://gitcitadel.com
**Funding**: gitcitadel@getalby.com
## Features
- **Multiple Event Kinds**: Support for kinds 1, 11, 30023, 30040, 30041, 30817, 30818
- **AsciiDoc Support**: Automatic parsing and splitting of AsciiDoc documents into nested 30040/30041 structures
- **Multiple Event Kinds**: Support for Markdown-formatted kinds (1, 11, 30023, 30817) and Asdciidoc-formatted kinds (30040, 30041, 30818).
- **Bookstr Support**: Automatic parsing and splitting of e-books/publications into nested 30040/30041 structures
- **Metadata Management**: YAML metadata files with validation per event kind
- **Structure Preview**: Visual preview of document structure before creating events
- **Structure Preview**: Visual preview of publication structure before creating events
- **Two-Step Workflow**: Create and sign events separately from publishing
- **Relay Management**: Automatic fetching of relay lists (kind 10002) with AUTH support
- **d-tag Normalization**: Automatic NIP-54 compliant d-tag generation from titles
## Installation
### Manual Installation
1. Clone this repository
2. Run `npm install`
2. Run `npm install`.obsidian/plugins/scriptorium-obsidian/
3. Run `npm run build`
4. Copy the `main.js`, `manifest.json`, and `styles.css` (if any) to your Obsidian vault's `.obsidian/plugins/scriptorium-obsidian/` directory
4. Create the plugin directory in your Obsidian vault (if it doesn't exist):
- Navigate to your vault's root directory
- Create `.obsidian/plugins/scriptorium-obsidian/` directory
5. Copy the `main.js` and `manifest.json` files to `.obsidian/plugins/scriptorium-obsidian/`
6. Reload Obsidian and enable the plugin in Settings → Community Plugins
**Note**: The `.obsidian` folder is hidden by default. You may need to show hidden files in your file manager to see it.
## Setup
@ -86,3 +98,9 @@ npm run build # Production build @@ -86,3 +98,9 @@ npm run build # Production build
## License
MIT
## Author
**Silberengel**
- Homepage: https://gitcitadel.com
- Funding: gitcitadel@getalby.com

6
manifest.json

@ -4,8 +4,8 @@ @@ -4,8 +4,8 @@
"version": "0.1.0",
"minAppVersion": "0.15.0",
"description": "Create, edit, and publish Nostr document events from Obsidian",
"author": "Scriptorium",
"authorUrl": "",
"fundingUrl": "",
"author": "Silberengel",
"authorUrl": "https://gitcitadel.com",
"fundingUrl": "gitcitadel@getalby.com",
"isDesktopOnly": false
}

24
src/eventStorage.ts

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import { TFile } from "obsidian";
import { SignedEvent } from "./types";
import { safeConsoleError, verifyEventSecurity } from "./utils/security";
/**
* Get events file path for a given file
@ -19,6 +20,13 @@ export async function saveEvents( @@ -19,6 +20,13 @@ export async function saveEvents(
events: SignedEvent[],
app: any
): Promise<void> {
// Security check: verify no events contain private keys
for (const event of events) {
if (!verifyEventSecurity(event)) {
throw new Error("Security error: Cannot save event containing private key");
}
}
const eventsPath = getEventsFilePath(file);
const lines = events.map((event) => JSON.stringify(event));
const content = lines.join("\n") + "\n";
@ -40,9 +48,19 @@ export async function loadEvents( @@ -40,9 +48,19 @@ export async function loadEvents(
}
const content = await app.vault.read(eventsFile);
const lines = content.split("\n").filter((line: string) => line.trim().length > 0);
return lines.map((line: string) => JSON.parse(line) as SignedEvent);
const events = lines.map((line: string) => JSON.parse(line) as SignedEvent);
// Security check: verify loaded events don't contain private keys
for (const event of events) {
if (!verifyEventSecurity(event)) {
safeConsoleError("Security warning: Loaded event contains private key - removing from results");
return [];
}
}
return events;
} catch (error) {
console.error("Error loading events:", error);
safeConsoleError("Error loading events:", error);
return [];
}
}
@ -67,6 +85,6 @@ export async function deleteEvents(file: TFile, app: any): Promise<void> { @@ -67,6 +85,6 @@ export async function deleteEvents(file: TFile, app: any): Promise<void> {
await app.vault.delete(eventsFile);
}
} catch (error) {
console.error("Error deleting events file:", error);
safeConsoleError("Error deleting events file:", error);
}
}

32
src/main.ts

@ -10,6 +10,7 @@ import { publishEventsWithRetry } from "./nostr/relayClient"; @@ -10,6 +10,7 @@ import { publishEventsWithRetry } from "./nostr/relayClient";
import { getWriteRelays } from "./relayManager";
import { parseAsciiDocStructure, isAsciiDocDocument } from "./asciidocParser";
import { normalizeSecretKey, getPubkeyFromPrivkey } from "./nostr/eventBuilder";
import { safeConsoleError, safeConsoleLog, verifyEventSecurity } from "./utils/security";
export default class ScriptoriumPlugin extends Plugin {
settings!: ScriptoriumSettings;
@ -73,7 +74,7 @@ export default class ScriptoriumPlugin extends Plugin { @@ -73,7 +74,7 @@ export default class ScriptoriumPlugin extends Plugin {
}
} catch (error) {
// Environment variable access not available, user must set manually
console.log("Environment variable access not available, use settings to set private key");
safeConsoleLog("Environment variable access not available, use settings to set private key");
}
}
@ -137,6 +138,15 @@ export default class ScriptoriumPlugin extends Plugin { @@ -137,6 +138,15 @@ export default class ScriptoriumPlugin extends Plugin {
return;
}
// Security check: verify events don't contain private keys
for (const event of result.events) {
if (!verifyEventSecurity(event)) {
new Notice("Security error: Event contains private key. Aborting.");
safeConsoleError("Event security check failed - event may contain private key");
return;
}
}
// Show preview for structured documents
if (result.structure.length > 0) {
new StructurePreviewModal(this.app, result.structure, async () => {
@ -148,8 +158,9 @@ export default class ScriptoriumPlugin extends Plugin { @@ -148,8 +158,9 @@ export default class ScriptoriumPlugin extends Plugin {
new Notice(`Created ${result.events.length} event(s) and saved to ${file.basename}_events.jsonl`);
}
} catch (error: any) {
new Notice(`Error creating events: ${error.message}`);
console.error(error);
const safeMessage = error?.message ? String(error.message).replace(/nsec1[a-z0-9]{58,}/gi, "[REDACTED]").replace(/[0-9a-f]{64}/gi, "[REDACTED]") : "Unknown error";
new Notice(`Error creating events: ${safeMessage}`);
safeConsoleError("Error creating events:", error);
}
}
@ -175,8 +186,9 @@ export default class ScriptoriumPlugin extends Plugin { @@ -175,8 +186,9 @@ export default class ScriptoriumPlugin extends Plugin {
const structure = parseAsciiDocStructure(content, metadata as any);
new StructurePreviewModal(this.app, structure, () => {}).open();
} catch (error: any) {
new Notice(`Error previewing structure: ${error.message}`);
console.error(error);
const safeMessage = error?.message ? String(error.message).replace(/nsec1[a-z0-9]{58,}/gi, "[REDACTED]").replace(/[0-9a-f]{64}/gi, "[REDACTED]") : "Unknown error";
new Notice(`Error previewing structure: ${safeMessage}`);
safeConsoleError("Error previewing structure:", error);
}
}
@ -231,8 +243,9 @@ export default class ScriptoriumPlugin extends Plugin { @@ -231,8 +243,9 @@ export default class ScriptoriumPlugin extends Plugin {
new Notice(`Published ${successCount} event(s), ${failureCount} failed`);
}
} catch (error: any) {
new Notice(`Error publishing events: ${error.message}`);
console.error(error);
const safeMessage = error?.message ? String(error.message).replace(/nsec1[a-z0-9]{58,}/gi, "[REDACTED]").replace(/[0-9a-f]{64}/gi, "[REDACTED]") : "Unknown error";
new Notice(`Error publishing events: ${safeMessage}`);
safeConsoleError("Error publishing events:", error);
}
}
@ -261,8 +274,9 @@ export default class ScriptoriumPlugin extends Plugin { @@ -261,8 +274,9 @@ export default class ScriptoriumPlugin extends Plugin {
new Notice("Metadata saved");
}).open();
} catch (error: any) {
new Notice(`Error editing metadata: ${error.message}`);
console.error(error);
const safeMessage = error?.message ? String(error.message).replace(/nsec1[a-z0-9]{58,}/gi, "[REDACTED]").replace(/[0-9a-f]{64}/gi, "[REDACTED]") : "Unknown error";
new Notice(`Error editing metadata: ${safeMessage}`);
safeConsoleError("Error editing metadata:", error);
}
}
}

3
src/metadataManager.ts

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import * as yaml from "js-yaml";
import { TFile } from "obsidian";
import { EventKind, EventMetadata } from "./types";
import { safeConsoleError } from "./utils/security";
/**
* Get metadata file path for a given file
@ -29,7 +30,7 @@ export async function readMetadata( @@ -29,7 +30,7 @@ export async function readMetadata(
const parsed = yaml.load(content) as any;
return parsed as EventMetadata;
} catch (error) {
console.error("Error reading metadata:", error);
safeConsoleError("Error reading metadata:", error);
return null;
}
}

9
src/nostr/authHandler.ts

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import { Relay, finalizeEvent, getPublicKey } from "nostr-tools";
import { normalizeSecretKey } from "./eventBuilder";
import { safeConsoleError } from "../utils/security";
/**
* Handle AUTH challenge from relay (NIP-42)
@ -21,7 +22,7 @@ export async function handleAuthChallenge( @@ -21,7 +22,7 @@ export async function handleAuthChallenge(
});
return true;
} catch (error) {
console.error("Error handling AUTH challenge:", error);
safeConsoleError("Error handling AUTH challenge:", error);
return false;
}
}
@ -46,7 +47,7 @@ export async function ensureAuthenticated( @@ -46,7 +47,7 @@ export async function ensureAuthenticated(
// The relay will call onauth if it needs authentication
return true;
} catch (error) {
console.error("Error ensuring authentication:", error);
safeConsoleError("Error ensuring authentication:", error);
return false;
}
}
@ -71,6 +72,8 @@ export async function handleAuthRequiredError( @@ -71,6 +72,8 @@ export async function handleAuthRequiredError(
// Retry original operation
return originalOperation();
} catch (error: any) {
throw new Error(`Failed to authenticate with relay: ${error.message}`);
// Sanitize error message to prevent private key leaks
const safeMessage = error?.message ? String(error.message).replace(/nsec1[a-z0-9]{58,}/gi, "[REDACTED]").replace(/[0-9a-f]{64}/gi, "[REDACTED]") : "Unknown error";
throw new Error(`Failed to authenticate with relay: ${safeMessage}`);
}
}

24
src/nostr/eventBuilder.ts

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import { finalizeEvent, getEventHash, getPublicKey, nip19 } from "nostr-tools";
import { EventKind, EventMetadata, SignedEvent } from "../types";
import { sanitizeString } from "../utils/security";
/**
* Normalize secret key from bech32 nsec or hex format to hex
@ -12,7 +13,8 @@ export function normalizeSecretKey(key: string): Uint8Array { @@ -12,7 +13,8 @@ export function normalizeSecretKey(key: string): Uint8Array {
return decoded.data;
}
} catch (e) {
throw new Error(`Invalid nsec format: ${e}`);
const errorMsg = e instanceof Error ? e.message : String(e);
throw new Error(`Invalid nsec format: ${sanitizeString(errorMsg)}`);
}
}
// Assume hex format (64 chars)
@ -42,6 +44,26 @@ export function getPubkeyFromPrivkeyBytes(privkey: Uint8Array): string { @@ -42,6 +44,26 @@ export function getPubkeyFromPrivkeyBytes(privkey: Uint8Array): string {
return getPublicKey(privkey);
}
/**
* Convert public key to npub (bech32 encoded)
*/
export function pubkeyToNpub(pubkey: string): string {
try {
return nip19.npubEncode(pubkey);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to encode pubkey to npub: ${sanitizeString(errorMsg)}`);
}
}
/**
* Get npub from private key
*/
export function getNpubFromPrivkey(privkey: string): string {
const pubkey = getPubkeyFromPrivkey(privkey);
return pubkeyToNpub(pubkey);
}
/**
* Build tags array from metadata
*/

98
src/nostr/profileFetcher.ts

@ -0,0 +1,98 @@ @@ -0,0 +1,98 @@
import { Relay } from "nostr-tools";
import { safeConsoleError } from "../utils/security";
/**
* User profile information
*/
export interface UserProfile {
name?: string;
display_name?: string;
about?: string;
picture?: string;
}
/**
* Fetch user profile (kind 0) from relays
*/
export async function fetchUserProfile(
pubkey: string,
relayUrls: string[],
timeout: number = 5000
): Promise<UserProfile | null> {
for (const relayUrl of relayUrls) {
try {
const profile = await fetchProfileFromRelay(relayUrl, pubkey, timeout);
if (profile) {
return profile;
}
} catch (error) {
safeConsoleError(`Error fetching profile from ${relayUrl}:`, error);
continue;
}
}
return null;
}
/**
* Fetch profile from a single relay
*/
async function fetchProfileFromRelay(
relayUrl: string,
pubkey: string,
timeout: number
): Promise<UserProfile | null> {
return new Promise(async (resolve) => {
let relay: Relay | null = null;
const timer = setTimeout(() => {
if (relay) {
relay.close();
}
resolve(null);
}, timeout);
try {
relay = new Relay(relayUrl);
await relay.connect();
const sub = relay.subscribe(
[
{
kinds: [0],
authors: [pubkey],
},
],
{
onevent: (event) => {
clearTimeout(timer);
relay?.close();
try {
const profile = JSON.parse(event.content) as UserProfile;
resolve(profile);
} catch (error) {
resolve(null);
}
},
oneose: () => {
clearTimeout(timer);
relay?.close();
resolve(null);
},
}
);
// Wait for response
setTimeout(() => {
sub.close();
if (relay) {
relay.close();
}
}, timeout - 100);
} catch (error) {
clearTimeout(timer);
if (relay) {
relay.close();
}
resolve(null);
}
});
}

9
src/nostr/relayClient.ts

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import { Relay } from "nostr-tools";
import { SignedEvent, PublishingResult } from "../types";
import { ensureAuthenticated, handleAuthRequiredError } from "./authHandler";
import { safeConsoleError } from "../utils/security";
/**
* Publish a single event to a relay
@ -27,7 +28,7 @@ export async function publishEventToRelay( @@ -27,7 +28,7 @@ export async function publishEventToRelay(
handleAuthRequiredError(relay!, privkey, relayUrl, async () => {
return await relay!.publish(event);
}).catch((error) => {
console.error("Auth failed:", error);
safeConsoleError("Auth failed:", error);
});
}
if (originalOnNotice) {
@ -64,22 +65,24 @@ export async function publishEventToRelay( @@ -64,22 +65,24 @@ export async function publishEventToRelay(
};
} catch (error: any) {
relay.close();
const safeMessage = error?.message ? String(error.message).replace(/nsec1[a-z0-9]{58,}/gi, "[REDACTED]").replace(/[0-9a-f]{64}/gi, "[REDACTED]") : "Publish failed";
return {
eventId: event.id,
relay: relayUrl,
success: false,
message: error.message || "Publish failed",
message: safeMessage,
};
}
} catch (error: any) {
if (relay) {
relay.close();
}
const safeMessage = error?.message ? String(error.message).replace(/nsec1[a-z0-9]{58,}/gi, "[REDACTED]").replace(/[0-9a-f]{64}/gi, "[REDACTED]") : "Failed to connect to relay";
return {
eventId: event.id,
relay: relayUrl,
success: false,
message: error.message || "Failed to connect to relay",
message: safeMessage,
};
}
}

3
src/relayManager.ts

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import { Relay, getPublicKey } from "nostr-tools";
import { RelayInfo } from "./types";
import { normalizeSecretKey } from "./nostr/eventBuilder";
import { safeConsoleError } from "./utils/security";
/**
* Default relay URLs to query for kind 10002
@ -98,7 +99,7 @@ export async function fetchRelayListFromRelay( @@ -98,7 +99,7 @@ export async function fetchRelayListFromRelay(
if (relay) {
relay.close();
}
console.error(`Error fetching relay list from ${relayUrl}:`, error);
safeConsoleError(`Error fetching relay list from ${relayUrl}:`, error);
resolve(null);
}
});

85
src/ui/settingsTab.ts

@ -1,8 +1,9 @@ @@ -1,8 +1,9 @@
import { App, PluginSettingTab, Setting } from "obsidian";
import ScriptoriumPlugin from "../main";
import { EventKind } from "../types";
import { fetchRelayList, addTheCitadelIfMissing, includesTheCitadel } from "../relayManager";
import { getPubkeyFromPrivkey } from "../nostr/eventBuilder";
import { fetchRelayList, addTheCitadelIfMissing, includesTheCitadel, getReadRelays } from "../relayManager";
import { getPubkeyFromPrivkey, getNpubFromPrivkey } from "../nostr/eventBuilder";
import { fetchUserProfile } from "../nostr/profileFetcher";
/**
* Settings tab for the plugin
@ -15,31 +16,69 @@ export class ScriptoriumSettingTab extends PluginSettingTab { @@ -15,31 +16,69 @@ export class ScriptoriumSettingTab extends PluginSettingTab {
this.plugin = plugin;
}
display(): void {
async display(): Promise<void> {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl("h2", { text: "Scriptorium Nostr Settings" });
// Private Key
new Setting(containerEl)
.setName("Private Key")
.setDesc("Your Nostr private key (nsec or hex). Loaded from SCRIPTORIUM_OBSIDIAN_KEY environment variable.")
.addText((text) => {
const key = this.plugin.settings.privateKey || "";
text.setValue(key ? "***" + key.slice(-4) : "")
.setPlaceholder("nsec1... or hex")
.setDisabled(true);
})
.addButton((button) => {
button.setButtonText("Refresh from Env")
.setCta()
.onClick(async () => {
await this.plugin.loadPrivateKey();
this.display();
// User Identity (npub and handle)
if (this.plugin.settings.privateKey) {
try {
const npub = getNpubFromPrivkey(this.plugin.settings.privateKey);
const pubkey = getPubkeyFromPrivkey(this.plugin.settings.privateKey);
// Fetch profile to get handle/name
let profile: { name?: string; display_name?: string } | null = null;
const readRelays = getReadRelays(this.plugin.settings.relayList);
if (readRelays.length > 0) {
profile = await fetchUserProfile(pubkey, readRelays);
}
const displayName = profile?.display_name || profile?.name || "Unknown";
new Setting(containerEl)
.setName("Your Identity")
.setDesc("Your Nostr public identity (loaded from SCRIPTORIUM_OBSIDIAN_KEY)")
.addText((text) => {
text.setValue(`${displayName} (${npub})`)
.setDisabled(true);
})
.addButton((button) => {
button.setButtonText("Refresh from Env")
.setCta()
.onClick(async () => {
await this.plugin.loadPrivateKey();
await this.display();
});
});
});
} catch (error: any) {
new Setting(containerEl)
.setName("Private Key Status")
.setDesc(`Error: ${error.message}`)
.addButton((button) => {
button.setButtonText("Refresh from Env")
.setCta()
.onClick(async () => {
await this.plugin.loadPrivateKey();
await this.display();
});
});
}
} else {
new Setting(containerEl)
.setName("Private Key")
.setDesc("No private key found. Set SCRIPTORIUM_OBSIDIAN_KEY environment variable.")
.addButton((button) => {
button.setButtonText("Refresh from Env")
.setCta()
.onClick(async () => {
await this.plugin.loadPrivateKey();
await this.display();
});
});
}
// Default Event Kind
new Setting(containerEl)
@ -125,7 +164,7 @@ export class ScriptoriumSettingTab extends PluginSettingTab { @@ -125,7 +164,7 @@ export class ScriptoriumSettingTab extends PluginSettingTab {
this.plugin.settings.relayList = finalList;
await this.plugin.saveSettings();
this.display();
await this.display();
} catch (error: any) {
alert(`Error fetching relay list: ${error.message}`);
}
@ -152,7 +191,7 @@ export class ScriptoriumSettingTab extends PluginSettingTab { @@ -152,7 +191,7 @@ export class ScriptoriumSettingTab extends PluginSettingTab {
.onClick(async () => {
this.plugin.settings.relayList.splice(index, 1);
await this.plugin.saveSettings();
this.display();
await this.display();
});
});
});
@ -179,7 +218,7 @@ export class ScriptoriumSettingTab extends PluginSettingTab { @@ -179,7 +218,7 @@ export class ScriptoriumSettingTab extends PluginSettingTab {
write: true,
});
await this.plugin.saveSettings();
this.display();
await this.display();
}
}
});

174
src/utils/security.ts

@ -0,0 +1,174 @@ @@ -0,0 +1,174 @@
/**
* Security utilities to prevent private key leaks
*/
/**
* Check if a hex string is likely a private key based on context
* Private keys are 64 hex characters, but so are public keys and event IDs.
* We can only identify private keys by context (error messages, variable names, etc.)
*/
function isLikelyPrivateKey(context: string, hexString: string): boolean {
const lowerContext = context.toLowerCase();
// Check for explicit private key indicators
const privateKeyIndicators = [
'privkey', 'private_key', 'privatekey', 'secret', 'nsec',
'private key', 'secret key', 'signing key'
];
// Check if context mentions private key concepts
for (const indicator of privateKeyIndicators) {
if (lowerContext.includes(indicator)) {
return true;
}
}
// If it's in an error message about keys, it's likely a private key
if (lowerContext.includes('key') &&
(lowerContext.includes('invalid') || lowerContext.includes('error') || lowerContext.includes('failed'))) {
return true;
}
return false;
}
/**
* Sanitize a string to remove any private key patterns
*
* Distinguishes between:
* - Private keys: nsec1... (bech32) or hex in private key context
* - Public keys: npub1... (bech32) or hex (64 chars) - these are SAFE to log
* - Event IDs: hex (64 chars) - these are SAFE to log
*/
export function sanitizeString(str: string): string {
if (!str) return str;
let sanitized = str;
// Remove nsec bech32 keys (nsec1...) - these are ALWAYS private keys
const nsecPattern = /nsec1[a-z0-9]{58,}/gi;
sanitized = sanitized.replace(nsecPattern, "[PRIVATE_KEY_REDACTED]");
// For hex strings (64 chars), only remove if context suggests it's a private key
// Public keys (npub1... or hex) and event IDs (hex) should NOT be removed
const hexPattern = /(?:^|\s|"|'|`)([0-9a-f]{64})(?:\s|"|'|`|$)/gi;
sanitized = sanitized.replace(hexPattern, (match, hexString, offset) => {
// Get surrounding context (50 chars before and after)
const start = Math.max(0, offset - 50);
const end = Math.min(str.length, offset + match.length + 50);
const context = str.substring(start, end);
// Only redact if context suggests it's a private key
if (isLikelyPrivateKey(context, hexString)) {
return match.replace(hexString, "[PRIVATE_KEY_REDACTED]");
}
// Otherwise, it's likely a public key or event ID - keep it
return match;
});
return sanitized;
}
/**
* Sanitize an error object to remove private keys
*/
export function sanitizeError(error: any): any {
if (!error) return error;
// If it's a string, sanitize it
if (typeof error === "string") {
return sanitizeString(error);
}
// If it's an Error object, sanitize the message
if (error instanceof Error) {
const sanitized = new Error(sanitizeString(error.message));
sanitized.name = error.name;
sanitized.stack = error.stack ? sanitizeString(error.stack) : undefined;
return sanitized;
}
// If it's an object, sanitize string properties
if (typeof error === "object") {
const sanitized: any = {};
for (const [key, value] of Object.entries(error)) {
if (typeof value === "string") {
sanitized[key] = sanitizeString(value);
} else if (value instanceof Error) {
sanitized[key] = sanitizeError(value);
} else {
sanitized[key] = value;
}
}
return sanitized;
}
return error;
}
/**
* Safe console.error that never logs private keys
*/
export function safeConsoleError(message: string, ...args: any[]): void {
const sanitizedArgs = args.map(arg => sanitizeError(arg));
console.error(sanitizeString(message), ...sanitizedArgs);
}
/**
* Safe console.log that never logs private keys
*/
export function safeConsoleLog(message: string, ...args: any[]): void {
const sanitizedArgs = args.map(arg => sanitizeError(arg));
console.log(sanitizeString(message), ...sanitizedArgs);
}
/**
* Verify that an event doesn't contain private key in content or tags
*
* Note: Public keys and event IDs are EXPECTED in events and should NOT be flagged.
* Only private keys (nsec1...) should be detected.
*/
export function verifyEventSecurity(event: any): boolean {
if (!event) return false;
// Check content for nsec bech32 private keys
if (event.content && typeof event.content === "string") {
// Only check for nsec1... bech32 private keys
// Public keys (npub1... or hex) and event IDs (hex) are safe
if (event.content.includes("nsec1")) {
return false;
}
// Check for hex strings that might be private keys in content
// This is tricky - we can't distinguish hex private keys from public keys/event IDs
// But if content contains "nsec" or "private key" context, flag it
const contentLower = event.content.toLowerCase();
if ((contentLower.includes("nsec") ||
contentLower.includes("private key") ||
contentLower.includes("privkey")) &&
/[0-9a-f]{64}/i.test(event.content)) {
// Context suggests private key - flag it
return false;
}
}
// Check tags for nsec bech32 private keys
if (event.tags && Array.isArray(event.tags)) {
for (const tag of event.tags) {
if (Array.isArray(tag)) {
for (const value of tag) {
if (typeof value === "string") {
// Only flag nsec1... bech32 private keys
// Public keys in "p" tags and event IDs in "e" tags are EXPECTED
if (value.includes("nsec1")) {
return false;
}
}
}
}
}
}
return true;
}
Loading…
Cancel
Save