feat: Zero-Trust DRM - EUPL-1.2 - Viva La OpenSource <3
This commit is contained in:
parent
741bbe11e8
commit
727072e2e5
6 changed files with 1831 additions and 0 deletions
134
examples/encrypt_media/main.go
Normal file
134
examples/encrypt_media/main.go
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
// Package main demonstrates encrypting media files into SMSG format for dapp.fm
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run main.go -input video.mp4 -output video.smsg -password "license-token" -title "My Track" -artist "Artist Name"
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"mime"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Snider/Borg/pkg/smsg"
|
||||
)
|
||||
|
||||
func main() {
|
||||
inputFile := flag.String("input", "", "Input media file (mp4, mp3, etc)")
|
||||
outputFile := flag.String("output", "", "Output SMSG file (default: input.smsg)")
|
||||
password := flag.String("password", "", "License token / password for encryption")
|
||||
title := flag.String("title", "", "Track title (default: filename)")
|
||||
artist := flag.String("artist", "", "Artist name")
|
||||
hint := flag.String("hint", "", "Optional password hint")
|
||||
outputBase64 := flag.Bool("base64", false, "Output as base64 text file instead of binary")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *inputFile == "" {
|
||||
log.Fatal("Input file is required. Use -input flag.")
|
||||
}
|
||||
|
||||
if *password == "" {
|
||||
log.Fatal("Password/license token is required. Use -password flag.")
|
||||
}
|
||||
|
||||
// Read input file
|
||||
data, err := os.ReadFile(*inputFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read input file: %v", err)
|
||||
}
|
||||
|
||||
// Determine MIME type
|
||||
ext := strings.ToLower(filepath.Ext(*inputFile))
|
||||
mimeType := mime.TypeByExtension(ext)
|
||||
if mimeType == "" {
|
||||
// Fallback for common types
|
||||
switch ext {
|
||||
case ".mp4":
|
||||
mimeType = "video/mp4"
|
||||
case ".mp3":
|
||||
mimeType = "audio/mpeg"
|
||||
case ".wav":
|
||||
mimeType = "audio/wav"
|
||||
case ".ogg":
|
||||
mimeType = "audio/ogg"
|
||||
case ".webm":
|
||||
mimeType = "video/webm"
|
||||
case ".m4a":
|
||||
mimeType = "audio/mp4"
|
||||
case ".flac":
|
||||
mimeType = "audio/flac"
|
||||
default:
|
||||
mimeType = "application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
trackTitle := *title
|
||||
if trackTitle == "" {
|
||||
trackTitle = strings.TrimSuffix(filepath.Base(*inputFile), ext)
|
||||
}
|
||||
|
||||
output := *outputFile
|
||||
if output == "" {
|
||||
output = *inputFile + ".smsg"
|
||||
if *outputBase64 {
|
||||
output = *inputFile + ".smsg.txt"
|
||||
}
|
||||
}
|
||||
|
||||
// Create SMSG message with media attachment
|
||||
msg := smsg.NewMessage("Licensed media content from dapp.fm")
|
||||
msg.WithSubject(trackTitle)
|
||||
|
||||
if *artist != "" {
|
||||
msg.WithFrom(*artist)
|
||||
}
|
||||
|
||||
// Add the media file as base64 attachment
|
||||
contentB64 := base64.StdEncoding.EncodeToString(data)
|
||||
msg.AddAttachment(filepath.Base(*inputFile), contentB64, mimeType)
|
||||
|
||||
// Encrypt
|
||||
var encrypted []byte
|
||||
if *hint != "" {
|
||||
encrypted, err = smsg.EncryptWithHint(msg, *password, *hint)
|
||||
} else {
|
||||
encrypted, err = smsg.Encrypt(msg, *password)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("Encryption failed: %v", err)
|
||||
}
|
||||
|
||||
// Write output
|
||||
if *outputBase64 {
|
||||
// Write as base64 text
|
||||
b64 := base64.StdEncoding.EncodeToString(encrypted)
|
||||
err = os.WriteFile(output, []byte(b64), 0644)
|
||||
} else {
|
||||
// Write as binary
|
||||
err = os.WriteFile(output, encrypted, 0644)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to write output file: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Encrypted media created successfully!\n")
|
||||
fmt.Printf(" Input: %s (%s)\n", *inputFile, mimeType)
|
||||
fmt.Printf(" Output: %s\n", output)
|
||||
fmt.Printf(" Title: %s\n", trackTitle)
|
||||
if *artist != "" {
|
||||
fmt.Printf(" Artist: %s\n", *artist)
|
||||
}
|
||||
fmt.Printf(" Size: %.2f MB -> %.2f MB\n",
|
||||
float64(len(data))/1024/1024,
|
||||
float64(len(encrypted))/1024/1024)
|
||||
fmt.Printf("\nLicense token: %s\n", *password)
|
||||
fmt.Printf("\nShare the .smsg file publicly. Only users with the license token can play it.\n")
|
||||
}
|
||||
835
js/borg-stmf/artist-portal.html
Normal file
835
js/borg-stmf/artist-portal.html
Normal file
|
|
@ -0,0 +1,835 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>dapp.fm - Artist Portal</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: linear-gradient(135deg, #0f0f1a 0%, #1a0a2e 50%, #0f1a2e 100%);
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
text-align: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.logo h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #ff006e 0%, #8338ec 50%, #3a86ff 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
|
||||
.logo .tagline {
|
||||
color: #888;
|
||||
font-size: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.artist-badge {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #ff006e, #8338ec);
|
||||
color: #fff;
|
||||
padding: 0.3rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.card h2 .icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #888;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
textarea, input[type="text"], input[type="email"] {
|
||||
width: 100%;
|
||||
padding: 1rem 1.25rem;
|
||||
border: 2px solid rgba(255,255,255,0.1);
|
||||
border-radius: 12px;
|
||||
background: rgba(0,0,0,0.4);
|
||||
color: #fff;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
textarea:focus, input:focus {
|
||||
outline: none;
|
||||
border-color: #8338ec;
|
||||
box-shadow: 0 0 0 4px rgba(131, 56, 236, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 1rem 2rem;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
background: linear-gradient(135deg, #ff006e 0%, #8338ec 100%);
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 20px rgba(255, 0, 110, 0.3);
|
||||
}
|
||||
|
||||
button.primary:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 30px rgba(255, 0, 110, 0.4);
|
||||
}
|
||||
|
||||
button.primary:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
button.secondary:hover {
|
||||
background: rgba(255,255,255,0.15);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-radius: 8px;
|
||||
background: rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.status-indicator .dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-indicator.loading .dot {
|
||||
background: #ffc107;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
.status-indicator.ready .dot {
|
||||
background: #00ff94;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
color: #8338ec;
|
||||
text-decoration: none;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
background: rgba(131, 56, 236, 0.1);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
background: rgba(131, 56, 236, 0.2);
|
||||
}
|
||||
|
||||
/* License generation styles */
|
||||
.license-generator {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 1rem;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.license-list {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.license-item {
|
||||
background: rgba(0,0,0,0.3);
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid rgba(131, 56, 236, 0.2);
|
||||
}
|
||||
|
||||
.license-item.new {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
border-color: rgba(0, 255, 148, 0.5);
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.license-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.license-number {
|
||||
font-size: 0.8rem;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.license-timestamp {
|
||||
font-size: 0.75rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.license-token-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
background: rgba(0, 255, 148, 0.1);
|
||||
border: 1px solid rgba(0, 255, 148, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.license-token-value {
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 1rem;
|
||||
color: #00ff94;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.8rem;
|
||||
background: rgba(0, 255, 148, 0.2);
|
||||
border: 1px solid rgba(0, 255, 148, 0.4);
|
||||
color: #00ff94;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: rgba(0, 255, 148, 0.3);
|
||||
}
|
||||
|
||||
.license-meta {
|
||||
font-size: 0.8rem;
|
||||
color: #888;
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.license-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.license-actions button {
|
||||
padding: 0.6rem 1rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.customer-input {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.customer-input input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Stats */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: rgba(0,0,0,0.3);
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #ff006e, #8338ec);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.8rem;
|
||||
color: #888;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Content preview */
|
||||
.content-preview {
|
||||
background: rgba(0,0,0,0.3);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.content-icon {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.content-info h3 {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.content-info p {
|
||||
font-size: 0.85rem;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.file-input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-input-wrapper input[type="file"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-input-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
border: 2px dashed rgba(255,255,255,0.2);
|
||||
border-radius: 12px;
|
||||
background: rgba(0,0,0,0.2);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.file-input-label:hover {
|
||||
border-color: #8338ec;
|
||||
background: rgba(131, 56, 236, 0.1);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: rgba(131, 56, 236, 0.1);
|
||||
border: 1px solid rgba(131, 56, 236, 0.3);
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.info-box h4 {
|
||||
color: #8338ec;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
font-size: 0.85rem;
|
||||
color: #aaa;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="logo">
|
||||
<h1>dapp.fm</h1>
|
||||
<p class="tagline">Artist Portal</p>
|
||||
<span class="artist-badge">Artist Dashboard</span>
|
||||
</div>
|
||||
|
||||
<nav class="nav-links">
|
||||
<a href="index.html">Form Encryption</a>
|
||||
<a href="support-reply.html">Decrypt Messages</a>
|
||||
<a href="media-player.html">Media Player</a>
|
||||
<a href="artist-portal.html" style="background: rgba(131, 56, 236, 0.3);">Artist Portal</a>
|
||||
</nav>
|
||||
|
||||
<div id="wasm-status" class="status-indicator loading">
|
||||
<span class="dot"></span>
|
||||
<span>Initializing encryption engine...</span>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="stat-licenses">0</div>
|
||||
<div class="stat-label">Licenses Issued</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="stat-revenue">$0</div>
|
||||
<div class="stat-label">Potential Revenue</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">100%</div>
|
||||
<div class="stat-label">Your Cut</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Selection -->
|
||||
<div class="card">
|
||||
<h2><span class="icon">🎵</span> Your Content</h2>
|
||||
|
||||
<div id="content-display" class="content-preview">
|
||||
<div class="content-icon">🎬</div>
|
||||
<div class="content-info">
|
||||
<h3 id="content-title">Demo Track</h3>
|
||||
<p id="content-meta">video/mp4 - Ready for licensing</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-input-wrapper" style="margin-top: 1rem;">
|
||||
<input type="file" id="content-file" accept="audio/*,video/*">
|
||||
<label class="file-input-label">
|
||||
<span>📁</span>
|
||||
<span>Upload your own content (optional)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- License Generator -->
|
||||
<div class="card">
|
||||
<h2><span class="icon">🎫</span> Issue New License</h2>
|
||||
|
||||
<div class="customer-input">
|
||||
<input type="email" id="customer-email" placeholder="Customer email (optional - for your records)">
|
||||
<input type="text" id="license-price" placeholder="Price (e.g., $9.99)" style="max-width: 150px;">
|
||||
</div>
|
||||
|
||||
<div class="license-generator">
|
||||
<div class="input-group" style="margin-bottom: 0;">
|
||||
<label for="custom-token">License Token (auto-generated or custom):</label>
|
||||
<input type="text" id="custom-token" placeholder="Leave blank to auto-generate...">
|
||||
</div>
|
||||
<button id="generate-btn" class="primary" disabled>Generate License</button>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<h4>How It Works</h4>
|
||||
<p>Each license token encrypts your content uniquely. The customer receives the encrypted file + their personal token. Only their token unlocks their copy. You can issue unlimited unique licenses - each one is cryptographically distinct.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Issued Licenses -->
|
||||
<div class="card">
|
||||
<h2><span class="icon">📋</span> Issued Licenses</h2>
|
||||
<div id="license-list" class="license-list">
|
||||
<p style="color: #666; text-align: center; padding: 2rem;">No licenses issued yet. Generate your first one above!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="wasm_exec.js"></script>
|
||||
<script>
|
||||
let wasmReady = false;
|
||||
let contentData = null;
|
||||
let contentName = 'Demo Track';
|
||||
let contentMime = 'video/mp4';
|
||||
let licenses = [];
|
||||
let licenseCounter = 0;
|
||||
|
||||
// Initialize WASM
|
||||
async function initWasm() {
|
||||
const statusEl = document.getElementById('wasm-status');
|
||||
|
||||
try {
|
||||
const go = new Go();
|
||||
const result = await WebAssembly.instantiateStreaming(
|
||||
fetch('stmf.wasm'),
|
||||
go.importObject
|
||||
);
|
||||
go.run(result.instance);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('WASM init timeout')), 5000);
|
||||
|
||||
if (typeof BorgSMSG !== 'undefined' && BorgSMSG.ready) {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener('borgstmf:ready', () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
wasmReady = true;
|
||||
updateStatus(statusEl, 'ready', 'Encryption engine ready');
|
||||
document.getElementById('generate-btn').disabled = false;
|
||||
|
||||
// Load demo content for easy testing
|
||||
await loadDemoContent();
|
||||
|
||||
} catch (err) {
|
||||
updateStatus(statusEl, 'error', 'Failed to load: ' + err.message);
|
||||
console.error('WASM init error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatus(el, status, message) {
|
||||
el.className = 'status-indicator ' + status;
|
||||
while (el.firstChild) el.removeChild(el.firstChild);
|
||||
const dot = document.createElement('span');
|
||||
dot.className = 'dot';
|
||||
const text = document.createElement('span');
|
||||
text.textContent = message;
|
||||
el.appendChild(dot);
|
||||
el.appendChild(text);
|
||||
}
|
||||
|
||||
// Load demo content
|
||||
async function loadDemoContent() {
|
||||
try {
|
||||
// For demo, we'll create a simple placeholder
|
||||
// In production, this would be the artist's actual content
|
||||
contentData = 'DEMO_CONTENT_PLACEHOLDER';
|
||||
contentName = 'Demo Track';
|
||||
contentMime = 'video/mp4';
|
||||
} catch (err) {
|
||||
console.log('Demo content not available, using placeholder');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle file upload
|
||||
document.getElementById('content-file').addEventListener('change', async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const buffer = await file.arrayBuffer();
|
||||
contentData = btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
||||
contentName = file.name;
|
||||
contentMime = file.type || 'application/octet-stream';
|
||||
|
||||
document.getElementById('content-title').textContent = file.name;
|
||||
document.getElementById('content-meta').textContent =
|
||||
contentMime + ' - ' + formatSize(file.size) + ' - Ready for licensing';
|
||||
|
||||
// Update icon based on type
|
||||
const icon = contentMime.startsWith('video/') ? '🎬' :
|
||||
contentMime.startsWith('audio/') ? '🎵' : '📄';
|
||||
document.querySelector('.content-icon').textContent = icon;
|
||||
|
||||
} catch (err) {
|
||||
alert('Failed to load file: ' + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
// Generate unique license token
|
||||
function generateToken() {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
const segments = [];
|
||||
for (let s = 0; s < 4; s++) {
|
||||
let segment = '';
|
||||
for (let i = 0; i < 4; i++) {
|
||||
segment += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
segments.push(segment);
|
||||
}
|
||||
return segments.join('-');
|
||||
}
|
||||
|
||||
// Generate license
|
||||
async function generateLicense() {
|
||||
if (!wasmReady) {
|
||||
alert('Encryption engine not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
const customToken = document.getElementById('custom-token').value.trim();
|
||||
const customerEmail = document.getElementById('customer-email').value.trim();
|
||||
const price = document.getElementById('license-price').value.trim() || '$0.00';
|
||||
|
||||
// Generate or use custom token
|
||||
const token = customToken || generateToken();
|
||||
|
||||
// Create the encrypted content
|
||||
const btn = document.getElementById('generate-btn');
|
||||
btn.textContent = 'Encrypting...';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
// Create message with content
|
||||
const encrypted = await BorgSMSG.encrypt({
|
||||
body: 'Licensed content from dapp.fm',
|
||||
subject: contentName,
|
||||
from: 'Artist',
|
||||
attachments: [{
|
||||
name: contentName.includes('.') ? contentName : contentName + '.mp4',
|
||||
content: contentData || btoa('Demo content - upload your own file for real encryption'),
|
||||
mime: contentMime
|
||||
}]
|
||||
}, token);
|
||||
|
||||
// Add to licenses list
|
||||
licenseCounter++;
|
||||
const license = {
|
||||
id: licenseCounter,
|
||||
token: token,
|
||||
customer: customerEmail,
|
||||
price: price,
|
||||
timestamp: new Date(),
|
||||
encrypted: encrypted,
|
||||
contentName: contentName
|
||||
};
|
||||
licenses.unshift(license);
|
||||
|
||||
// Update UI
|
||||
updateLicenseList();
|
||||
updateStats();
|
||||
|
||||
// Clear inputs
|
||||
document.getElementById('custom-token').value = '';
|
||||
document.getElementById('customer-email').value = '';
|
||||
|
||||
} catch (err) {
|
||||
alert('Encryption failed: ' + err.message);
|
||||
} finally {
|
||||
btn.textContent = 'Generate License';
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Update license list display
|
||||
function updateLicenseList() {
|
||||
const container = document.getElementById('license-list');
|
||||
|
||||
// Clear container
|
||||
while (container.firstChild) {
|
||||
container.removeChild(container.firstChild);
|
||||
}
|
||||
|
||||
if (licenses.length === 0) {
|
||||
const empty = document.createElement('p');
|
||||
empty.style.cssText = 'color: #666; text-align: center; padding: 2rem;';
|
||||
empty.textContent = 'No licenses issued yet. Generate your first one above!';
|
||||
container.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
licenses.forEach((license, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'license-item' + (index === 0 ? ' new' : '');
|
||||
|
||||
// Header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'license-header';
|
||||
|
||||
const number = document.createElement('span');
|
||||
number.className = 'license-number';
|
||||
number.textContent = 'License #' + license.id;
|
||||
|
||||
const timestamp = document.createElement('span');
|
||||
timestamp.className = 'license-timestamp';
|
||||
timestamp.textContent = license.timestamp.toLocaleString();
|
||||
|
||||
header.appendChild(number);
|
||||
header.appendChild(timestamp);
|
||||
|
||||
// Token display
|
||||
const tokenDisplay = document.createElement('div');
|
||||
tokenDisplay.className = 'license-token-display';
|
||||
|
||||
const tokenValue = document.createElement('span');
|
||||
tokenValue.className = 'license-token-value';
|
||||
tokenValue.textContent = license.token;
|
||||
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'copy-btn';
|
||||
copyBtn.textContent = 'Copy';
|
||||
copyBtn.addEventListener('click', () => copyToClipboard(license.token, copyBtn));
|
||||
|
||||
tokenDisplay.appendChild(tokenValue);
|
||||
tokenDisplay.appendChild(copyBtn);
|
||||
|
||||
// Meta info
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'license-meta';
|
||||
|
||||
const customerSpan = document.createElement('span');
|
||||
customerSpan.textContent = license.customer ? 'Customer: ' + license.customer : 'No customer email';
|
||||
|
||||
const priceSpan = document.createElement('span');
|
||||
priceSpan.textContent = 'Price: ' + license.price;
|
||||
|
||||
const contentSpan = document.createElement('span');
|
||||
contentSpan.textContent = 'Content: ' + license.contentName;
|
||||
|
||||
meta.appendChild(customerSpan);
|
||||
meta.appendChild(priceSpan);
|
||||
meta.appendChild(contentSpan);
|
||||
|
||||
// Actions
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'license-actions';
|
||||
|
||||
const downloadBtn = document.createElement('button');
|
||||
downloadBtn.className = 'secondary';
|
||||
downloadBtn.textContent = 'Download Encrypted File';
|
||||
downloadBtn.addEventListener('click', () => downloadEncrypted(license));
|
||||
|
||||
const testBtn = document.createElement('button');
|
||||
testBtn.className = 'secondary';
|
||||
testBtn.textContent = 'Test in Player';
|
||||
testBtn.addEventListener('click', () => testInPlayer(license));
|
||||
|
||||
actions.appendChild(downloadBtn);
|
||||
actions.appendChild(testBtn);
|
||||
|
||||
// Assemble
|
||||
item.appendChild(header);
|
||||
item.appendChild(tokenDisplay);
|
||||
item.appendChild(meta);
|
||||
item.appendChild(actions);
|
||||
|
||||
container.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
// Update stats
|
||||
function updateStats() {
|
||||
document.getElementById('stat-licenses').textContent = licenses.length;
|
||||
|
||||
// Calculate revenue (parse prices)
|
||||
let total = 0;
|
||||
licenses.forEach(l => {
|
||||
const match = l.price.match(/[\d.]+/);
|
||||
if (match) total += parseFloat(match[0]);
|
||||
});
|
||||
document.getElementById('stat-revenue').textContent = '$' + total.toFixed(2);
|
||||
}
|
||||
|
||||
// Copy to clipboard
|
||||
async function copyToClipboard(text, btn) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
const original = btn.textContent;
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(() => btn.textContent = original, 1500);
|
||||
} catch (err) {
|
||||
alert('Failed to copy');
|
||||
}
|
||||
}
|
||||
|
||||
// Download encrypted file
|
||||
function downloadEncrypted(license) {
|
||||
const blob = new Blob([license.encrypted], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = license.contentName.replace(/\.[^.]+$/, '') + '-' + license.id + '.smsg';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// Test in player
|
||||
function testInPlayer(license) {
|
||||
// Store in sessionStorage and redirect
|
||||
sessionStorage.setItem('dappfm-test-content', license.encrypted);
|
||||
sessionStorage.setItem('dappfm-test-token', license.token);
|
||||
window.location.href = 'media-player.html?test=1';
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('generate-btn').addEventListener('click', generateLicense);
|
||||
|
||||
// Initialize
|
||||
initWasm();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1
js/borg-stmf/demo-track.smsg
Normal file
1
js/borg-stmf/demo-track.smsg
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -310,6 +310,8 @@
|
|||
<nav class="nav-links">
|
||||
<a href="index.html" class="active">Form Encryption</a>
|
||||
<a href="support-reply.html">Decrypt Messages</a>
|
||||
<a href="media-player.html">Media Player</a>
|
||||
<a href="artist-portal.html">Artist Portal</a>
|
||||
</nav>
|
||||
|
||||
<div id="wasm-status" class="status-indicator loading">
|
||||
|
|
|
|||
857
js/borg-stmf/media-player.html
Normal file
857
js/borg-stmf/media-player.html
Normal file
|
|
@ -0,0 +1,857 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>dapp.fm - Decentralized Music Player</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: linear-gradient(135deg, #0f0f1a 0%, #1a0a2e 50%, #0f1a2e 100%);
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
text-align: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.logo h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #ff006e 0%, #8338ec 50%, #3a86ff 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
|
||||
.logo .tagline {
|
||||
color: #888;
|
||||
font-size: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.hero-text {
|
||||
text-align: center;
|
||||
margin: 2rem 0;
|
||||
padding: 1.5rem;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.hero-text p {
|
||||
color: #aaa;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.hero-text strong {
|
||||
color: #ff006e;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.card h2 .icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #888;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
textarea, input[type="password"], input[type="text"], input[type="url"] {
|
||||
width: 100%;
|
||||
padding: 1rem 1.25rem;
|
||||
border: 2px solid rgba(255,255,255,0.1);
|
||||
border-radius: 12px;
|
||||
background: rgba(0,0,0,0.4);
|
||||
color: #fff;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
textarea:focus, input:focus {
|
||||
outline: none;
|
||||
border-color: #8338ec;
|
||||
box-shadow: 0 0 0 4px rgba(131, 56, 236, 0.2);
|
||||
}
|
||||
|
||||
textarea.encrypted {
|
||||
min-height: 100px;
|
||||
font-size: 0.75rem;
|
||||
word-break: break-all;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.unlock-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.unlock-row .input-group {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 1rem 2.5rem;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
background: linear-gradient(135deg, #ff006e 0%, #8338ec 100%);
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 20px rgba(255, 0, 110, 0.3);
|
||||
}
|
||||
|
||||
button.primary:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 30px rgba(255, 0, 110, 0.4);
|
||||
}
|
||||
|
||||
button.primary:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
button.secondary:hover {
|
||||
background: rgba(255,255,255,0.15);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-radius: 8px;
|
||||
background: rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.status-indicator .dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-indicator.loading .dot {
|
||||
background: #ffc107;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
.status-indicator.ready .dot {
|
||||
background: #00ff94;
|
||||
}
|
||||
|
||||
.status-indicator.error .dot {
|
||||
background: #ff5252;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
background: rgba(255, 82, 82, 0.15);
|
||||
border: 1px solid rgba(255, 82, 82, 0.4);
|
||||
border-radius: 12px;
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
display: none;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.error-banner.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Media Player Styles */
|
||||
.player-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.player-container.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.track-info {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.track-artwork {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin: 0 auto 1.5rem;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #2d1b4e 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 5rem;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.track-artwork img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.track-artwork video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.track-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.track-artist {
|
||||
color: #888;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.media-player-wrapper {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
/* Custom Audio Player */
|
||||
.audio-player {
|
||||
width: 100%;
|
||||
background: rgba(0,0,0,0.3);
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
audio, video {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
audio::-webkit-media-controls-panel {
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #2d1b4e 100%);
|
||||
}
|
||||
|
||||
video {
|
||||
max-height: 500px;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.video-player-wrapper {
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
/* License Info */
|
||||
.license-info {
|
||||
margin-top: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: rgba(131, 56, 236, 0.1);
|
||||
border: 1px solid rgba(131, 56, 236, 0.3);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.license-info h4 {
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: #8338ec;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.license-info p {
|
||||
font-size: 0.85rem;
|
||||
color: #aaa;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.license-info .license-token {
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.75rem;
|
||||
background: rgba(0,0,0,0.3);
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
margin-top: 0.75rem;
|
||||
word-break: break-all;
|
||||
color: #00ff94;
|
||||
}
|
||||
|
||||
/* Download section */
|
||||
.download-section {
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.download-section button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* How it works */
|
||||
.how-it-works {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.how-step {
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
background: rgba(0,0,0,0.2);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.how-step .step-icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.how-step h4 {
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.how-step p {
|
||||
font-size: 0.8rem;
|
||||
color: #888;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* File input styling */
|
||||
.file-input-wrapper {
|
||||
position: relative;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.file-input-wrapper input[type="file"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-input-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
padding: 2rem;
|
||||
border: 2px dashed rgba(255,255,255,0.2);
|
||||
border-radius: 12px;
|
||||
background: rgba(0,0,0,0.2);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.file-input-label:hover {
|
||||
border-color: #8338ec;
|
||||
background: rgba(131, 56, 236, 0.1);
|
||||
}
|
||||
|
||||
.file-input-label .icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.or-divider {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
margin: 1rem 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
color: #8338ec;
|
||||
text-decoration: none;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
background: rgba(131, 56, 236, 0.1);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
background: rgba(131, 56, 236, 0.2);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="logo">
|
||||
<h1>dapp.fm</h1>
|
||||
<p class="tagline">Decentralized Music Distribution</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-links">
|
||||
<a href="index.html">Form Encryption</a>
|
||||
<a href="support-reply.html">Decrypt Messages</a>
|
||||
<a href="media-player.html" style="background: rgba(131, 56, 236, 0.3);">Media Player</a>
|
||||
<a href="artist-portal.html">Artist Portal</a>
|
||||
</nav>
|
||||
|
||||
<div class="hero-text">
|
||||
<p>
|
||||
<strong>No middlemen. No platforms. No 70% cuts.</strong><br>
|
||||
Artists encrypt their music with ChaCha20-Poly1305. Fans unlock with a license token.
|
||||
Content lives on any CDN, IPFS, or artist's own server. The password IS the license.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="wasm-status" class="status-indicator loading">
|
||||
<span class="dot"></span>
|
||||
<span>Initializing decryption engine...</span>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2><span class="icon">🔐</span> Unlock Licensed Content</h2>
|
||||
|
||||
<div class="file-input-wrapper">
|
||||
<input type="file" id="file-input" accept=".smsg,.enc,.borg">
|
||||
<label class="file-input-label">
|
||||
<span class="icon">📁</span>
|
||||
<span>Drop encrypted file here or click to browse</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="or-divider">- or paste encrypted content -</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="encrypted-content">Encrypted Content (base64):</label>
|
||||
<textarea id="encrypted-content" class="encrypted" placeholder="Paste the encrypted content from the artist..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="demo-banner" style="background: rgba(255, 0, 110, 0.1); border: 1px solid rgba(255, 0, 110, 0.3); border-radius: 12px; padding: 1rem; margin-bottom: 1rem;">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 1rem;">
|
||||
<div>
|
||||
<strong style="color: #ff006e;">Try the Demo!</strong>
|
||||
<span style="color: #888; font-size: 0.85rem; margin-left: 0.5rem;">Load a sample encrypted video</span>
|
||||
</div>
|
||||
<button id="load-demo-btn" class="secondary" style="padding: 0.6rem 1.2rem; font-size: 0.85rem;">Load Demo Track</button>
|
||||
</div>
|
||||
<div style="font-size: 0.8rem; color: #666; margin-top: 0.5rem;">
|
||||
Password: <code style="background: rgba(0,0,0,0.3); padding: 0.2rem 0.5rem; border-radius: 4px; color: #00ff94;">dapp-fm-2024</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="error-banner" class="error-banner"></div>
|
||||
|
||||
<div class="unlock-row">
|
||||
<div class="input-group">
|
||||
<label for="license-token">License Token (Password):</label>
|
||||
<input type="password" id="license-token" placeholder="Enter your license token from the artist">
|
||||
</div>
|
||||
<button id="unlock-btn" class="primary" disabled>Unlock</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Player appears after unlock -->
|
||||
<div id="player-container" class="card player-container">
|
||||
<h2><span class="icon">🎵</span> Now Playing</h2>
|
||||
|
||||
<div class="track-info">
|
||||
<div class="track-artwork" id="track-artwork">🎶</div>
|
||||
<div class="track-title" id="track-title">Track Title</div>
|
||||
<div class="track-artist" id="track-artist">Artist Name</div>
|
||||
</div>
|
||||
|
||||
<div class="media-player-wrapper" id="media-player-wrapper">
|
||||
<!-- Audio/Video player inserted here -->
|
||||
</div>
|
||||
|
||||
<div class="license-info">
|
||||
<h4>🔓 Licensed Content</h4>
|
||||
<p>This content was unlocked with your personal license token.
|
||||
The decryption happens entirely in your browser - no servers involved.</p>
|
||||
<div class="license-token" id="license-display"></div>
|
||||
</div>
|
||||
|
||||
<div class="download-section">
|
||||
<button class="secondary" id="download-btn">Download Original</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2><span class="icon">💡</span> How It Works</h2>
|
||||
<div class="how-it-works">
|
||||
<div class="how-step">
|
||||
<div class="step-icon">🎤</div>
|
||||
<h4>Artist Creates</h4>
|
||||
<p>Artist encrypts their music/video with a password using ChaCha20-Poly1305</p>
|
||||
</div>
|
||||
<div class="how-step">
|
||||
<div class="step-icon">☁️</div>
|
||||
<h4>Host Anywhere</h4>
|
||||
<p>Encrypted file goes on any CDN, IPFS, S3, or artist's own website</p>
|
||||
</div>
|
||||
<div class="how-step">
|
||||
<div class="step-icon">🎫</div>
|
||||
<h4>Sell License</h4>
|
||||
<p>Artist sells the password (license token) directly to fans</p>
|
||||
</div>
|
||||
<div class="how-step">
|
||||
<div class="step-icon">🎧</div>
|
||||
<h4>Fan Plays</h4>
|
||||
<p>Fan unlocks and plays in browser. No middleman, no platform fees</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="wasm_exec.js"></script>
|
||||
<script>
|
||||
let wasmReady = false;
|
||||
let currentMediaBlob = null;
|
||||
let currentMediaName = null;
|
||||
let currentMediaMime = null;
|
||||
|
||||
// Initialize WASM
|
||||
async function initWasm() {
|
||||
const statusEl = document.getElementById('wasm-status');
|
||||
|
||||
try {
|
||||
const go = new Go();
|
||||
const result = await WebAssembly.instantiateStreaming(
|
||||
fetch('stmf.wasm'),
|
||||
go.importObject
|
||||
);
|
||||
go.run(result.instance);
|
||||
|
||||
// Wait for BorgSMSG to be ready
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('WASM init timeout')), 5000);
|
||||
|
||||
if (typeof BorgSMSG !== 'undefined' && BorgSMSG.ready) {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener('borgstmf:ready', () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
wasmReady = true;
|
||||
updateStatus(statusEl, 'ready', 'Decryption engine ready (v' + BorgSMSG.version + ')');
|
||||
document.getElementById('unlock-btn').disabled = false;
|
||||
|
||||
} catch (err) {
|
||||
updateStatus(statusEl, 'error', 'Failed to load: ' + err.message);
|
||||
console.error('WASM init error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatus(el, status, message) {
|
||||
el.className = 'status-indicator ' + status;
|
||||
while (el.firstChild) el.removeChild(el.firstChild);
|
||||
const dot = document.createElement('span');
|
||||
dot.className = 'dot';
|
||||
const text = document.createElement('span');
|
||||
text.textContent = message;
|
||||
el.appendChild(dot);
|
||||
el.appendChild(text);
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
const errorBanner = document.getElementById('error-banner');
|
||||
errorBanner.textContent = msg;
|
||||
errorBanner.classList.add('visible');
|
||||
}
|
||||
|
||||
function hideError() {
|
||||
document.getElementById('error-banner').classList.remove('visible');
|
||||
}
|
||||
|
||||
// Handle file input
|
||||
document.getElementById('file-input').addEventListener('change', async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const content = await file.arrayBuffer();
|
||||
const base64 = btoa(String.fromCharCode(...new Uint8Array(content)));
|
||||
document.getElementById('encrypted-content').value = base64;
|
||||
} catch (err) {
|
||||
showError('Failed to read file: ' + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Unlock content
|
||||
async function unlockContent() {
|
||||
hideError();
|
||||
|
||||
const encryptedB64 = document.getElementById('encrypted-content').value.trim();
|
||||
const password = document.getElementById('license-token').value;
|
||||
|
||||
if (!encryptedB64) {
|
||||
showError('Please provide encrypted content');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
showError('Please enter your license token');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const message = await BorgSMSG.decrypt(encryptedB64, password);
|
||||
displayMedia(message, password);
|
||||
} catch (err) {
|
||||
showError('Unlock failed: Invalid license token or corrupted content');
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Display media player
|
||||
function displayMedia(msg, password) {
|
||||
const playerContainer = document.getElementById('player-container');
|
||||
const mediaWrapper = document.getElementById('media-player-wrapper');
|
||||
const artworkEl = document.getElementById('track-artwork');
|
||||
|
||||
// Set track info
|
||||
document.getElementById('track-title').textContent = msg.subject || 'Untitled Track';
|
||||
document.getElementById('track-artist').textContent = msg.from || 'Unknown Artist';
|
||||
|
||||
// Show partial license token (safely using textContent)
|
||||
const maskedToken = password.substring(0, 4) + String.fromCharCode(0x2022).repeat(8) + password.substring(password.length - 4);
|
||||
document.getElementById('license-display').textContent = maskedToken;
|
||||
|
||||
// Clear previous media safely
|
||||
while (mediaWrapper.firstChild) {
|
||||
mediaWrapper.removeChild(mediaWrapper.firstChild);
|
||||
}
|
||||
|
||||
// Reset artwork to default
|
||||
while (artworkEl.firstChild) {
|
||||
artworkEl.removeChild(artworkEl.firstChild);
|
||||
}
|
||||
artworkEl.textContent = '';
|
||||
|
||||
// Find media attachments
|
||||
if (msg.attachments && msg.attachments.length > 0) {
|
||||
msg.attachments.forEach((att, index) => {
|
||||
const mime = att.mime || 'application/octet-stream';
|
||||
|
||||
// Convert base64 to blob
|
||||
const binary = atob(att.content);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
const blob = new Blob([bytes], { type: mime });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Store for download
|
||||
currentMediaBlob = blob;
|
||||
currentMediaName = att.name;
|
||||
currentMediaMime = mime;
|
||||
|
||||
if (mime.startsWith('video/')) {
|
||||
// Video player
|
||||
const videoWrapper = document.createElement('div');
|
||||
videoWrapper.className = 'video-player-wrapper';
|
||||
|
||||
const video = document.createElement('video');
|
||||
video.controls = true;
|
||||
video.autoplay = false;
|
||||
video.src = url;
|
||||
video.style.width = '100%';
|
||||
|
||||
videoWrapper.appendChild(video);
|
||||
mediaWrapper.appendChild(videoWrapper);
|
||||
|
||||
// Update artwork to show video icon
|
||||
artworkEl.textContent = '🎬';
|
||||
|
||||
} else if (mime.startsWith('audio/')) {
|
||||
// Audio player
|
||||
const audioWrapper = document.createElement('div');
|
||||
audioWrapper.className = 'audio-player';
|
||||
|
||||
const audio = document.createElement('audio');
|
||||
audio.controls = true;
|
||||
audio.autoplay = false;
|
||||
audio.src = url;
|
||||
audio.style.width = '100%';
|
||||
|
||||
audioWrapper.appendChild(audio);
|
||||
mediaWrapper.appendChild(audioWrapper);
|
||||
|
||||
artworkEl.textContent = '🎵';
|
||||
|
||||
} else if (mime.startsWith('image/')) {
|
||||
// Image - use as artwork (safe - just setting src)
|
||||
const img = document.createElement('img');
|
||||
img.src = url;
|
||||
img.alt = 'Album artwork';
|
||||
artworkEl.textContent = '';
|
||||
artworkEl.appendChild(img);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show player
|
||||
playerContainer.classList.add('visible');
|
||||
playerContainer.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// Download handler
|
||||
document.getElementById('download-btn').addEventListener('click', () => {
|
||||
if (!currentMediaBlob) {
|
||||
alert('No media to download');
|
||||
return;
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(currentMediaBlob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = currentMediaName || 'media';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
// Load demo content
|
||||
async function loadDemo() {
|
||||
const btn = document.getElementById('load-demo-btn');
|
||||
const originalText = btn.textContent;
|
||||
btn.textContent = 'Loading...';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('demo-track.smsg');
|
||||
if (!response.ok) {
|
||||
throw new Error('Demo file not found');
|
||||
}
|
||||
const content = await response.text();
|
||||
document.getElementById('encrypted-content').value = content;
|
||||
document.getElementById('license-token').value = 'dapp-fm-2024';
|
||||
btn.textContent = 'Demo Loaded!';
|
||||
setTimeout(() => {
|
||||
btn.textContent = originalText;
|
||||
btn.disabled = false;
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
showError('Failed to load demo: ' + err.message);
|
||||
btn.textContent = originalText;
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('unlock-btn').addEventListener('click', unlockContent);
|
||||
document.getElementById('license-token').addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') unlockContent();
|
||||
});
|
||||
document.getElementById('load-demo-btn').addEventListener('click', loadDemo);
|
||||
|
||||
// Check for test mode from Artist Portal
|
||||
function checkTestMode() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('test') === '1') {
|
||||
const content = sessionStorage.getItem('dappfm-test-content');
|
||||
const token = sessionStorage.getItem('dappfm-test-token');
|
||||
if (content && token) {
|
||||
document.getElementById('encrypted-content').value = content;
|
||||
document.getElementById('license-token').value = token;
|
||||
sessionStorage.removeItem('dappfm-test-content');
|
||||
sessionStorage.removeItem('dappfm-test-token');
|
||||
// Auto-unlock after WASM is ready
|
||||
const checkReady = setInterval(() => {
|
||||
if (wasmReady) {
|
||||
clearInterval(checkReady);
|
||||
unlockContent();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
initWasm();
|
||||
checkTestMode();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -389,6 +389,8 @@
|
|||
<nav class="nav-links">
|
||||
<a href="index.html">Form Encryption</a>
|
||||
<a href="support-reply.html" class="active">Decrypt Messages</a>
|
||||
<a href="media-player.html">Media Player</a>
|
||||
<a href="artist-portal.html">Artist Portal</a>
|
||||
</nav>
|
||||
|
||||
<div id="wasm-status" class="status-indicator loading">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue