Import existing project
This commit is contained in:
parent
7887817595
commit
80b0cc4939
125 changed files with 16980 additions and 0 deletions
224
develop/html/ai-san.html
Normal file
224
develop/html/ai-san.html
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Text Cleaner - caileb.com</title>
|
||||
<meta name="description" content="Clean and convert formatted text to plain text">
|
||||
<link rel="icon" href="/images/favi.png" type="image/png">
|
||||
<link rel="apple-touch-icon" href="/images/favi.png">
|
||||
<link rel="shortcut icon" href="/images/favi.png">
|
||||
<link rel="preload" href="/webfonts/Poppins-Regular.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/webfonts/Poppins-SemiBold.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="/css/u.css">
|
||||
<style>
|
||||
:root {
|
||||
--background-color: #121212;
|
||||
--card-gradient-start: #1e1e1e;
|
||||
--card-gradient-end: #333;
|
||||
--header-background: #262626;
|
||||
--text-color: #fff;
|
||||
--accent-color: #9B59B6;
|
||||
--subtext-color: #ccc;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #1c1c1c;
|
||||
color: var(--text-color);
|
||||
font-family: Poppins, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 1.25rem;
|
||||
color: var(--subtext-color);
|
||||
}
|
||||
|
||||
.cleaner-card {
|
||||
background: linear-gradient(135deg, rgba(30, 30, 30, 0.8), rgba(51, 51, 51, 0.8));
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
margin-bottom: 0.8rem;
|
||||
color: var(--accent-color);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem;
|
||||
font-size: 1rem;
|
||||
font-family: Poppins, sans-serif;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #444;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
color: var(--text-color);
|
||||
resize: vertical;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
background: var(--accent-color);
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-family: Poppins, sans-serif;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #8e44ad;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(155, 89, 182, 0.4);
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 1rem 0;
|
||||
color: var(--subtext-color);
|
||||
font-size: 0.9rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
</style>
|
||||
<script src="/js/u.js" async></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>Text Cleaner</h1>
|
||||
<p>Convert formatted text to clean plain text</p>
|
||||
</header>
|
||||
|
||||
<div class="cleaner-card">
|
||||
<label for="inputText">Input Text</label>
|
||||
<textarea id="inputText" placeholder="Paste text with smart quotes, dashes, or other special characters here..."></textarea>
|
||||
|
||||
<div class="button-row">
|
||||
<button onclick="cleanText()">Clean Text</button>
|
||||
<button onclick="copyToClipboard()">Copy to Clipboard</button>
|
||||
</div>
|
||||
|
||||
<label for="outputText">Clean Text</label>
|
||||
<textarea id="outputText" readonly placeholder="Cleaned text will appear here..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script async>
|
||||
function cleanText() {
|
||||
const input = document.getElementById('inputText').value;
|
||||
let cleaned = input;
|
||||
|
||||
// Fix all quotes
|
||||
const quoteReplacements = {
|
||||
'\u201C': '"', '\u201D': '"',
|
||||
'\u2018': "'", '\u2019': "'",
|
||||
'\u00AB': '"', '\u00BB': '"',
|
||||
'\u2039': "'", '\u203A': "'"
|
||||
};
|
||||
|
||||
cleaned = cleaned.replace(/[\u201C\u201D\u2018\u2019\u00AB\u00BB\u2039\u203A]/g, function(char) {
|
||||
return quoteReplacements[char] || char;
|
||||
});
|
||||
|
||||
// Fix all dashes
|
||||
const dashReplacements = {
|
||||
'\u2013': '-', '\u2014': '-',
|
||||
'\u2010': '-', '\u2011': '-',
|
||||
'\u2012': '-', '\u2015': '-',
|
||||
'\u2212': '-'
|
||||
};
|
||||
|
||||
cleaned = cleaned.replace(/[\u2013\u2014\u2010\u2011\u2012\u2015\u2212]/g, function(char) {
|
||||
return dashReplacements[char] || char;
|
||||
});
|
||||
|
||||
// Fix ellipses
|
||||
cleaned = cleaned.replace(/\u2026/g, '...');
|
||||
|
||||
// Normalize line breaks (but preserve them)
|
||||
cleaned = cleaned.replace(/\r\n|\r/g, '\n');
|
||||
|
||||
// Remove extra spaces within each line but preserve newlines
|
||||
cleaned = cleaned.split('\n').map(line => line.replace(/\s+/g, ' ').trim()).join('\n');
|
||||
|
||||
// Remove lines that are just whitespace
|
||||
cleaned = cleaned.replace(/^\s*[\r\n]/gm, '\n');
|
||||
|
||||
// Fix other common characters
|
||||
cleaned = cleaned.replace(/[\u00B6\u00A7]/g, ''); // Remove paragraph and section marks
|
||||
cleaned = cleaned.replace(/[\u00AE\u00A9\u2122]/g, ''); // Remove trademark symbols
|
||||
cleaned = cleaned.replace(/[\u2022\u25E6\u2023\u2043]/g, '-'); // Replace bullets with hyphens
|
||||
|
||||
document.getElementById('outputText').value = cleaned;
|
||||
}
|
||||
|
||||
function copyToClipboard() {
|
||||
const outputText = document.getElementById('outputText');
|
||||
|
||||
if (!outputText.value) {
|
||||
cleanText();
|
||||
}
|
||||
|
||||
outputText.select();
|
||||
document.execCommand('copy');
|
||||
|
||||
const copyButton = document.getElementsByTagName('button')[1];
|
||||
copyButton.textContent = "Copied!";
|
||||
|
||||
setTimeout(function() {
|
||||
copyButton.textContent = "Copy to Clipboard";
|
||||
}, 1500);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
743
develop/html/checkpoint.html
Normal file
743
develop/html/checkpoint.html
Normal file
|
|
@ -0,0 +1,743 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset=UTF-8>
|
||||
<meta http-equiv=X-UA-Compatible content="IE=edge">
|
||||
<meta name=viewport content="width=device-width,initial-scale=1">
|
||||
<title>Checkpoint Documentation</title>
|
||||
<meta name=description content="Documentation for the Checkpoint Protection System, a secure Proof-of-Work solution to prevent automated abuse.">
|
||||
<link rel=icon href=/images/favi.png type=image/png>
|
||||
<link rel=apple-touch-icon href=/images/favi.png>
|
||||
<link rel="shortcut icon" href=/images/favi.png>
|
||||
<link rel=preload href=/webfonts/Poppins-Regular.woff2 as=font type=font/woff2 crossorigin>
|
||||
<link rel=preload href=/webfonts/Poppins-SemiBold.woff2 as=font type=font/woff2 crossorigin>
|
||||
<link rel="preload" href="/js/u.js" as="script">
|
||||
<link rel="stylesheet" href="/css/u.css">
|
||||
<link rel="stylesheet" href="/css/docs.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/@speed-highlight/core@1.2.7/dist/themes/github-dark.css">
|
||||
<style>
|
||||
:root {
|
||||
--background-color: #1a1a1a;
|
||||
--overlay-bg: rgba(28, 28, 28, 0.95);
|
||||
--text-color: #fff;
|
||||
--subtext-color: #ccc;
|
||||
--accent-color: #9B59B6;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: rgba(30, 30, 30, 0.85);
|
||||
backdrop-filter: blur(8px);
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
margin: 40px auto;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3), 0 1px 2px rgba(155,89,182,0.2);
|
||||
max-width: 960px;
|
||||
}
|
||||
</style>
|
||||
<script async src=/js/u.js></script>
|
||||
<script type="module" src="/js/docs.js"></script>
|
||||
<script async src="/js/lightbox.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<div class="disclaimer note">
|
||||
<p><strong>Disclaimer:</strong> Some internal fields and implementation details are omitted here for security reasons.</p>
|
||||
</div>
|
||||
<h1>Checkpoint Protection System</h1>
|
||||
|
||||
<div class="toc">
|
||||
<h2>Contents</h2>
|
||||
<ul>
|
||||
<li><a href="#overview">Overview</a></li>
|
||||
<li><a href="#how-it-works">How It Works</a></li>
|
||||
<li><a href="#challenge-generation">Challenge Generation</a></li>
|
||||
<li><a href="#proof-verification">Proof Verification</a></li>
|
||||
<li><a href="#token-structure">Token Structure</a></li>
|
||||
<li><a href="#security-features">Security Features</a></li>
|
||||
<li><a href="#configuration">Configuration Options</a></li>
|
||||
<li><a href="#middleware">Middleware Integration</a></li>
|
||||
<li><a href="#client-side">Client-Side Implementation</a></li>
|
||||
<li><a href="#api-endpoints">API Endpoints</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<section id="overview" class="section">
|
||||
<h2>Overview</h2>
|
||||
<p>Checkpoint Protection asks visitors to solve a quick puzzle before letting them through, cutting down on automated traffic while keeping the experience smooth for real users.</p>
|
||||
<ul>
|
||||
<li>No account or personal data needed</li>
|
||||
<li>Privacy-focused and lightweight</li>
|
||||
<li>Blocks bots and scripts effectively</li>
|
||||
<li>Works seamlessly in modern browsers</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="how-it-works" class="section">
|
||||
<h2>How It Works</h2>
|
||||
<p>When you navigate to a protected page, the middleware checks for a valid token cookie (<code>__Host-checkpoint_token</code>).</p>
|
||||
<ol>
|
||||
<li>If the token is present, the server verifies its signature and confirms it's bound to your device.</li>
|
||||
<li>Missing or invalid tokens trigger an interstitial page with a request ID.</li>
|
||||
<li>The browser fetches challenge data from <code>/api/pow/challenge?id=REQUEST_ID</code>. This payload includes a random challenge, salt, difficulty, and hidden parameters.</li>
|
||||
<li>The client runs two proofs in parallel:
|
||||
<ul>
|
||||
<li><strong>Proof of Work:</strong> finds a nonce such that <code>SHA‑256(challenge + salt + nonce)</code> meets the difficulty.</li>
|
||||
<li><strong>Proof of Space:</strong> allocates and hashes large memory buffers to confirm resource availability.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Results are sent to <code>/api/pow/verify</code> along with the request ID.</li>
|
||||
<li>On success, the server issues a signed token (valid for 24h) and sets it as a cookie for future visits.</li>
|
||||
</ol>
|
||||
|
||||
<div class="diagram">
|
||||
<h3>Checkpoint Protection Flow</h3>
|
||||
<img src="/images/Basic-POW-Overview.excalidraw.svg" alt="Checkpoint Protection Flow Diagram" id="flowDiagram" loading="lazy">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="challenge-generation" class="section">
|
||||
<h2>Challenge Generation</h2>
|
||||
<p>
|
||||
Challenges are generated using cryptographically secure random bytes combined with a salt for additional entropy:
|
||||
</p>
|
||||
<div class="code-example">
|
||||
<span class="code-label">Go</span>
|
||||
<pre><code>func generateChallenge() (string, string) {
|
||||
// Generate a random challenge
|
||||
randomBytes := make([]byte, 16)
|
||||
_, err := cryptorand.Read(randomBytes)
|
||||
if err != nil {
|
||||
log.Fatalf("CRITICAL: Failed to generate secure random challenge: %v", err)
|
||||
}
|
||||
|
||||
// Generate a random salt for additional entropy
|
||||
saltBytes := make([]byte, saltLength)
|
||||
_, err = cryptorand.Read(saltBytes)
|
||||
if err != nil {
|
||||
log.Fatalf("CRITICAL: Failed to generate secure random salt: %v", err)
|
||||
}
|
||||
|
||||
return hex.EncodeToString(randomBytes), hex.EncodeToString(saltBytes)
|
||||
}</code></pre>
|
||||
</div>
|
||||
<div class="note">
|
||||
<p>
|
||||
<strong>Security Note:</strong> The system uses Go's crypto/rand package for secure random number generation, ensuring challenges cannot be predicted even by sophisticated attackers.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3>Challenge Parameters</h3>
|
||||
<p>
|
||||
Challenges are stored with a unique request ID and include parameters for verification:
|
||||
</p>
|
||||
<div class="code-example">
|
||||
<span class="code-label">Go</span>
|
||||
<pre><code>type ChallengeParams struct {
|
||||
Challenge string `json:"challenge"` // Base64 encoded
|
||||
Salt string `json:"salt"` // Base64 encoded
|
||||
Difficulty int `json:"difficulty"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
ClientIP string `json:"-"`
|
||||
PoSSeed string `json:"pos_seed"` // Hex encoded
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
When a client requests a challenge, the parameters are delivered in an obfuscated format to prevent automated analysis:
|
||||
</p>
|
||||
<div class="code-example">
|
||||
<span class="code-label">JSON</span>
|
||||
<pre><code>{
|
||||
"a": "base64-encoded-challenge",
|
||||
"b": "base64-encoded-salt",
|
||||
"c": 4,
|
||||
"d": "hex-encoded-pos-seed"
|
||||
}</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="proof-verification" class="section">
|
||||
<h2>Proof Verification</h2>
|
||||
<p>
|
||||
The system performs a two-step verification process:
|
||||
</p>
|
||||
|
||||
<h3>1. Computational Proof (Proof of Work)</h3>
|
||||
<p>
|
||||
Verification checks that the hash of the challenge, salt, and nonce combination has the required number of leading zeros:
|
||||
</p>
|
||||
<div class="code-example">
|
||||
<span class="code-label">Go</span>
|
||||
<pre><code>func verifyProofOfWork(challenge, salt, nonce string, difficulty int) bool {
|
||||
input := challenge + salt + nonce
|
||||
hash := calculateHash(input)
|
||||
|
||||
// Check if the hash has the required number of leading zeros
|
||||
prefix := strings.Repeat("0", difficulty)
|
||||
return strings.HasPrefix(hash, prefix)
|
||||
}
|
||||
|
||||
func calculateHash(input string) string {
|
||||
hash := sha256.Sum256([]byte(input))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>2. Memory Proof (Proof of Space)</h3>
|
||||
<p>
|
||||
In addition to the computational work, clients must prove they can allocate and manipulate significant memory resources:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Clients allocate between 48MB to 160MB of memory (size determined by the PoS seed)</li>
|
||||
<li>Client divides memory into 4-8 chunks and performs deterministic filling operations</li>
|
||||
<li>The process is run three times, hashing the entire buffer each time</li>
|
||||
<li>The resulting hashes and execution times are submitted for verification</li>
|
||||
</ul>
|
||||
<p>
|
||||
The server verifies:
|
||||
</p>
|
||||
<ul>
|
||||
<li>All three hashes are identical (proving deterministic execution)</li>
|
||||
<li>Each hash is 64 characters (valid SHA-256)</li>
|
||||
<li>Execution times are consistent (within 20% variation)</li>
|
||||
</ul>
|
||||
<div class="note">
|
||||
<p>
|
||||
The dual-verification approach makes the system resistant to specialized hardware acceleration. While the computational proof can be solved by ASICs or GPUs, the memory proof is specifically designed to be inefficient on such hardware.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="token-structure" class="section">
|
||||
<h2>Token Structure</h2>
|
||||
<p>
|
||||
Checkpoint tokens contain various fields for security and binding:
|
||||
</p>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Purpose</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Nonce</td>
|
||||
<td>The solution to the challenge</td>
|
||||
<td>Verification proof</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ExpiresAt</td>
|
||||
<td>Token expiration timestamp</td>
|
||||
<td>Enforces time-limited access (24 hours)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ClientIP</td>
|
||||
<td>Hashed full client IP</td>
|
||||
<td>Device binding (first 8 bytes of SHA-256)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>UserAgent</td>
|
||||
<td>Hashed user agent</td>
|
||||
<td>Browser binding</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>BrowserHint</td>
|
||||
<td>Derived from Sec-CH-UA headers</td>
|
||||
<td>Additional client identity verification</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Entropy</td>
|
||||
<td>Random data</td>
|
||||
<td>Prevents token prediction/correlation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Created</td>
|
||||
<td>Token creation timestamp</td>
|
||||
<td>Token age tracking</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LastVerified</td>
|
||||
<td>Last verification timestamp</td>
|
||||
<td>Token usage tracking</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Signature</td>
|
||||
<td>HMAC signature</td>
|
||||
<td>Prevents token forgery</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>TokenFormat</td>
|
||||
<td>Version number</td>
|
||||
<td>Backward compatibility support</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="code-example">
|
||||
<span class="code-label">Go</span>
|
||||
<pre><code>type CheckpointToken struct {
|
||||
Nonce string `json:"g"` // Nonce
|
||||
ExpiresAt time.Time `json:"exp"`
|
||||
ClientIP string `json:"cip,omitempty"`
|
||||
UserAgent string `json:"ua,omitempty"`
|
||||
BrowserHint string `json:"bh,omitempty"`
|
||||
Entropy string `json:"ent,omitempty"`
|
||||
Created time.Time `json:"crt"`
|
||||
LastVerified time.Time `json:"lvf,omitempty"`
|
||||
Signature string `json:"sig,omitempty"`
|
||||
TokenFormat int `json:"fmt"`
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>Token Security</h3>
|
||||
<p>
|
||||
Every token is cryptographically signed using HMAC-SHA256 with a server-side secret:
|
||||
</p>
|
||||
<div class="code-example">
|
||||
<span class="code-label">Go</span>
|
||||
<pre><code>func computeTokenSignature(token CheckpointToken, tokenBytes []byte) string {
|
||||
tokenCopy := token
|
||||
tokenCopy.Signature = "" // Ensure signature field is empty for signing
|
||||
tokenToSign, _ := json.Marshal(tokenCopy)
|
||||
h := hmac.New(sha256.New, hmacSecret)
|
||||
h.Write(tokenToSign)
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func verifyTokenSignature(token CheckpointToken, tokenBytes []byte) bool {
|
||||
if token.Signature == "" {
|
||||
return false
|
||||
}
|
||||
expectedSignature := computeTokenSignature(token, tokenBytes)
|
||||
return hmac.Equal([]byte(token.Signature), []byte(expectedSignature))
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>Token Storage</h3>
|
||||
<p>
|
||||
Successfully verified tokens are stored in a persistent store for faster validation:
|
||||
</p>
|
||||
<div class="code-example">
|
||||
<span class="code-label">Go</span>
|
||||
<pre><code>// TokenStore manages persistent storage of verified tokens
|
||||
type TokenStore struct {
|
||||
VerifiedTokens map[string]time.Time `json:"verified_tokens"`
|
||||
Mutex sync.RWMutex `json:"-"`
|
||||
FilePath string `json:"-"`
|
||||
}
|
||||
|
||||
// Each token is identified by a unique hash
|
||||
func calculateTokenHash(token CheckpointToken) string {
|
||||
data := fmt.Sprintf("%s:%s:%d",
|
||||
token.Nonce, // Use nonce as part of the key
|
||||
token.Entropy, // Use entropy as part of the key
|
||||
token.Created.UnixNano()) // Use creation time
|
||||
hash := sha256.Sum256([]byte(data))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="security-features" class="section">
|
||||
<h2>Security Features</h2>
|
||||
|
||||
<div class="security">
|
||||
<h3>Anti-Forgery Protections</h3>
|
||||
<ul>
|
||||
<li><strong>HMAC Signatures:</strong> Each token is cryptographically signed using HMAC-SHA256 to prevent tampering</li>
|
||||
<li><strong>Token Binding:</strong> Tokens are bound to client properties (hashed full IP, hashed user agent, browser client hints)</li>
|
||||
<li><strong>Random Entropy:</strong> Each token contains unique entropy to prevent token prediction or correlation</li>
|
||||
<li><strong>Format Versioning:</strong> Tokens include a format version to support evolving security requirements</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="security">
|
||||
<h3>Replay Prevention</h3>
|
||||
<ul>
|
||||
<li><strong>Nonce Tracking:</strong> Used nonces are tracked to prevent replay attacks</li>
|
||||
<li><strong>Expiration Times:</strong> All tokens and challenges have expiration times</li>
|
||||
<li><strong>Token Cleanup:</strong> Expired tokens are automatically purged from the system</li>
|
||||
<li><strong>Challenge Invalidation:</strong> Challenges are immediately invalidated after successful verification</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="security">
|
||||
<h3>Rate Limiting</h3>
|
||||
<ul>
|
||||
<li><strong>IP-Based Limits:</strong> Maximum verification attempts per hour (default: 10)</li>
|
||||
<li><strong>Request ID Binding:</strong> Challenge parameters are bound to the requesting IP</li>
|
||||
<li><strong>Challenge Expiration:</strong> Challenges expire after 5 minutes to prevent stockpiling</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="security">
|
||||
<h3>Advanced Verification</h3>
|
||||
<ul>
|
||||
<li><strong>Proof of Space:</strong> Memory-intensive operations prevent GPU/ASIC acceleration</li>
|
||||
<li><strong>Browser Fingerprinting:</strong> Secure client-hint headers verify legitimate browsers</li>
|
||||
<li><strong>Challenge Obfuscation:</strong> Challenges are encoded and structured to resist automated analysis</li>
|
||||
<li><strong>Persistent Secret:</strong> The system uses a persistent HMAC secret stored securely on disk</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="configuration" class="section">
|
||||
<h2>Configuration Options</h2>
|
||||
<p>
|
||||
The Checkpoint system can be configured through these constants:
|
||||
</p>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Constant</th>
|
||||
<th>Description</th>
|
||||
<th>Default</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Difficulty</td>
|
||||
<td>Number of leading zeros required in the hash</td>
|
||||
<td>4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>TokenExpiration</td>
|
||||
<td>Duration for which a token is valid</td>
|
||||
<td>24 hours</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cookie Name</td>
|
||||
<td>__Host-checkpoint_token</td>
|
||||
<td>The cookie name storing the issued token</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>maxAttemptsPerHour</td>
|
||||
<td>Rate limit for verification attempts</td>
|
||||
<td>10</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>saltLength</td>
|
||||
<td>Length of the random salt in bytes</td>
|
||||
<td>16</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>maxNonceAge</td>
|
||||
<td>Time before nonces are cleaned up</td>
|
||||
<td>24 hours</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>challengeExpiration</td>
|
||||
<td>Time before a challenge expires</td>
|
||||
<td>5 minutes</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="warning">
|
||||
<p>
|
||||
<strong>Warning:</strong> Increasing the Difficulty significantly increases the computational work required by clients.
|
||||
A value that's too high may result in poor user experience, especially on mobile devices.
|
||||
</p>
|
||||
</div>
|
||||
<div class="code-example">
|
||||
<span class="code-label">Go</span>
|
||||
<pre><code>const (
|
||||
// Difficulty defines the number of leading zeros required in hash
|
||||
Difficulty = 4
|
||||
// TokenExpiration sets token validity period
|
||||
TokenExpiration = 24 * time.Hour
|
||||
// CookieName defines the cookie name for tokens
|
||||
CookieName = "__Host-checkpoint_token"
|
||||
// Max verification attempts per IP per hour
|
||||
maxAttemptsPerHour = 10
|
||||
// Salt length for additional entropy
|
||||
saltLength = 16
|
||||
)</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="middleware" class="section">
|
||||
<h2>Middleware Integration</h2>
|
||||
<p>
|
||||
The Checkpoint system provides a middleware handler that automatically protects HTML routes while bypassing API routes and static assets:
|
||||
</p>
|
||||
|
||||
<h3>HTMLCheckpointMiddleware</h3>
|
||||
<p>
|
||||
This middleware is optimized for HTML routes, with smart content-type detection and automatic exclusions for static assets and API endpoints.
|
||||
</p>
|
||||
<div class="code-example">
|
||||
<span class="code-label">Go</span>
|
||||
<pre><code>// HTMLCheckpointMiddleware handles challenges specifically for HTML pages
|
||||
func HTMLCheckpointMiddleware() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
// Allow certain paths to bypass verification
|
||||
path := c.Path()
|
||||
if path == "/video-player" || path == "/video-player.html" || strings.HasPrefix(path, "/videos/") {
|
||||
return c.Next()
|
||||
}
|
||||
if strings.HasPrefix(path, "/api") {
|
||||
return c.Next()
|
||||
}
|
||||
if path == "/favicon.ico" || (strings.Contains(path, ".") && !strings.HasSuffix(path, ".html")) {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
// Only apply to HTML routes
|
||||
isHtmlRoute := strings.HasSuffix(path, ".html") || path == "/" ||
|
||||
(len(path) > 0 && !strings.Contains(path, "."))
|
||||
if !isHtmlRoute {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
token := c.Cookies(CookieName)
|
||||
if token != "" {
|
||||
valid, err := validateToken(token, c)
|
||||
if err == nil && valid {
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
return serveInterstitial(c)
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>Usage in Application</h3>
|
||||
<div class="code-example">
|
||||
<span class="code-label">Go</span>
|
||||
<pre><code>// Enable HTML checkpoint protection for all routes
|
||||
app.Use(middleware.HTMLCheckpointMiddleware())
|
||||
|
||||
// API group with verification endpoints
|
||||
api := app.Group("/api")
|
||||
|
||||
// Verification endpoints
|
||||
api.Post("/pow/verify", middleware.VerifyCheckpointHandler)
|
||||
api.Get("/pow/challenge", middleware.GetCheckpointChallengeHandler)
|
||||
|
||||
// Example protected API endpoint
|
||||
api.Get("/protected", func(c *fiber.Ctx) error {
|
||||
// Access is already verified by cookie presence
|
||||
return c.JSON(fiber.Map{
|
||||
"message": "You have accessed the protected endpoint!",
|
||||
"time": time.Now(),
|
||||
})
|
||||
})</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="client-side" class="section">
|
||||
<h2>Client-Side Implementation</h2>
|
||||
<p>
|
||||
The client-side implementation is handled by the interstitial page and its associated JavaScript:
|
||||
</p>
|
||||
<ol>
|
||||
<li>Client attempts to access a protected resource</li>
|
||||
<li>Server serves the interstitial page with a request ID</li>
|
||||
<li>JavaScript fetches challenge parameters from <code>/api/pow/challenge?id=REQUEST_ID</code></li>
|
||||
<li>Two verification stages run in parallel:
|
||||
<ul>
|
||||
<li>Computational proof: Using Web Workers to find a valid nonce</li>
|
||||
<li>Memory proof: Allocating and manipulating memory buffers</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Results are submitted to <code>/api/pow/verify</code> endpoint</li>
|
||||
<li>On success, the server sets a cookie and redirects to the original URL</li>
|
||||
</ol>
|
||||
|
||||
<h3>Web Worker Implementation</h3>
|
||||
<p>
|
||||
Computational proof is handled by Web Workers to avoid freezing the UI:
|
||||
</p>
|
||||
<div class="code-example">
|
||||
<span class="code-label">JavaScript</span>
|
||||
<pre><code>function workerFunction() {
|
||||
self.onmessage = function(e) {
|
||||
const { type, data } = e.data;
|
||||
|
||||
if (type === 'pow') {
|
||||
// PoW calculation
|
||||
const { challenge, salt, startNonce, endNonce, target, batchId } = data;
|
||||
let count = 0;
|
||||
let solution = null;
|
||||
|
||||
processNextNonce(startNonce);
|
||||
|
||||
function processNextNonce(nonce) {
|
||||
const input = String(challenge) + String(salt) + nonce.toString();
|
||||
const msgBuffer = new TextEncoder().encode(input);
|
||||
|
||||
crypto.subtle.digest('SHA-256', msgBuffer)
|
||||
.then(hashBuffer => {
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const result = hashArray.map(b =>
|
||||
b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
count++;
|
||||
|
||||
if (result.startsWith(target)) {
|
||||
solution = { nonce: nonce.toString(), found: true };
|
||||
self.postMessage({
|
||||
type: 'pow_result',
|
||||
solution: solution,
|
||||
count: count,
|
||||
batchId: batchId
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (nonce < endNonce && !solution) {
|
||||
setTimeout(() => processNextNonce(nonce + 1), 0);
|
||||
} else if (!solution) {
|
||||
self.postMessage({
|
||||
type: 'pow_result',
|
||||
solution: null,
|
||||
count: count,
|
||||
batchId: batchId
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>Memory Proof Implementation</h3>
|
||||
<p>
|
||||
The memory proof allocates and manipulates large buffers to verify client capabilities:
|
||||
</p>
|
||||
<div class="code-example">
|
||||
<span class="code-label">JavaScript</span>
|
||||
<pre><code>async function runProofOfSpace(seedHex, isDecoy) {
|
||||
// Deterministic memory size (48MB to 160MB) based on seed
|
||||
const minMB = 48, maxMB = 160;
|
||||
let seedInt = parseInt(seedHex.slice(0, 8), 16);
|
||||
const CHUNK_MB = minMB + (seedInt % (maxMB - minMB + 1));
|
||||
const CHUNK_SIZE = CHUNK_MB * 1024 * 1024;
|
||||
|
||||
// Chunk memory for controlled allocation
|
||||
const chunkCount = 4 + (seedInt % 5); // 4-8 chunks
|
||||
const chunkSize = Math.floor(CHUNK_SIZE / chunkCount);
|
||||
|
||||
// Run the proof multiple times to verify consistency
|
||||
const runs = 3;
|
||||
const hashes = [];
|
||||
const times = [];
|
||||
|
||||
// For each run...
|
||||
for (let r = 0; r < runs; r++) {
|
||||
// Generate deterministic chunk order
|
||||
let prng = seededPRNG(seedHex + r.toString(16));
|
||||
let order = Array.from({length: chunkCount}, (_, i) => i);
|
||||
for (let i = order.length - 1; i > 0; i--) {
|
||||
const j = prng() % (i + 1);
|
||||
[order[i], order[j]] = [order[j], order[i]];
|
||||
}
|
||||
|
||||
// Allocate and fill memory buffer
|
||||
let t0 = performance.now();
|
||||
let buf = new ArrayBuffer(CHUNK_SIZE);
|
||||
let view = new Uint8Array(buf);
|
||||
|
||||
// Fill buffer with deterministic pattern
|
||||
for (let c = 0; c < chunkCount; c++) {
|
||||
let chunkIdx = order[c];
|
||||
let start = chunkIdx * chunkSize;
|
||||
let end = (chunkIdx + 1) * chunkSize;
|
||||
for (let i = start; i < end; i += 4096) {
|
||||
view[i] = prng() & 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
// Hash the entire buffer
|
||||
let hashBuf = await crypto.subtle.digest('SHA-256', view);
|
||||
let t2 = performance.now();
|
||||
|
||||
// Convert hash to hex string
|
||||
let hashHex = Array.from(new Uint8Array(hashBuf))
|
||||
.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
// Store results
|
||||
hashes.push(hashHex);
|
||||
times.push(Math.round(t2 - t0));
|
||||
|
||||
// Clean up
|
||||
buf = null; view = null;
|
||||
}
|
||||
|
||||
return { hashes, times };
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<p>
|
||||
The client-side implementation is designed to be difficult to reverse-engineer. The obfuscated API responses, minimal logging, and anti-debugging measures prevent automated circumvention.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="api-endpoints" class="section">
|
||||
<h2>API Endpoints</h2>
|
||||
<p>
|
||||
The Checkpoint system exposes two primary API endpoints:
|
||||
</p>
|
||||
|
||||
<h3>1. Challenge Endpoint</h3>
|
||||
<p>
|
||||
Retrieves challenge parameters for a verification request:
|
||||
</p>
|
||||
<div class="code-example">
|
||||
<span class="code-label">HTTP</span>
|
||||
<pre><code>GET /api/pow/challenge?id=REQUEST_ID
|
||||
|
||||
Response:
|
||||
{
|
||||
"a": "base64-encoded-challenge",
|
||||
"b": "base64-encoded-salt",
|
||||
"c": 4,
|
||||
"d": "hex-encoded-pos-seed"
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>2. Verification Endpoint</h3>
|
||||
<p>
|
||||
Accepts proof solutions and issues tokens when valid:
|
||||
</p>
|
||||
<div class="code-example">
|
||||
<span class="code-label">HTTP</span>
|
||||
<pre><code>POST /api/pow/verify
|
||||
|
||||
Request:
|
||||
{
|
||||
"request_id": "unique-request-id",
|
||||
"g": "nonce-solution",
|
||||
"h": ["pos-hash1", "pos-hash2", "pos-hash3"],
|
||||
"i": [time1, time2, time3]
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"token": "base64-encoded-token",
|
||||
"expires_at": "2025-04-17T18:57:48Z"
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<p>
|
||||
<strong>Backwards Compatibility:</strong> The older endpoint <code>/api/verify</code> is maintained for compatibility with existing clients.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="doc-version-note">These docs reflect version 2.0 of the <strong>Checkpoint Protection System</strong>.</div>
|
||||
<p>Last updated: <span id="last-updated">Tuesday, April 16, 2025</span></p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
213
develop/html/index.html
Normal file
213
develop/html/index.html
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
<!doctype html>
|
||||
<html lang=en>
|
||||
|
||||
<head>
|
||||
<meta charset=UTF-8>
|
||||
<meta http-equiv=X-UA-Compatible content="IE=edge">
|
||||
<meta name=viewport content="width=device-width,initial-scale=1">
|
||||
<title>caileb.com</title>
|
||||
<meta name=description content="Public-facing services hosted on caileb.com">
|
||||
<link rel=icon href=/images/favi.png type=image/png>
|
||||
<link rel=apple-touch-icon href=/images/favi.png>
|
||||
<link rel="shortcut icon" href=/images/favi.png>
|
||||
<link rel=preload href=/webfonts/Poppins-Regular.woff2 as=font type=font/woff2 crossorigin>
|
||||
<link rel=preload href=/webfonts/Poppins-SemiBold.woff2 as=font type=font/woff2 crossorigin>
|
||||
<link rel="preload" href="/js/u.js" as="script">
|
||||
<link rel=preload as=image href=/images/logos/immich.svg>
|
||||
<link rel=preload as=image href=/images/logos/jellyfin.svg>
|
||||
<link rel=preload as=image type=image/webp href=/images/logos/linkwarden.webp>
|
||||
<link rel=preload as=image href=/images/logos/navidrome.svg>
|
||||
<link rel=manifest href=/manifest.json>
|
||||
<link rel="stylesheet" href="/css/u.css">
|
||||
<style>
|
||||
:root {
|
||||
--background-color: #121212;
|
||||
--card-gradient-start: #1e1e1e;
|
||||
--card-gradient-end: #333;
|
||||
--header-background: #262626;
|
||||
--text-color: #fff;
|
||||
--accent-color: #9B59B6;
|
||||
--subtext-color: #ccc
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
body {
|
||||
background: #1c1c1c;
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin: 0;
|
||||
padding: 20px 20px 0 20px
|
||||
}
|
||||
|
||||
@media (min-width:1024px) {
|
||||
body {
|
||||
height: 100vh;
|
||||
justify-content: center;
|
||||
padding: 20px 20px 60px
|
||||
}
|
||||
|
||||
footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
font-size: 1.2rem
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
width: 100%
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
background: var(--header-background);
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, .3)
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: .5rem;
|
||||
color: var(--accent-color)
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 1.25rem;
|
||||
color: var(--subtext-color)
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
flex-basis: 280px;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, .25)
|
||||
}
|
||||
|
||||
.card .icon {
|
||||
width: 112px;
|
||||
height: 112px;
|
||||
margin-bottom: 1rem;
|
||||
object-fit: contain
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: .5rem
|
||||
}
|
||||
|
||||
.card p {
|
||||
font-size: 1rem;
|
||||
text-align: center
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
background-color: transparent;
|
||||
color: #c3c3c3;
|
||||
font-size: 1.2rem
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #cf7bf1;
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
.no-hover .card {
|
||||
transition: none !important
|
||||
}
|
||||
|
||||
.no-hover .card:hover {
|
||||
transform: none !important;
|
||||
box-shadow: none !important
|
||||
}
|
||||
|
||||
@keyframes waveColor {
|
||||
0% {
|
||||
color: grey
|
||||
}
|
||||
|
||||
50% {
|
||||
color: #fff
|
||||
}
|
||||
|
||||
100% {
|
||||
color: grey
|
||||
}
|
||||
}
|
||||
|
||||
.highlight-letter {
|
||||
display: inline-block;
|
||||
animation-name: waveColor;
|
||||
animation-duration: 1.2s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear
|
||||
}
|
||||
</style>
|
||||
<script src="/js/u.js" async></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class=container>
|
||||
<section class=grid>
|
||||
<a class="card eel" href=https://gallery.caileb.com
|
||||
style=background:linear-gradient(135deg,rgba(252,87,94,.55),rgba(247,180,44,.55))>
|
||||
<img alt="Immich Logo" class=icon src=/images/logos/immich.svg sizes=112x112>
|
||||
<h1>Immich</h1>
|
||||
</a>
|
||||
<a class="card eel" href=https://jellyfin.caileb.com
|
||||
style=background:linear-gradient(135deg,rgba(99,49,148,.55),rgba(43,131,237,.55))>
|
||||
<img alt="Jellyfin Logo" class=icon src=/images/logos/jellyfin.svg sizes=112x112>
|
||||
<h1>Jellyfin</h1>
|
||||
</a>
|
||||
<a class="card eel" href=https://archive.caileb.com
|
||||
style=background:linear-gradient(135deg,rgba(15,76,129,.55),rgba(22,191,253,.55))>
|
||||
<img alt="Linkwarden Logo" class=icon src=/images/logos/linkwarden.webp sizes=112x112>
|
||||
<h1>Linkwarden</h1>
|
||||
</a>
|
||||
<a class="card eel" href=https://music.caileb.com
|
||||
style=background:linear-gradient(135deg,rgba(33,150,243,.55),rgba(3,218,197,.55))>
|
||||
<img alt="Navidrome Logo" class=icon src=/images/logos/navidrome.svg sizes=112x112>
|
||||
<h1>Navidrome</h1>
|
||||
</a>
|
||||
</section>
|
||||
</div>
|
||||
<footer id="email-footer">Email: <a href="mailto:a@caileb.com">a@caileb.com</a></footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
550
develop/html/integrity-demo.html
Normal file
550
develop/html/integrity-demo.html
Normal file
|
|
@ -0,0 +1,550 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Integrity Checker - caileb.com</title>
|
||||
<meta name="description" content="Demonstrates the SRI integrity checker feature">
|
||||
<link rel="icon" href="/images/favi.png" type="image/png">
|
||||
<link rel="apple-touch-icon" href="/images/favi.png">
|
||||
<link rel="shortcut icon" href="/images/favi.png">
|
||||
<link rel="preload" href="/webfonts/Poppins-Regular.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/webfonts/Poppins-SemiBold.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="/css/u.css">
|
||||
<link rel="stylesheet" href="/css/docs.css">
|
||||
|
||||
<!-- External stylesheet (will get integrity) -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
|
||||
|
||||
<!-- Local preloaded script (won't get integrity) -->
|
||||
<link rel="preload" href="/js/u.js" as="script">
|
||||
|
||||
<!-- External preloaded script (will get integrity) -->
|
||||
<link rel="preload" href="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" as="script">
|
||||
|
||||
<!-- Add Lodash for another test -->
|
||||
<link rel="preload" href="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js" as="script">
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/quicklink/2.3.0/quicklink.umd.js"></script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--background-color: #121212;
|
||||
--card-gradient-start: #1e1e1e;
|
||||
--card-gradient-end: #333;
|
||||
--header-background: #262626;
|
||||
--text-color: #fff;
|
||||
--accent-color: #9B59B6;
|
||||
--subtext-color: #ccc;
|
||||
--success-color: #2ecc71;
|
||||
--error-color: #e74c3c;
|
||||
--warning-color: #f39c12;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #1c1c1c;
|
||||
color: var(--text-color);
|
||||
font-family: Poppins, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 1.25rem;
|
||||
color: var(--subtext-color);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: linear-gradient(135deg, rgba(30, 30, 30, 0.8), rgba(51, 51, 51, 0.8));
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: var(--accent-color);
|
||||
margin: 1.5rem 0 1rem;
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: var(--accent-color);
|
||||
margin: 1rem 0;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p, li {
|
||||
color: var(--text-color);
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.resource-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1.5rem 0;
|
||||
background: rgba(30, 30, 30, 0.6);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.resource-table th, .resource-table td {
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
.resource-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.resource-table th {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
color: var(--accent-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.external {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.local {
|
||||
color: #2ecc71;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
padding-left: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.status-loaded {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background-color: var(--error-color);
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: var(--warning-color);
|
||||
}
|
||||
|
||||
.script-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
background: linear-gradient(135deg, rgba(30, 30, 30, 0.6), rgba(51, 51, 51, 0.6));
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.demo-card h4 {
|
||||
color: var(--accent-color);
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.demo-result {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
font-family: monospace;
|
||||
color: #e0e0e0;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: var(--accent-color);
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-family: Poppins, sans-serif;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #8e44ad;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(155, 89, 182, 0.4);
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.flex-container > div {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.toc {
|
||||
background-color: rgba(30, 30, 30, 0.5);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px 0 30px 0;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.toc h2 {
|
||||
margin-top: 0;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 15px;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.toc ul {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toc li {
|
||||
margin-bottom: 8px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.toc a {
|
||||
display: block;
|
||||
padding: 5px 15px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
background-color: rgba(20, 20, 20, 0.5);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.toc a:hover {
|
||||
background-color: rgba(50, 50, 50, 0.5);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.section {
|
||||
scroll-margin-top: 20px;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background-color: rgba(40, 40, 40, 0.5);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
border: 1px solid var(--border-color);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
color: var(--accent-color);
|
||||
margin-top: 0;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.code-example {
|
||||
position: relative;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.code-label {
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
right: 10px;
|
||||
background-color: var(--accent-color);
|
||||
color: white;
|
||||
font-size: 0.8rem;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: monospace;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.toc ul {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.toc a {
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.resource-status,
|
||||
.demo-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script src="/js/u.js" async></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Auto-Integrity Hash Demo</h1>
|
||||
|
||||
<div class="info-box">
|
||||
<p><strong>This is a live demonstration of automatic SRI hash generation.</strong></p>
|
||||
<p>The server automatically adds integrity hashes to all external resources when the site is built - no manual work required.</p>
|
||||
<p>If you view the source code of this page, you'll see all external CSS and JavaScript files have <code>integrity</code> and <code>crossorigin</code> attributes that were added automatically during build.</p>
|
||||
<p>This security feature protects against compromised CDNs and ensures resources haven't been tampered with.</p>
|
||||
</div>
|
||||
|
||||
<h2>External Scripts Working</h2>
|
||||
<p>These demos confirm that the external scripts are loaded and working correctly with their integrity hashes:</p>
|
||||
|
||||
<div class="demo-cards">
|
||||
<div class="demo-card">
|
||||
<h3>jQuery Demo</h3>
|
||||
<p>jQuery provides DOM manipulation and animation capabilities.</p>
|
||||
<div class="demo-result" id="jquery-result">Running jQuery test...</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h3>Lodash Demo</h3>
|
||||
<p>Lodash provides utility functions for common programming tasks.</p>
|
||||
<div class="demo-result" id="lodash-result">Running Lodash test...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-cards">
|
||||
<div class="demo-card">
|
||||
<h3>Bootstrap Components</h3>
|
||||
<p>Bootstrap provides responsive UI components.</p>
|
||||
<div class="demo-result" id="bootstrap-result">
|
||||
<div class="alert alert-info">
|
||||
This is a Bootstrap alert component
|
||||
</div>
|
||||
<div class="progress" style="height: 20px; background-color: #444;">
|
||||
<div class="progress-bar bg-success" role="progressbar" style="width: 75%;" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100">75%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h3>Quicklink Demo</h3>
|
||||
<p>Quicklink prefetches links that are in the viewport.</p>
|
||||
<div class="demo-result" id="quicklink-result">Running Quicklink test...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Monitored Resources</h2>
|
||||
<p>The following resources have integrity checks automatically applied during build:</p>
|
||||
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Resource Type</th>
|
||||
<th>Location</th>
|
||||
<th>Integrity Added?</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Stylesheet</td>
|
||||
<td class="local">/css/u.css</td>
|
||||
<td>No (Local)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Stylesheet</td>
|
||||
<td class="external">Bootstrap CSS (CDN)</td>
|
||||
<td>Yes (External)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Preloaded Script</td>
|
||||
<td class="local">/js/u.js</td>
|
||||
<td>No (Local)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Preloaded Script</td>
|
||||
<td class="external">jQuery (CDN)</td>
|
||||
<td>Yes (External)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Preloaded Script</td>
|
||||
<td class="external">Lodash (CDN)</td>
|
||||
<td>Yes (External)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Script</td>
|
||||
<td class="external">Quicklink (CDN)</td>
|
||||
<td>Yes (External)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Local script (won't get integrity) -->
|
||||
<script src="/js/u.js"></script>
|
||||
|
||||
<!-- External scripts (will get integrity) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Auto-run jQuery demo
|
||||
function runJqueryDemo() {
|
||||
const resultElement = document.getElementById('jquery-result');
|
||||
|
||||
try {
|
||||
if (typeof jQuery !== 'undefined') {
|
||||
resultElement.textContent = '';
|
||||
const demoText = document.createElement('div');
|
||||
demoText.textContent = 'jQuery ' + jQuery.fn.jquery + ' loaded successfully! This color animation is powered by jQuery.';
|
||||
resultElement.appendChild(demoText);
|
||||
|
||||
// Use jQuery for color animation
|
||||
jQuery(demoText).css('color', '#e74c3c')
|
||||
.animate({ color: '#2ecc71' }, 1000)
|
||||
.animate({ color: '#3498db' }, 1000)
|
||||
.animate({ color: '#f39c12' }, 1000)
|
||||
.animate({ color: '#9b59b6' }, 1000);
|
||||
} else {
|
||||
resultElement.textContent = 'Error: jQuery is not loaded';
|
||||
}
|
||||
} catch (e) {
|
||||
resultElement.textContent = 'Error: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-run Lodash demo
|
||||
function runLodashDemo() {
|
||||
const resultElement = document.getElementById('lodash-result');
|
||||
|
||||
try {
|
||||
if (typeof _ !== 'undefined') {
|
||||
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
const chunked = _.chunk(array, 3);
|
||||
const shuffled = _.shuffle([...array]);
|
||||
const summed = _.sum(array);
|
||||
|
||||
resultElement.innerHTML =
|
||||
'<div>Lodash ' + _.VERSION + ' loaded successfully!</div>' +
|
||||
'<div>• Chunking [1-10] into groups of 3: ' + JSON.stringify(chunked) + '</div>' +
|
||||
'<div>• Shuffled array: ' + JSON.stringify(shuffled) + '</div>' +
|
||||
'<div>• Sum of array: ' + summed + '</div>';
|
||||
} else {
|
||||
resultElement.textContent = 'Error: Lodash is not loaded';
|
||||
}
|
||||
} catch (e) {
|
||||
resultElement.textContent = 'Error: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-run Quicklink check
|
||||
function runQuicklinkCheck() {
|
||||
const resultElement = document.getElementById('quicklink-result');
|
||||
|
||||
try {
|
||||
if (typeof quicklink !== 'undefined') {
|
||||
// Call quicklink to prefetch
|
||||
quicklink.listen();
|
||||
resultElement.innerHTML =
|
||||
'<div>Quicklink loaded successfully!</div>' +
|
||||
'<div>Now prefetching links as you scroll near them.</div>' +
|
||||
'<div style="margin-top: 10px; font-size: 0.9em; color: #aaa;">Check network tab in dev tools to see prefetch requests.</div>';
|
||||
} else {
|
||||
resultElement.textContent = 'Error: Quicklink is not loaded';
|
||||
}
|
||||
} catch (e) {
|
||||
resultElement.textContent = 'Error: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Check when DOM is fully loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Small delay to ensure scripts are fully initialized
|
||||
setTimeout(function() {
|
||||
runJqueryDemo();
|
||||
runLodashDemo();
|
||||
runQuicklinkCheck();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Fallback if DOMContentLoaded already fired
|
||||
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||
setTimeout(function() {
|
||||
runJqueryDemo();
|
||||
runLodashDemo();
|
||||
runQuicklinkCheck();
|
||||
}, 300);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
398
develop/html/kb.html
Normal file
398
develop/html/kb.html
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
<!doctype html>
|
||||
<html lang=en>
|
||||
|
||||
<head>
|
||||
<meta charset=UTF-8>
|
||||
<meta http-equiv=X-UA-Compatible content="IE=edge">
|
||||
<meta name=viewport content="width=device-width,initial-scale=1">
|
||||
<title>Caileb's Knowledgebase</title>
|
||||
<meta name=description content="Caileb's Knowledgebase">
|
||||
<link rel=icon href=/images/favi.png type=image/png>
|
||||
<link rel=apple-touch-icon href=/images/favi.png>
|
||||
<link rel="shortcut icon" href=/images/favi.png>
|
||||
<link rel=preload href=/webfonts/Poppins-Regular.woff2 as=font type=font/woff2 crossorigin>
|
||||
<link rel=preload href=/webfonts/Poppins-SemiBold.woff2 as=font type=font/woff2 crossorigin>
|
||||
<link rel="preload" href="/js/u.js" as="script">
|
||||
<link rel="stylesheet" href="/css/u.css">
|
||||
<style>
|
||||
:root {
|
||||
--background-color: #1e1e1e;
|
||||
--text-color: #cfcfcf;
|
||||
--heading-color: #ffffff;
|
||||
--code-block-bg: #333;
|
||||
--code-block-border: #4d9cfa;
|
||||
--code-color: #dcdcdc;
|
||||
--link-color: #4d9cfa;
|
||||
--link-hover-color: #4d9cfa;
|
||||
--input-border: #f39c12;
|
||||
--input-focus-border: #e67e22;
|
||||
--collapsible-bg: #2d2d2d;
|
||||
--collapsible-hover-bg: #3a3a3a;
|
||||
--scrollbar-bg: #2d2d2d;
|
||||
--scrollbar-thumb-bg: #4d9cfa;
|
||||
--scrollbar-thumb-hover-bg: #3971a3;
|
||||
--section-divider: #333;
|
||||
--section-accent: #f39c12;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 700px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1, h3, h4 {
|
||||
color: var(--heading-color);
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: var(--heading-color);
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
position: relative;
|
||||
padding-bottom: 10px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h2:hover::before {
|
||||
content: "#";
|
||||
position: absolute;
|
||||
left: -1.6rem;
|
||||
color: var(--link-color);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
h2::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--section-accent);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.section::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent,
|
||||
var(--section-divider) 10%,
|
||||
var(--section-divider) 90%,
|
||||
transparent);
|
||||
}
|
||||
|
||||
.section:last-child::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background-color: var(--code-block-bg);
|
||||
color: var(--heading-color);
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid var(--code-block-border)
|
||||
}
|
||||
|
||||
.code-block.command::before {
|
||||
content: "$";
|
||||
color: var(--code-block-border);
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 11px;
|
||||
font-family: Consolas, "Courier New", monospace
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: Consolas, "Courier New", monospace;
|
||||
color: var(--code-color)
|
||||
}
|
||||
|
||||
code.command {
|
||||
padding-left: 15px
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link-color);
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
.collapsible-label {
|
||||
background-color: var(--collapsible-bg);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
border: 2px solid var(--input-border);
|
||||
text-align: left;
|
||||
outline: 0;
|
||||
font-size: 18px;
|
||||
border-radius: 5px 5px 0 0;
|
||||
margin-bottom: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 700px;
|
||||
box-sizing: border-box;
|
||||
transition: background-color .3s, border-color .3s;
|
||||
position: relative;
|
||||
z-index: 2
|
||||
}
|
||||
|
||||
.collapsible-label:hover {
|
||||
background-color: var(--collapsible-hover-bg);
|
||||
border-color: var(--input-focus-border)
|
||||
}
|
||||
|
||||
.collapsible-input {
|
||||
display: none
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-bottom: 20px
|
||||
}
|
||||
|
||||
.content {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
background-color: var(--collapsible-bg);
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
border: 2px solid transparent;
|
||||
border-top: none;
|
||||
transition: max-height .45s ease, padding .45s ease, border-color .45s ease;
|
||||
padding: 0 10px
|
||||
}
|
||||
|
||||
.collapsible-input:checked+.collapsible-label+.content {
|
||||
border-color: var(--input-border);
|
||||
padding: 10px
|
||||
}
|
||||
|
||||
.arrow {
|
||||
transition: transform .3s
|
||||
}
|
||||
|
||||
.collapsible-input:checked+.collapsible-label .arrow {
|
||||
transform: rotate(180deg)
|
||||
}
|
||||
|
||||
.step {
|
||||
margin-bottom: 20px
|
||||
}
|
||||
|
||||
.step h4 {
|
||||
margin-bottom: 10px
|
||||
}
|
||||
</style>
|
||||
<script src="/js/u.js" async></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="section" id="fail2ban">
|
||||
<h2>Fail2ban</h2>
|
||||
<div class="content-wrapper">
|
||||
<input type="checkbox" id="collapsible-fail2ban" class="collapsible-input">
|
||||
<label for="collapsible-fail2ban" class="collapsible-label">
|
||||
Setup Fail2ban
|
||||
<span class="arrow">▼</span>
|
||||
</label>
|
||||
<div class="content">
|
||||
<div class="step">
|
||||
<h4>Step 1: Install Fail2ban (Debian/Ubuntu)</h4>
|
||||
<p>First, install Fail2ban by running:</p>
|
||||
<div class="code-block command">
|
||||
<code class="command">sudo apt install fail2ban</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<h4>Step 2: Navigate to the Fail2ban Directory</h4>
|
||||
<p>Change to the Fail2ban configuration directory:</p>
|
||||
<div class="code-block command">
|
||||
<code class="command">cd /etc/fail2ban/</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<h4>Step 3: Copy the Example Configuration File</h4>
|
||||
<p>Copy the example configuration file as a base for your custom configuration:</p>
|
||||
<div class="code-block command">
|
||||
<code class="command">sudo cp jail.conf jail.local</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<h4>Step 4: Create a New Filter</h4>
|
||||
<p>Navigate to the filter.d directory and create a new filter file:</p>
|
||||
<div class="code-block command">
|
||||
<code class="command">cd filter.d/</code>
|
||||
</div>
|
||||
<div class="code-block command">
|
||||
<code class="command">sudo nano nginx-4xx.conf</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<h4>Step 5: Define the Filter to Block Repeated 4xx Errors</h4>
|
||||
<p>Add the following content to the <code>nginx-4xx.conf</code> file:</p>
|
||||
<div class="code-block">
|
||||
<code>[Definition]<br>failregex = ^<HOST>.*"(GET|POST|HEAD|CONNECT).*" (404|444|403|400) .*<br>ignoreregex = 127.0.0.1 127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<h4>Step 6: Edit the Jail Configuration to Use the New Filter</h4>
|
||||
<p>Go back to the previous directory and edit <code>jail.local</code>:</p>
|
||||
<div class="code-block command">
|
||||
<code class="command">cd ..</code>
|
||||
</div>
|
||||
<div class="code-block command">
|
||||
<code class="command">sudo nano jail.local</code>
|
||||
</div>
|
||||
<p>Add the following section:</p>
|
||||
<div class="code-block">
|
||||
<code>#<br># Repeated 4xx errors (Nginx)<br>#<br>[nginx-4xx]<br>enabled = true<br>port = http,https<br>logpath = /var/log/nginx/access.log<br>maxretry = 4</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<h4>Step 7: Restart Fail2ban for the Changes to Take Effect</h4>
|
||||
<p>Restart the Fail2ban service:</p>
|
||||
<div class="code-block command">
|
||||
<code class="command">sudo systemctl restart fail2ban</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<h4>Step 8: Check the Filter Status</h4>
|
||||
<p>Verify the filter is working:</p>
|
||||
<div class="code-block command">
|
||||
<code class="command">sudo fail2ban-client status nginx-4xx</code>
|
||||
</div>
|
||||
<h4>OR</h4>
|
||||
<p>For a prettified output:</p>
|
||||
<div class="code-block command">
|
||||
<code class="command">sudo fail2ban-client get nginx-4xx banip | tr ' ' '\n'</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section" id="pm2">
|
||||
<h2>Node PM2</h2>
|
||||
<p>Restart</p>
|
||||
<pre class="code-block command">
|
||||
<code class="command">pm2 restart caileb.com</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div class="section" id="ffmpeg">
|
||||
<h2>FFmpeg</h2>
|
||||
<p>Highest quality AV1</p>
|
||||
<div class="code-block command">
|
||||
<code class="command">ffmpeg -i input -c:v av1_nvenc -preset p7 -cq 1 -b:v 0 -qmin 1 -qmax 5 -rc-lookahead 250 -spatial-aq 1 -aq-strength 15 -refs 16 -temporal-aq 1 -c:a flac -compression_level 8 highest_quality.mkv</code>
|
||||
</div>
|
||||
<p>Standard compression</p>
|
||||
<div class="code-block command">
|
||||
<code class="command">ffmpeg -i input -vf "mpdecimate" -fps_mode vfr -c:v av1_nvenc -preset p7 -cq 30 -b:v 0 -maxrate 18.5M -bufsize 25M -g 240 -keyint_min 24 -rc vbr -c:a libopus -b:a 128k compressed.webm</code>
|
||||
</div>
|
||||
<p>Extreme compression</p>
|
||||
<div class="code-block command">
|
||||
<code class="command">ffmpeg -i input -vf "mpdecimate,scale=-1:1080" -fps_mode vfr -c:v av1_nvenc -preset p7 -rc vbr -b:v 6M -maxrate 12M -bufsize 18M -g 300 -keyint_min 34 -c:a libopus -b:a 96k compressed.webm</code>
|
||||
</div>
|
||||
<p>Rocket.Chat</p>
|
||||
<div class="code-block command">
|
||||
<code class="command">ffmpeg -i input -vf "mpdecimate,scale=-1:1440" -fps_mode vfr -c:v av1_nvenc -preset p7 -rc vbr -b:v 8M -maxrate 15M -bufsize 22M -g 270 -keyint_min 28 -c:a libopus -b:a 112k rocket_chat.webm</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section" id="html">
|
||||
<h2>Useful HTML Stuffs</h2>
|
||||
<p>Make iFrames/Images Lazy Load <a href="https://developer.mozilla.org/docs/Web/Performance/Lazy_loading#images_and_iframes" target="_blank" rel="noopener noreferrer">MDN</a></p>
|
||||
<p>Replace FitVids or other similar JS libraries with CSS' aspect-ratio <a href="https://developer.mozilla.org/docs/Web/CSS/aspect-ratio" target="_blank" rel="noopener noreferrer">MDN</a></p>
|
||||
</div>
|
||||
|
||||
<div class="section" id="malware-removal">
|
||||
<h2>Malware Removal</h2>
|
||||
<ol>
|
||||
<li>
|
||||
<strong><a href="https://www.malwarebytes.com/mwb-download" target="_blank" rel="noopener noreferrer">Malwarebytes Free</a></strong>
|
||||
<p>Easy-to-use tool that quickly detects and removes a broad range of malware.</p>
|
||||
</li>
|
||||
<li>
|
||||
<strong><a href="https://www.emsisoft.com/home/emergency-kit/" target="_blank" rel="noopener noreferrer">Emsisoft Emergency Kit</a></strong>
|
||||
<p>Utilizes Bitdefender's engine on top of their own for a strong all-in-one cleanup.</p>
|
||||
</li>
|
||||
<li>
|
||||
<strong><a href="https://www.sophos.com/free-tools/virus-removal-tool" target="_blank" rel="noopener noreferrer">Sophos Scan & Clean</a></strong>
|
||||
<p>Portable scanner with effective heuristic analysis for detecting malware.</p>
|
||||
</li>
|
||||
<li>
|
||||
<strong><a href="https://www.kaspersky.com/downloads/free-virus-removal-tool" target="_blank" rel="noopener noreferrer">Kaspersky Virus Removal Tool</a></strong>
|
||||
<p>Efficiently finds and removes stubborn malware threats.</p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script defer>
|
||||
const collapsibleInputs = document.querySelectorAll(".collapsible-input");
|
||||
collapsibleInputs.forEach((input) => {
|
||||
input.addEventListener("change", function() {
|
||||
const content = this.nextElementSibling.nextElementSibling;
|
||||
content.style.maxHeight = this.checked ? content.scrollHeight + "px" : "0";
|
||||
});
|
||||
});
|
||||
|
||||
// Add click event to headers to update URL with section ID
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Get all h2 elements
|
||||
const headers = document.querySelectorAll('h2');
|
||||
|
||||
// Add click event listeners to each header
|
||||
headers.forEach(header => {
|
||||
header.addEventListener('click', function() {
|
||||
// Find the parent section with an ID
|
||||
const section = this.closest('.section');
|
||||
if (section && section.id) {
|
||||
// Update the URL without reloading the page
|
||||
history.pushState(null, null, `#${section.id}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
664
develop/html/lazy-video.html
Normal file
664
develop/html/lazy-video.html
Normal file
|
|
@ -0,0 +1,664 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset=UTF-8>
|
||||
<meta http-equiv=X-UA-Compatible content="IE=edge">
|
||||
<meta name=viewport content="width=device-width,initial-scale=1">
|
||||
<title>Lazy Video Component - Multi-Platform Video Embedding</title>
|
||||
<meta name=description content="A lightweight, customizable web component for optimized video embeds from YouTube, Bitchute and more platforms with lazy loading for performance.">
|
||||
<link rel=icon href=/images/favi.png type=image/png>
|
||||
<link rel=apple-touch-icon href=/images/favi.png>
|
||||
<link rel="shortcut icon" href=/images/favi.png>
|
||||
<link rel=preload href=/webfonts/Poppins-Regular.woff2 as=font type=font/woff2 crossorigin>
|
||||
<link rel=preload href=/webfonts/Poppins-SemiBold.woff2 as=font type=font/woff2 crossorigin>
|
||||
<link rel="preload" href="/js/u.js" as="script">
|
||||
<link rel="preload" href="/js/lv.js" as="script">
|
||||
<link rel="stylesheet" href="/css/u.css">
|
||||
<link rel="stylesheet" href="/css/docs.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/@speed-highlight/core@1.2.7/dist/themes/github-dark.css">
|
||||
<script async src=/js/u.js></script>
|
||||
<script async src="/js/lv.js"></script>
|
||||
<script type="module" src="/js/docs.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<h1>Lazy Video Docs</h1>
|
||||
|
||||
<div class="toc">
|
||||
<h2>Contents</h2>
|
||||
<ul>
|
||||
<li><a href="#overview">Overview</a></li>
|
||||
<li><a href="#basic-usage">Basic Usage</a></li>
|
||||
<li><a href="#platforms">Supported Platforms</a></li>
|
||||
<li><a href="#attributes">Attributes</a></li>
|
||||
<li><a href="#styling">Styling & CSS Variables</a></li>
|
||||
<li><a href="#examples">Examples</a></li>
|
||||
<li><a href="#converting">Converting Existing iframes</a></li>
|
||||
<li><a href="#security">Security & Privacy</a></li>
|
||||
<li><a href="#browser-support">Browser Support</a></li>
|
||||
<li><a href="#breaking-change">Breaking Change</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<section id="overview" class="section">
|
||||
<h2>Overview</h2>
|
||||
<p>
|
||||
Embedding videos with standard <code><iframe></code> tags can dramatically slow down your site and consume large amounts of data. Each iframe loads the full video player and related resources immediately-even if the user never interacts with it. On pages with several videos, this can add <strong>hundreds of megabytes</strong> to the initial page load, resulting in a sluggish and costly experience, especially for users on mobile devices or limited networks.
|
||||
</p>
|
||||
<h3>How Lazy Video Helps</h3>
|
||||
<p>
|
||||
The <code><lazy-video></code> component solves this by loading only a lightweight thumbnail and play button at first. The actual video player is loaded only when the user clicks play (or when the video scrolls into view if <code>autoload</code> is enabled). This keeps your pages fast, responsive, and bandwidth-friendly.
|
||||
</p>
|
||||
<div class="lv-btn-group">
|
||||
<a href="/js/lv.js" target="_blank" rel="noopener" class="lv-btn lv-btn-primary">View Source</a>
|
||||
<a href="/js/lv.js" download class="lv-btn lv-btn-outline">Download</a>
|
||||
<span class="lv-size-info">~17.0kB / <span>6.0kB</span> (Gzip)</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="basic-usage" class="section">
|
||||
<h2>Basic Usage</h2>
|
||||
<p>
|
||||
To get started, include the script on your page and use the custom element as shown below:
|
||||
</p>
|
||||
<div class="code-example">
|
||||
<span class="code-label">HTML</span>
|
||||
<pre><code><lazy-video
|
||||
src="https://www.youtube.com/embed/wPr3kws2prM"
|
||||
title="Till We Have Faces by Silent Planet">
|
||||
</lazy-video></code></pre>
|
||||
</div>
|
||||
<div class="note">
|
||||
<p>
|
||||
Always add a <code>title</code> for accessibility and better alt text on thumbnails.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="platforms" class="section">
|
||||
<h2>Officially Supported Platforms</h2>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Platform</th>
|
||||
<th>URL Pattern</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>YouTube</td>
|
||||
<td>
|
||||
<ul class="url-patterns">
|
||||
<li><code>youtube.com/embed/ID</code></li>
|
||||
<li><code>youtube.com/watch?v=ID</code></li>
|
||||
<li><code>youtu.be/ID</code></li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>Full support for thumbnails and parameters.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bitchute</td>
|
||||
<td>
|
||||
<ul class="url-patterns">
|
||||
<li><code>bitchute.com/video/ID/</code></li>
|
||||
<li><code>bitchute.com/embed/ID/</code></li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>Custom thumbnails are only needed if autoload is disabled.</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="attributes" class="section">
|
||||
<h2>Attributes</h2>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Attribute</th>
|
||||
<th>Description</th>
|
||||
<th>Default</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>src</td>
|
||||
<td>Video embed URL (required)</td>
|
||||
<td>N/A</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>title</td>
|
||||
<td>Video title</td>
|
||||
<td>"Video"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>width</td>
|
||||
<td>Width in pixels or percent</td>
|
||||
<td>100% (responsive)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>height</td>
|
||||
<td>Height in pixels</td>
|
||||
<td>16:9 ratio</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>thumbnail</td>
|
||||
<td>Custom thumbnail URL</td>
|
||||
<td>Auto-detected per platform</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>thumbnail-quality</td>
|
||||
<td>YouTube thumbnail quality (default, hq, mq, sd, maxres)</td>
|
||||
<td>Auto (maxres on desktop, hq on mobile)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>service</td>
|
||||
<td>Force a specific service (youtube, bitchute)</td>
|
||||
<td>Auto-detected</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>sandbox</td>
|
||||
<td>Extra security for the iframe. Restricts what the embedded player can do. See <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox" target="_blank">MDN</a> for details.</td>
|
||||
<td>allow-scripts allow-same-origin allow-popups allow-forms allow-presentation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>no-cookie</td>
|
||||
<td>Use youtube-nocookie.com for YouTube (privacy-friendly)</td>
|
||||
<td>true</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>autoload</td>
|
||||
<td>Load video when scrolled into view</td>
|
||||
<td>false (YouTube), true (Bitchute)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>hide-title</td>
|
||||
<td>Hide the video title bar</td>
|
||||
<td>false</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>align</td>
|
||||
<td>Set alignment (left, right, center)</td>
|
||||
<td>center</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>container-fit</td>
|
||||
<td>Make video fill the container (FitVids style)</td>
|
||||
<td>false</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="warning">
|
||||
<p>
|
||||
<strong>Warning:</strong> Using <code>autoload</code> with many videos on one page can impact performance as users scroll. Use with care!
|
||||
</p>
|
||||
</div>
|
||||
<div class="note">
|
||||
<p>
|
||||
<strong>Note:</strong> With <code>container-fit</code>, the component overrides max-width to 100% and sets max-height to auto, making it fill its container while keeping the aspect ratio.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="styling" class="section">
|
||||
<h2>Styling & CSS Variables</h2>
|
||||
<p>
|
||||
You can customize the look of <code><lazy-video></code> using CSS variables:
|
||||
</p>
|
||||
<div class="code-example">
|
||||
<span class="code-label">CSS</span>
|
||||
<pre><code>lazy-video {
|
||||
--lv-max-width: 600px;
|
||||
--lv-border-radius: 8px;
|
||||
--lv-play-button-color: #f00;
|
||||
--lv-play-button-bg: rgba(0, 0, 0, 0.7);
|
||||
--lv-show-title: none;
|
||||
}</code></pre>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<h3>Available CSS Variables</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<th>CSS Variable</th>
|
||||
<th>Description</th>
|
||||
<th>Default</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-max-width</td>
|
||||
<td>Maximum width of the video</td>
|
||||
<td>560px</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-aspect-ratio</td>
|
||||
<td>Aspect ratio</td>
|
||||
<td>16 / 9</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-display</td>
|
||||
<td>Display type</td>
|
||||
<td>block</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-position</td>
|
||||
<td>CSS position</td>
|
||||
<td>relative</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-border-radius</td>
|
||||
<td>Border radius for the container</td>
|
||||
<td>0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-margin</td>
|
||||
<td>Container margin</td>
|
||||
<td>0 auto</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-margin-left</td>
|
||||
<td>Margin for left alignment</td>
|
||||
<td>0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-margin-right</td>
|
||||
<td>Margin for right alignment</td>
|
||||
<td>0 0 0 auto</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-margin-center</td>
|
||||
<td>Margin for center alignment</td>
|
||||
<td>0 auto</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-align</td>
|
||||
<td>Set alignment (left, right, center)</td>
|
||||
<td>center</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-background</td>
|
||||
<td>Background color</td>
|
||||
<td>#000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-thumbnail-opacity</td>
|
||||
<td>Thumbnail opacity</td>
|
||||
<td>0.85</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-thumbnail-hover-opacity</td>
|
||||
<td>Opacity on hover</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-thumbnail-object-fit</td>
|
||||
<td>Object-fit for thumbnail</td>
|
||||
<td>cover</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-play-button-width</td>
|
||||
<td>Play button width</td>
|
||||
<td>68px</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-play-button-height</td>
|
||||
<td>Play button height</td>
|
||||
<td>48px</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-play-button-bg</td>
|
||||
<td>Play button background</td>
|
||||
<td>rgba(33, 33, 33, 0.8)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-play-button-bg-hover</td>
|
||||
<td>Play button hover background</td>
|
||||
<td>rgba(230, 33, 23, 1)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-play-button-color</td>
|
||||
<td>Play button arrow color</td>
|
||||
<td>rgba(255, 255, 255, 0.9)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-play-button-radius</td>
|
||||
<td>Play button border radius</td>
|
||||
<td>8px</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-play-button-arrow-size</td>
|
||||
<td>Play button arrow size</td>
|
||||
<td>12px 0 12px 20px</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-title-padding</td>
|
||||
<td>Title bar padding</td>
|
||||
<td>10px 12px</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-title-bg</td>
|
||||
<td>Title background</td>
|
||||
<td>rgba(0, 0, 0, 0.75)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-title-color</td>
|
||||
<td>Title text color</td>
|
||||
<td>white</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-title-font-family</td>
|
||||
<td>Title font family</td>
|
||||
<td>Roboto, Arial, sans-serif</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-title-font-size</td>
|
||||
<td>Title font size</td>
|
||||
<td>18px</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-title-font-weight</td>
|
||||
<td>Title font weight</td>
|
||||
<td>500</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-title-line-height</td>
|
||||
<td>Title line height</td>
|
||||
<td>1.2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-focus-outline</td>
|
||||
<td>Focus outline</td>
|
||||
<td>2px solid #4285F4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-focus-outline-offset</td>
|
||||
<td>Focus outline offset</td>
|
||||
<td>2px</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-show-title</td>
|
||||
<td>Show/hide title bar (use 'none' to hide)</td>
|
||||
<td>block</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-timestamp-right</td>
|
||||
<td>Timestamp right position</td>
|
||||
<td>10px</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-timestamp-bottom</td>
|
||||
<td>Timestamp bottom position</td>
|
||||
<td>10px</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-timestamp-bg</td>
|
||||
<td>Timestamp background</td>
|
||||
<td>rgba(0, 0, 0, 0.7)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-timestamp-color</td>
|
||||
<td>Timestamp text color</td>
|
||||
<td>white</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-timestamp-padding</td>
|
||||
<td>Timestamp padding</td>
|
||||
<td>2px 6px</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-timestamp-radius</td>
|
||||
<td>Timestamp border radius</td>
|
||||
<td>3px</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-timestamp-font-size</td>
|
||||
<td>Timestamp font size</td>
|
||||
<td>12px</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-timestamp-font-family</td>
|
||||
<td>Timestamp font family</td>
|
||||
<td>system-ui, sans-serif</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-loading-bg</td>
|
||||
<td>Loading background</td>
|
||||
<td>rgba(0,0,0,0.7)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-loading-color</td>
|
||||
<td>Loading text color</td>
|
||||
<td>white</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-loading-font-family</td>
|
||||
<td>Loading font family</td>
|
||||
<td>system-ui, sans-serif</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-fallback-bg</td>
|
||||
<td>Fallback background</td>
|
||||
<td>#1a1a1a</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-fallback-color</td>
|
||||
<td>Fallback text color</td>
|
||||
<td>white</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-fallback-font-family</td>
|
||||
<td>Fallback font family</td>
|
||||
<td>system-ui, sans-serif</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--lv-fallback-font-size</td>
|
||||
<td>Fallback font size</td>
|
||||
<td>14px</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="examples" class="section">
|
||||
<h2>Examples</h2>
|
||||
<h3>YouTube Embed with Custom Size</h3>
|
||||
<div class="code-example">
|
||||
<span class="code-label">HTML</span>
|
||||
<pre><code><lazy-video
|
||||
src="https://www.youtube.com/embed/wPr3kws2prM"
|
||||
title="Till We Have Faces by Silent Planet"
|
||||
width="50%"
|
||||
height="260px"
|
||||
thumbnail-quality="maxres">
|
||||
</lazy-video></code></pre>
|
||||
</div>
|
||||
<div class="example">
|
||||
<lazy-video src="https://www.youtube.com/embed/wPr3kws2prM" title="Till We Have Faces by Silent Planet" width="50%" height="260px"></lazy-video>
|
||||
</div>
|
||||
<h3>Bitchute with Autoload Off</h3>
|
||||
<div class="code-example">
|
||||
<span class="code-label">HTML</span>
|
||||
<pre><code><lazy-video
|
||||
src="https://www.bitchute.com/video/zSfeNPF-OpY"
|
||||
title="Trump Assassination Attempt Documents LOCKED Away. What are they Hiding?"
|
||||
autoload="false"
|
||||
thumbnail="https://static-3.bitchute.com/live/cover_images/nDPZqzyLkFKW/zSfeNPF-OpY_640x360.jpg">
|
||||
</lazy-video></code></pre>
|
||||
</div>
|
||||
<div class="example">
|
||||
<lazy-video
|
||||
src="https://www.bitchute.com/video/zSfeNPF-OpY"
|
||||
title="Trump Assassination Attempt Documents LOCKED Away. What are they Hiding?"
|
||||
autoload="false"
|
||||
thumbnail="https://static-3.bitchute.com/live/cover_images/nDPZqzyLkFKW/zSfeNPF-OpY_640x360.jpg">
|
||||
</lazy-video>
|
||||
<div class="note">
|
||||
<p>
|
||||
With <code>autoload="false"</code> on Bitchute, users need to click twice: once to load the player, and again to play. This saves bandwidth but may be less convenient.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<h3>Bitchute with Autoload</h3>
|
||||
<div class="code-example">
|
||||
<span class="code-label">HTML</span>
|
||||
<pre><code><lazy-video
|
||||
src="https://www.bitchute.com/video/zSfeNPF-OpY"
|
||||
title="Trump Assassination Attempt Documents LOCKED Away. What are they Hiding?">
|
||||
</lazy-video></code></pre>
|
||||
</div>
|
||||
<div class="example">
|
||||
<lazy-video
|
||||
src="https://www.bitchute.com/video/zSfeNPF-OpY"
|
||||
title="Trump Assassination Attempt Documents LOCKED Away. What are they Hiding?">
|
||||
</lazy-video>
|
||||
</div>
|
||||
<h3>Responsive Container (FitVids Style)</h3>
|
||||
<div class="code-example">
|
||||
<span class="code-label">HTML</span>
|
||||
<pre><code><div style="max-width: 100%; width: 100%;">
|
||||
<lazy-video
|
||||
src="https://www.youtube.com/embed/wPr3kws2prM"
|
||||
title="Responsive container example"
|
||||
container-fit="true">
|
||||
</lazy-video>
|
||||
</div></code></pre>
|
||||
</div>
|
||||
<div class="example">
|
||||
<div style="max-width: 100%; width: 100%;">
|
||||
<lazy-video
|
||||
src="https://www.youtube.com/embed/wPr3kws2prM"
|
||||
title="Till We Have Faces by Silent Planet"
|
||||
container-fit="true">
|
||||
</lazy-video>
|
||||
</div>
|
||||
<div class="note">
|
||||
<p>
|
||||
<code>container-fit="true"</code> makes the video fill its parent container while keeping the aspect ratio. Great for fluid layouts.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<h3>YouTube with Hidden Title Bar</h3>
|
||||
<div class="code-example">
|
||||
<span class="code-label">HTML</span>
|
||||
<pre><code><lazy-video
|
||||
src="https://www.youtube.com/embed/wPr3kws2prM"
|
||||
title="Hidden title example"
|
||||
hide-title>
|
||||
</lazy-video></code></pre>
|
||||
</div>
|
||||
<div class="example">
|
||||
<lazy-video
|
||||
src="https://www.youtube.com/embed/wPr3kws2prM"
|
||||
title="Hidden title example"
|
||||
hide-title>
|
||||
</lazy-video>
|
||||
</div>
|
||||
<h3>Global Title Control with CSS</h3>
|
||||
<div class="code-example">
|
||||
<span class="code-label">CSS</span>
|
||||
<pre><code>/* Hide titles for all videos */
|
||||
lazy-video {
|
||||
--lv-show-title: none;
|
||||
}
|
||||
|
||||
/* Hide titles for a group */
|
||||
.article-videos lazy-video {
|
||||
--lv-show-title: none;
|
||||
}</code></pre>
|
||||
</div>
|
||||
<h3>Global Alignment Control with CSS</h3>
|
||||
<div class="code-example">
|
||||
<span class="code-label">CSS</span>
|
||||
<pre><code>/* Set alignment for all videos */
|
||||
lazy-video {
|
||||
--lv-align: left;
|
||||
}
|
||||
|
||||
/* Responsive alignment */
|
||||
@media (max-width: 768px) {
|
||||
lazy-video {
|
||||
--lv-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Different alignments for different contexts */
|
||||
.sidebar lazy-video {
|
||||
--lv-align: right;
|
||||
}</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="converting" class="section">
|
||||
<h2>Converting Existing iframes</h2>
|
||||
<p>
|
||||
You can convert existing video iframes to <code><lazy-video></code> by simply changing the tag name.
|
||||
</p>
|
||||
<p>Standard YouTube iframe:</p>
|
||||
<div class="code-example">
|
||||
<span class="code-label">HTML</span>
|
||||
<pre><code><iframe
|
||||
src="https://www.youtube.com/embed/wPr3kws2prM?start=30&rel=0&controls=0"
|
||||
width="560"
|
||||
height="315"
|
||||
title="Till We Have Faces by Silent Planet"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
</code></pre>
|
||||
</div>
|
||||
<p>Converted to <code><lazy-video></code> (just change the tag):</p>
|
||||
<div class="code-example">
|
||||
<span class="code-label">HTML</span>
|
||||
<pre><code><lazy-video
|
||||
src="https://www.youtube.com/embed/wPr3kws2prM?start=30&rel=0&controls=0"
|
||||
width="560"
|
||||
height="315"
|
||||
title="Till We Have Faces by Silent Planet"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen>
|
||||
</lazy-video></code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="security" class="section">
|
||||
<h2>Security & Privacy</h2>
|
||||
<p>
|
||||
<code><lazy-video></code> is built with modern web security and privacy best practices:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
All embedded iframes use the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/credentialless" target="_blank"><code>credentialless</code></a> attribute. This helps prevent credential leaks and keeps third-party content isolated from your site's cookies and storage.
|
||||
</li>
|
||||
<li>
|
||||
The <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/iframe#sandbox" target="_blank"><code>sandbox</code></a> attribute is set by default, restricting what the embedded player can do and reducing risk from third-party content.
|
||||
</li>
|
||||
<li>
|
||||
For YouTube, the <code>youtube-nocookie.com</code> domain is used by default, so no tracking cookies are set unless the user interacts with the video.
|
||||
</li>
|
||||
</ul>
|
||||
<div class="note">
|
||||
<p>
|
||||
<strong>Note:</strong> You can override the <code>sandbox</code> attribute if you need to enable additional features, but the default is designed for maximum safety.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="browser-support" class="section">
|
||||
<h2>Browser Support</h2>
|
||||
<p>
|
||||
Works in all modern browsers (Chrome, Firefox, Safari, Edge). Uses standard web component APIs. For IE11 or older, use the <a href="https://github.com/webcomponents/polyfills/tree/master/packages/custom-elements" target="_blank">custom-elements polyfill</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="breaking-change" class="section">
|
||||
<h2>Breaking Change</h2>
|
||||
<p>
|
||||
<strong>April 3, 2025:</strong> The old <code><lazy-youtube></code> element is no longer supported. Please update any code to use <code><lazy-video></code> instead.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="doc-version-note">These docs reflect the latest release version of <strong>@lv.js</strong>.</div>
|
||||
<p>Last updated: Friday, April 11th, 2025</p>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in a new issue