Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,10 @@ dist-ssr
.temp-audio/
.temp-audio-transcription/

# Transcription / pipeline intermediate artifacts
# (These may embed Nostr events with upstream URLs; never commit them.)
.episodes-to-transcribe.json
.transcript-mapping.json
.show-notes-mapping.json

# Transcription temp files
9 changes: 0 additions & 9 deletions .show-notes-mapping.json

This file was deleted.

58 changes: 0 additions & 58 deletions .transcript-mapping.json

This file was deleted.

62 changes: 56 additions & 6 deletions scripts/lib/conversion-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,73 @@ import { NSyteBunkerSigner } from './nsyte-bunker-minimal';
import type { NostrEvent } from '@nostrify/nostrify';
import { getPublicKey } from 'nostr-tools';

/**
* Strip Hugging Face access tokens and other bearer tokens from a URL.
* Removes `token=...`, `hf_token=...` query params and any `hf_...` literals.
* Prevents credential leakage into Nostr events and committed JSON artifacts.
*/
export function redactUrlSecrets(url: string): string {
try {
const parsed = new URL(url);
// Drop known token-bearing params from any host.
for (const key of ['token', 'hf_token', 'access_token', 'jwt']) {
parsed.searchParams.delete(key);
}
let cleaned = parsed.toString();
// Final sweep for any inline hf_ literals (defensive).
cleaned = cleaned.replace(/hf_[A-Za-z0-9]{20,}/g, '[REDACTED]');
return cleaned;
} catch {
// Not a parseable URL — scrub token-like literals anyway.
return url.replace(/hf_[A-Za-z0-9]{20,}/g, '[REDACTED]');
}
}

/**
* Recursively redact Hugging Face token literals from any string in a Nostr event
* (tags, content). Used before serializing events to disk to satisfy GitHub
* push protection and avoid leaking credentials to Nostr relays.
*/
export function redactEventSecrets<T extends NostrEvent>(event: T): T {
const scrub = (s: string) => s.replace(/hf_[A-Za-z0-9]{20,}/g, '[REDACTED]');
return {
...event,
content: scrub(event.content),
tags: event.tags.map((tag) =>
tag.map((value) => (typeof value === 'string' ? scrub(redactUrlSecrets(value)) : value)),
),
};
}

export function extractRecordingUrl(livestream: NostrEvent): string | null {
const download = livestream.tags.find(([name]) => name === 'download')?.[1];
if (download) {
console.log('✅ Found download tag (Shoshou recording):', download);
return download;
const safe = redactUrlSecrets(download);
if (safe !== download) {
console.warn('🔐 Stripped credentials from download URL before publishing');
}
console.log('✅ Found download tag (Shoshou recording):', safe);
return safe;
}

const recording = livestream.tags.find(([name]) => name === 'recording')?.[1];
if (recording) {
console.log('✅ Found recording tag:', recording);
return recording;
const safe = redactUrlSecrets(recording);
if (safe !== recording) {
console.warn('🔐 Stripped credentials from recording URL before publishing');
}
console.log('✅ Found recording tag:', safe);
return safe;
}

const streaming = livestream.tags.find(([name]) => name === 'streaming')?.[1];
if (streaming) {
console.warn('⚠️ Using streaming URL instead of recording (quality may be poor):', streaming);
return streaming;
const safe = redactUrlSecrets(streaming);
if (safe !== streaming) {
console.warn('🔐 Stripped credentials from streaming URL before publishing');
}
console.warn('⚠️ Using streaming URL instead of recording (quality may be poor):', safe);
return safe;
}

console.error('❌ No download, recording, or streaming URL found');
Expand Down
12 changes: 10 additions & 2 deletions scripts/transcribe-audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { exec, spawn } from 'child_process';
import { EpisodeMetadata } from './lib/conversion-types';
import type { NostrEvent } from '@nostrify/nostrify';
import { queryRelay } from './lib/relay-query';
import { redactEventSecrets } from './lib/conversion-utils';

interface TranscriptionResult {
dTag: string;
Expand Down Expand Up @@ -488,8 +489,15 @@ async function main() {
console.warn('⚠️ Failed to cleanup temp directory:', error);
}

// Save transcript mapping
await fs.writeFile(TRANSCRIPT_MAPPING_PATH, JSON.stringify(results, null, 2));
// Save transcript mapping.
// Redact any embedded Hugging Face tokens from events before writing to disk
// so credentials never enter the git history (GitHub push protection blocks
// commits containing HF user access tokens).
const sanitized = results.map(({ event, ...rest }) => ({
...rest,
...(event ? { event: redactEventSecrets(event) } : {}),
}));
await fs.writeFile(TRANSCRIPT_MAPPING_PATH, JSON.stringify(sanitized, null, 2));
console.log(`💾 Transcript mapping saved to: ${TRANSCRIPT_MAPPING_PATH}`);

// Log summary
Expand Down
Loading