Borg/search/search_index.json

1 line
No EOL
40 KiB
JSON

{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"","title":"Borg","text":"<p>Borg is a command-line tool for collecting resources from various URIs (like Git repositories and websites) into a unified format.</p>"},{"location":"#installation","title":"Installation","text":"<p>You can install Borg using <code>go install</code>:</p> <pre><code>go install github.com/Snider/Borg@latest\n</code></pre>"},{"location":"#usage","title":"Usage","text":"<p>Borg provides several subcommands for collecting different types of resources.</p>"},{"location":"#borg-collect","title":"<code>borg collect</code>","text":"<p>The <code>collect</code> command is the main entry point for collecting resources. It has several subcommands for different resource types.</p>"},{"location":"#borg-collect-github-repo","title":"<code>borg collect github repo</code>","text":"<p>This command collects a single Git repository and stores it in a DataNode.</p> <pre><code>./borg collect github repo https://github.com/Snider/Borg --output borg.dat\n</code></pre>"},{"location":"#borg-collect-github-release","title":"<code>borg collect github release</code>","text":"<p>This command downloads and packages the assets from a GitHub release.</p> <pre><code>./borg collect github release https://github.com/Snider/Borg/releases/latest --output borg-release.dat\n</code></pre>"},{"location":"#borg-collect-pwa","title":"<code>borg collect pwa</code>","text":"<p>This command collects a Progressive Web App (PWA) from a given URI.</p> <pre><code>./borg collect pwa --uri https://squoosh.app --output squoosh.dat\n</code></pre>"},{"location":"#borg-collect-website","title":"<code>borg collect website</code>","text":"<p>This command collects a single website and stores it in a DataNode.</p> <pre><code>./borg collect website https://example.com --output example.dat\n</code></pre>"},{"location":"#borg-all","title":"<code>borg all</code>","text":"<p>The <code>borg all</code> command collects all public repositories from a GitHub user or organization.</p> <pre><code>./borg all https://github.com/Snider --output snider.dat\n</code></pre>"},{"location":"#borg-compile","title":"<code>borg compile</code>","text":"<p>The <code>borg compile</code> command compiles a <code>Borgfile</code> into a Terminal Isolation Matrix.</p> <pre><code>./borg compile --file Borgfile --output a.tim\n</code></pre>"},{"location":"#borg-run","title":"<code>borg run</code>","text":"<p>The <code>borg run</code> command executes a Terminal Isolation Matrix.</p> <pre><code>./borg run a.tim\n</code></pre>"},{"location":"#borg-serve","title":"<code>borg serve</code>","text":"<p>The <code>borg serve</code> command serves a DataNode or Terminal Isolation Matrix using a static file server.</p> <pre><code>./borg serve my-collected-data.dat --port 8080\n</code></pre>"},{"location":"#borg-decode","title":"<code>borg decode</code>","text":"<p>The <code>borg decode</code> command decodes a <code>.trix</code> or <code>.tim</code> file.</p> <pre><code>./borg decode my-collected-data.trix --output my-collected-data.dat\n</code></pre>"},{"location":"#formats","title":"Formats","text":"<p>Borg supports three output formats: <code>datanode</code>, <code>tim</code>, and <code>trix</code>.</p>"},{"location":"#datanode","title":"DataNode","text":"<p>The <code>datanode</code> format is a simple tarball containing the collected resources. This is the default format.</p>"},{"location":"#terminal-isolation-matrix-tim","title":"Terminal Isolation Matrix (TIM)","text":"<p>The Terminal Isolation Matrix (<code>tim</code>) is a <code>runc</code> bundle that can be executed in an isolated environment. This is useful for analyzing potentially malicious code without affecting the host system. A <code>.tim</code> file is a specialized <code>.trix</code> file with the <code>tim</code> flag set in its header.</p> <p>To create a TIM, use the <code>--format tim</code> flag with any of the <code>collect</code> subcommands.</p> <pre><code>./borg collect github repo https://github.com/Snider/Borg --output borg.tim --format tim\n</code></pre>"},{"location":"#trix","title":"Trix","text":"<p>The <code>trix</code> format is an encrypted and structured file format. It is used as the underlying format for <code>.tim</code> files, but can also be used on its own for encrypting any <code>DataNode</code>.</p> <p>To create a <code>.trix</code> file, use the <code>--format trix</code> flag with any of the <code>collect</code> subcommands.</p> <pre><code>./borg collect github repo https://github.com/Snider/Borg --output borg.trix --format trix --password \"my-secret-password\"\n</code></pre>"},{"location":"#encryption","title":"Encryption","text":"<p>Both the <code>tim</code> and <code>trix</code> formats can be encrypted with a password by using the <code>--password</code> flag.</p>"},{"location":"#decoding","title":"Decoding","text":"<p>To decode a <code>.trix</code> or <code>.tim</code> file, use the <code>decode</code> command. If the file is encrypted, you must provide the <code>--password</code> flag.</p> <pre><code>./borg decode borg.trix --output borg.dat --password \"my-secret-password\"\n</code></pre> <p>If you are decoding a <code>.tim</code> file, you must also provide the <code>--i-am-in-isolation</code> flag. This is a safety measure to prevent you from accidentally executing potentially malicious code on your host system.</p> <pre><code>./borg decode borg.tim --output borg.dat --i-am-in-isolation\n</code></pre>"},{"location":"cli/","title":"CLI Usage","text":"<p><code>borg</code> is a command-line tool for collecting repositories, websites, and PWAs into portable data artifacts (DataNodes) or Terminal Isolation Matrices.</p> <p>Use <code>borg --help</code> and <code>borg &lt;command&gt; --help</code> to see all flags.</p>"},{"location":"cli/#top-level","title":"Top-level","text":"<ul> <li><code>borg --help</code></li> <li><code>borg --version</code></li> </ul>"},{"location":"cli/#commands","title":"Commands","text":""},{"location":"cli/#collect","title":"collect","text":"<p>Collect and package inputs.</p> <p>Subcommands: - <code>borg collect github repo &lt;repo-url&gt; [--output &lt;file&gt;] [--format datanode|tim|trix] [--compression none|gz|xz]</code> - <code>borg collect github release &lt;release-url&gt; [--output &lt;file&gt;]</code> - <code>borg collect github repos &lt;org-or-user&gt; [--output &lt;file&gt;] [--format ...] [--compression ...]</code> - <code>borg collect website &lt;url&gt; [--depth N] [--output &lt;file&gt;] [--format ...] [--compression ...]</code> - <code>borg collect pwa --uri &lt;url&gt; [--output &lt;file&gt;] [--format ...] [--compression ...]</code></p> <p>Examples: - <code>borg collect github repo https://github.com/Snider/Borg --output borg.dat</code> - <code>borg collect website https://example.com --depth 1 --output site.dat</code> - <code>borg collect pwa --uri https://squoosh.app --output squoosh.dat</code></p>"},{"location":"cli/#all","title":"all","text":"<p>Collect all public repositories from a GitHub user or organization.</p> <ul> <li><code>borg all &lt;url&gt; [--output &lt;file&gt;]</code></li> </ul> <p>Example: - <code>borg all https://github.com/Snider --output snider.dat</code></p>"},{"location":"cli/#compile","title":"compile","text":"<p>Compile a Borgfile into a Terminal Isolation Matrix (TIM).</p> <ul> <li><code>borg compile [--file &lt;Borgfile&gt;] [--output &lt;file&gt;]</code></li> </ul> <p>Example: - <code>borg compile --file Borgfile --output a.tim</code></p>"},{"location":"cli/#run","title":"run","text":"<p>Execute a Terminal Isolation Matrix (TIM).</p> <ul> <li><code>borg run &lt;tim-file&gt;</code></li> </ul> <p>Example: - <code>borg run a.tim</code></p>"},{"location":"cli/#serve","title":"serve","text":"<p>Serve a packaged DataNode or TIM via a static file server.</p> <ul> <li><code>borg serve &lt;file&gt; [--port 8080]</code></li> </ul> <p>Examples: - <code>borg serve squoosh.dat --port 8888</code> - <code>borg serve borg.tim --port 9999</code></p>"},{"location":"cli/#decode","title":"decode","text":"<p>Decode a <code>.trix</code> or <code>.tim</code> file back into a DataNode (<code>.dat</code>).</p> <ul> <li><code>borg decode &lt;file&gt; [--output &lt;file&gt;] [--password &lt;password&gt;]</code></li> </ul> <p>Examples: - <code>borg decode borg.trix --output borg.dat --password \"secret\"</code> - <code>borg decode borg.tim --output borg.dat --i-am-in-isolation</code></p>"},{"location":"cli/#compression","title":"Compression","text":"<p>All collect commands accept <code>--compression</code> with values: - <code>none</code> (default) - <code>gz</code> - <code>xz</code></p> <p>Output filenames gain the appropriate extension automatically.</p>"},{"location":"cli/#formats","title":"Formats","text":"<p>Borg supports three output formats via the <code>--format</code> flag:</p> <ul> <li><code>datanode</code>: A simple tarball containing the collected resources. (Default)</li> <li><code>tim</code>: Terminal Isolation Matrix, a runc-compatible bundle.</li> <li><code>trix</code>: Encrypted and structured file format.</li> </ul>"},{"location":"development/","title":"Development","text":"<p>Prerequisites: - Go 1.25 or newer - Task (optional) \u2014 https://taskfile.dev - MkDocs Material (optional for docs) \u2014 <code>pip install mkdocs-material</code></p>"},{"location":"development/#workspace","title":"Workspace","text":"<p>This repo includes a <code>go.work</code> file configured for Go 1.25 to align with common workflows.</p>"},{"location":"development/#build","title":"Build","text":"<ul> <li><code>go build ./...</code></li> <li><code>task build</code></li> </ul>"},{"location":"development/#test","title":"Test","text":"<ul> <li><code>go test ./...</code></li> <li><code>task test</code></li> </ul> <p>Note: Some tests may require network or git tooling depending on environment (e.g., pushing to a temporary repo).</p>"},{"location":"development/#run","title":"Run","text":"<ul> <li><code>task run</code></li> <li><code>./borg --help</code></li> </ul>"},{"location":"development/#docs","title":"Docs","text":"<p>Serve the documentation locally with MkDocs:</p> <ul> <li><code>pip install mkdocs-material</code></li> <li><code>mkdocs serve</code></li> </ul> <p>The site configuration lives in <code>mkdocs.yml</code> and content in <code>docs/</code>.</p>"},{"location":"installation/","title":"Installation","text":"<p>This project builds a single binary named <code>borg</code>.</p> <p>Options to install:</p> <ul> <li>From source (requires Go 1.25 or newer):</li> <li>Clone the repository and build:<ul> <li><code>go build -o borg ./</code></li> </ul> </li> <li> <p>Or use the Taskfile:</p> <ul> <li><code>task build</code></li> </ul> </li> <li> <p>From releases (recommended):</p> </li> <li>Download an archive for your OS/ARCH from GitHub Releases once you publish with GoReleaser.</li> <li> <p>Unpack and place <code>borg</code> on your PATH.</p> </li> <li> <p>Homebrew (if you publish to a tap):</p> </li> <li><code>brew tap Snider/homebrew-tap</code></li> <li><code>brew install borg</code></li> </ul> <p>Requirements: - Go 1.25+ to build from source. - macOS, Linux, Windows, or FreeBSD on amd64/arm64; Linux ARM v6/v7 supported.</p>"},{"location":"ipfs-distribution/","title":"IPFS Distribution Guide","text":"<p>This guide explains how to distribute your encrypted <code>.smsg</code> content via IPFS (InterPlanetary File System) for permanent, decentralized hosting.</p>"},{"location":"ipfs-distribution/#why-ipfs","title":"Why IPFS?","text":"<p>IPFS is ideal for dapp.fm content because:</p> <ul> <li>Permanent links - Content-addressed (CID) means the URL never changes</li> <li>No hosting costs - Pin with free services or self-host</li> <li>Censorship resistant - No single point of failure</li> <li>Global CDN - Content served from nearest peer</li> <li>Perfect for archival - Your content survives even if you disappear</li> </ul> <p>Combined with password-as-license, IPFS creates truly permanent media distribution:</p> <pre><code>Artist uploads to IPFS \u2192 Fan downloads from anywhere \u2192 Password unlocks forever\n</code></pre>"},{"location":"ipfs-distribution/#quick-start","title":"Quick Start","text":""},{"location":"ipfs-distribution/#1-install-ipfs","title":"1. Install IPFS","text":"<p>macOS:</p> <pre><code>brew install ipfs\n</code></pre> <p>Linux:</p> <pre><code>wget https://dist.ipfs.tech/kubo/v0.24.0/kubo_v0.24.0_linux-amd64.tar.gz\ntar xvfz kubo_v0.24.0_linux-amd64.tar.gz\nsudo mv kubo/ipfs /usr/local/bin/\n</code></pre> <p>Windows: Download from https://dist.ipfs.tech/#kubo</p>"},{"location":"ipfs-distribution/#2-initialize-and-start","title":"2. Initialize and Start","text":"<pre><code>ipfs init\nipfs daemon\n</code></pre>"},{"location":"ipfs-distribution/#3-add-your-content","title":"3. Add Your Content","text":"<pre><code># Create your encrypted content first\ngo run ./cmd/mkdemo my-album.mp4 my-album.smsg\n\n# Add to IPFS\nipfs add my-album.smsg\n# Output: added QmX...abc my-album.smsg\n\n# Your content is now available at:\n# - Local: http://localhost:8080/ipfs/QmX...abc\n# - Gateway: https://ipfs.io/ipfs/QmX...abc\n</code></pre>"},{"location":"ipfs-distribution/#distribution-workflow","title":"Distribution Workflow","text":""},{"location":"ipfs-distribution/#for-artists","title":"For Artists","text":"<pre><code># 1. Package your media\ngo run ./cmd/mkdemo album.mp4 album.smsg\n# Save the password: PMVXogAJNVe_DDABfTmLYztaJAzsD0R7\n\n# 2. Add to IPFS\nipfs add album.smsg\n# added QmYourContentCID album.smsg\n\n# 3. Pin for persistence (choose one):\n\n# Option A: Pin locally (requires running node)\nipfs pin add QmYourContentCID\n\n# Option B: Use Pinata (free tier: 1GB)\ncurl -X POST \"https://api.pinata.cloud/pinning/pinByHash\" \\\n -H \"Authorization: Bearer YOUR_JWT\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"hashToPin\": \"QmYourContentCID\"}'\n\n# Option C: Use web3.storage (free tier: 5GB)\n# Upload at https://web3.storage\n\n# 4. Share with fans\n# CID: QmYourContentCID\n# Password: PMVXogAJNVe_DDABfTmLYztaJAzsD0R7\n# Gateway URL: https://ipfs.io/ipfs/QmYourContentCID\n</code></pre>"},{"location":"ipfs-distribution/#for-fans","title":"For Fans","text":"<pre><code># Download via any gateway\ncurl -o album.smsg https://ipfs.io/ipfs/QmYourContentCID\n\n# Or via local node (faster if running)\nipfs get QmYourContentCID -o album.smsg\n\n# Play with password in browser demo or native app\n</code></pre>"},{"location":"ipfs-distribution/#ipfs-gateways","title":"IPFS Gateways","text":"<p>Public gateways for sharing (no IPFS node required):</p> Gateway URL Pattern Notes ipfs.io <code>https://ipfs.io/ipfs/{CID}</code> Official, reliable dweb.link <code>https://{CID}.ipfs.dweb.link</code> Subdomain style cloudflare <code>https://cloudflare-ipfs.com/ipfs/{CID}</code> Fast, cached w3s.link <code>https://{CID}.ipfs.w3s.link</code> web3.storage nftstorage.link <code>https://{CID}.ipfs.nftstorage.link</code> NFT.storage <p>Example URLs for CID <code>QmX...abc</code>:</p> <pre><code>https://ipfs.io/ipfs/QmX...abc\nhttps://QmX...abc.ipfs.dweb.link\nhttps://cloudflare-ipfs.com/ipfs/QmX...abc\n</code></pre>"},{"location":"ipfs-distribution/#pinning-services","title":"Pinning Services","text":"<p>Content on IPFS is only available while someone is hosting it. Use pinning services for persistence:</p>"},{"location":"ipfs-distribution/#free-options","title":"Free Options","text":"Service Free Tier Link Pinata 1 GB https://pinata.cloud web3.storage 5 GB https://web3.storage NFT.storage Unlimited* https://nft.storage Filebase 5 GB https://filebase.com <p>*NFT.storage is designed for NFT data but works for any content.</p>"},{"location":"ipfs-distribution/#pin-via-cli","title":"Pin via CLI","text":"<pre><code># Pinata\nexport PINATA_JWT=\"your-jwt-token\"\ncurl -X POST \"https://api.pinata.cloud/pinning/pinByHash\" \\\n -H \"Authorization: Bearer $PINATA_JWT\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"hashToPin\": \"QmYourCID\", \"pinataMetadata\": {\"name\": \"my-album.smsg\"}}'\n\n# web3.storage (using w3 CLI)\nnpm install -g @web3-storage/w3cli\nw3 login your@email.com\nw3 up my-album.smsg\n</code></pre>"},{"location":"ipfs-distribution/#integration-with-demo-page","title":"Integration with Demo Page","text":"<p>The demo page can load content directly from IPFS gateways:</p> <pre><code>// In the demo page, use gateway URL\nconst ipfsCID = 'QmYourContentCID';\nconst gatewayUrl = `https://ipfs.io/ipfs/${ipfsCID}`;\n\n// Fetch and decrypt\nconst response = await fetch(gatewayUrl);\nconst bytes = new Uint8Array(await response.arrayBuffer());\nconst msg = await BorgSMSG.decryptBinary(bytes, password);\n</code></pre> <p>Or use the Fan tab with the IPFS gateway URL directly.</p>"},{"location":"ipfs-distribution/#best-practices","title":"Best Practices","text":""},{"location":"ipfs-distribution/#1-always-pin-your-content","title":"1. Always Pin Your Content","text":"<p>IPFS garbage-collects unpinned content. Always pin important files:</p> <pre><code>ipfs pin add QmYourCID\n# Or use a pinning service\n</code></pre>"},{"location":"ipfs-distribution/#2-use-multiple-pins","title":"2. Use Multiple Pins","text":"<p>Pin with 2-3 services for redundancy:</p> <pre><code># Pin locally\nipfs pin add QmYourCID\n\n# Also pin with Pinata\ncurl -X POST \"https://api.pinata.cloud/pinning/pinByHash\" ...\n\n# And web3.storage as backup\nw3 up my-album.smsg\n</code></pre>"},{"location":"ipfs-distribution/#3-share-cid-password-separately","title":"3. Share CID + Password Separately","text":"<pre><code>Download: https://ipfs.io/ipfs/QmYourCID\nLicense: [sent via email/DM after purchase]\n</code></pre>"},{"location":"ipfs-distribution/#4-use-ipns-for-updates-optional","title":"4. Use IPNS for Updates (Optional)","text":"<p>IPNS lets you update content while keeping the same URL:</p> <pre><code># Create IPNS name\nipfs name publish QmYourCID\n# Published to k51...xyz\n\n# Your content is now at:\n# https://ipfs.io/ipns/k51...xyz\n\n# Update to new version later:\nipfs name publish QmNewVersionCID\n</code></pre>"},{"location":"ipfs-distribution/#example-full-album-release","title":"Example: Full Album Release","text":"<pre><code># 1. Create encrypted album\ngo run ./cmd/mkdemo my-album.mp4 my-album.smsg\n# Password: PMVXogAJNVe_DDABfTmLYztaJAzsD0R7\n\n# 2. Add to IPFS\nipfs add my-album.smsg\n# added QmAlbumCID my-album.smsg\n\n# 3. Pin with multiple services\nipfs pin add QmAlbumCID\nw3 up my-album.smsg\n\n# 4. Create release page\ncat &gt; release.html &lt;&lt; 'EOF'\n&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n&lt;head&gt;&lt;title&gt;My Album - Download&lt;/title&gt;&lt;/head&gt;\n&lt;body&gt;\n &lt;h1&gt;My Album&lt;/h1&gt;\n &lt;p&gt;Download: &lt;a href=\"https://ipfs.io/ipfs/QmAlbumCID\"&gt;IPFS&lt;/a&gt;&lt;/p&gt;\n &lt;p&gt;After purchase, you'll receive your license key via email.&lt;/p&gt;\n &lt;p&gt;&lt;a href=\"https://demo.dapp.fm\"&gt;Play with license key&lt;/a&gt;&lt;/p&gt;\n&lt;/body&gt;\n&lt;/html&gt;\nEOF\n\n# 5. Host release page on IPFS too!\nipfs add release.html\n# added QmReleaseCID release.html\n# Share: https://ipfs.io/ipfs/QmReleaseCID\n</code></pre>"},{"location":"ipfs-distribution/#troubleshooting","title":"Troubleshooting","text":""},{"location":"ipfs-distribution/#content-not-loading","title":"Content Not Loading","text":"<ol> <li>Check if pinned: <code>ipfs pin ls | grep QmYourCID</code></li> <li>Try different gateway: Some gateways cache slowly</li> <li>Check daemon running: <code>ipfs swarm peers</code> should show peers</li> </ol>"},{"location":"ipfs-distribution/#slow-downloads","title":"Slow Downloads","text":"<ol> <li>Use a faster gateway (cloudflare-ipfs.com is often fastest)</li> <li>Run your own IPFS node for direct access</li> <li>Pre-warm gateways by accessing content once</li> </ol>"},{"location":"ipfs-distribution/#cid-changed-after-re-adding","title":"CID Changed After Re-adding","text":"<p>IPFS CIDs are content-addressed. If you modify the file, the CID changes. For the same content, the CID is always identical.</p>"},{"location":"ipfs-distribution/#resources","title":"Resources","text":"<ul> <li>IPFS Documentation</li> <li>Pinata Docs</li> <li>web3.storage Docs</li> <li>IPFS Gateway Checker</li> </ul>"},{"location":"library/","title":"Library Usage","text":"<p>Borg can also be used as a Go library. The public API is exposed under the <code>pkg</code> directory. Import paths use the module <code>github.com/Snider/Borg</code>.</p>"},{"location":"library/#collecting-a-github-repo-into-a-datanode","title":"Collecting a GitHub repo into a DataNode","text":"<pre><code>package main\n\nimport (\n \"log\"\n \"os\"\n\n \"github.com/Snider/Borg/pkg/vcs\"\n)\n\nfunc main() {\n // Clone and package the repository\n cloner := vcs.NewGitCloner()\n dn, err := cloner.CloneGitRepository(\"https://github.com/Snider/Borg\", nil)\n if err != nil {\n log.Fatal(err)\n }\n\n // Save to disk\n tarBytes, err := dn.ToTar()\n if err != nil {\n log.Fatal(err)\n }\n if err := os.WriteFile(\"repo.dat\", tarBytes, 0644); err != nil {\n log.Fatal(err)\n }\n}\n</code></pre>"},{"location":"library/#collecting-a-website","title":"Collecting a Website","text":"<pre><code>package main\n\nimport (\n \"log\"\n \"os\"\n\n \"github.com/Snider/Borg/pkg/website\"\n)\n\nfunc main() {\n // Download and package the website\n // 1 is the depth (0 = just the page, 1 = page + links on page)\n dn, err := website.DownloadAndPackageWebsite(\"https://example.com\", 1, nil)\n if err != nil {\n log.Fatal(err)\n }\n\n // Save to disk\n tarBytes, err := dn.ToTar()\n if err != nil {\n log.Fatal(err)\n }\n if err := os.WriteFile(\"website.dat\", tarBytes, 0644); err != nil {\n log.Fatal(err)\n }\n}\n</code></pre>"},{"location":"library/#pwa-collection","title":"PWA Collection","text":"<pre><code>package main\n\nimport (\n \"log\"\n \"os\"\n\n \"github.com/Snider/Borg/pkg/pwa\"\n)\n\nfunc main() {\n client := pwa.NewPWAClient()\n pwaURL := \"https://squoosh.app\"\n\n // Find the manifest\n manifestURL, err := client.FindManifest(pwaURL)\n if err != nil {\n log.Fatal(err)\n }\n\n // Download and package the PWA\n dn, err := client.DownloadAndPackagePWA(pwaURL, manifestURL, nil)\n if err != nil {\n log.Fatal(err)\n }\n\n // Save to disk\n tarBytes, err := dn.ToTar()\n if err != nil {\n log.Fatal(err)\n }\n if err := os.WriteFile(\"pwa.dat\", tarBytes, 0644); err != nil {\n log.Fatal(err)\n }\n}\n</code></pre>"},{"location":"library/#logging","title":"Logging","text":"<p>The package <code>pkg/logger</code> provides helpers for configuring output. See <code>pkg/logger</code> tests for examples.</p>"},{"location":"library/#notes","title":"Notes","text":"<ul> <li>Import paths throughout this repository already use the module path and should work when consumed via <code>go get github.com/Snider/Borg@latest</code>.</li> <li>The exact function names may differ; consult GoDoc/pkg.go.dev for up-to-date signatures based on the current code.</li> </ul>"},{"location":"payment-integration/","title":"Payment Integration Guide","text":"<p>This guide shows how to sell your encrypted <code>.smsg</code> content and deliver license keys (passwords) to customers using popular payment processors.</p>"},{"location":"payment-integration/#overview","title":"Overview","text":"<p>The dapp.fm model is simple:</p> <pre><code>1. Customer pays via Stripe/Gumroad/PayPal\n2. Payment processor triggers webhook or delivers digital product\n3. Customer receives password (license key)\n4. Customer downloads .smsg from your CDN/IPFS\n5. Customer decrypts with password - done forever\n</code></pre> <p>No license servers, no accounts, no ongoing infrastructure.</p>"},{"location":"payment-integration/#stripe-integration","title":"Stripe Integration","text":""},{"location":"payment-integration/#option-1-stripe-payment-links-easiest","title":"Option 1: Stripe Payment Links (Easiest)","text":"<p>No code required - use Stripe's hosted checkout:</p> <ol> <li>Create a Payment Link in Stripe Dashboard</li> <li>Set up a webhook to email the password on successful payment</li> <li>Host your <code>.smsg</code> file anywhere (CDN, IPFS, S3)</li> </ol> <p>Webhook endpoint (Node.js/Express):</p> <pre><code>const express = require('express');\nconst stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);\nconst nodemailer = require('nodemailer');\n\nconst app = express();\n\n// Your content passwords (store securely!)\nconst PRODUCTS = {\n 'prod_ABC123': {\n name: 'My Album',\n password: 'PMVXogAJNVe_DDABfTmLYztaJAzsD0R7',\n downloadUrl: 'https://ipfs.io/ipfs/QmYourCID'\n }\n};\n\napp.post('/webhook', express.raw({type: 'application/json'}), async (req, res) =&gt; {\n const sig = req.headers['stripe-signature'];\n const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;\n\n let event;\n try {\n event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);\n } catch (err) {\n return res.status(400).send(`Webhook Error: ${err.message}`);\n }\n\n if (event.type === 'checkout.session.completed') {\n const session = event.data.object;\n const customerEmail = session.customer_details.email;\n const productId = session.metadata.product_id;\n const product = PRODUCTS[productId];\n\n if (product) {\n await sendLicenseEmail(customerEmail, product);\n }\n }\n\n res.json({received: true});\n});\n\nasync function sendLicenseEmail(email, product) {\n const transporter = nodemailer.createTransport({\n // Configure your email provider\n service: 'gmail',\n auth: {\n user: process.env.EMAIL_USER,\n pass: process.env.EMAIL_PASS\n }\n });\n\n await transporter.sendMail({\n from: 'artist@example.com',\n to: email,\n subject: `Your License Key for ${product.name}`,\n html: `\n &lt;h1&gt;Thank you for your purchase!&lt;/h1&gt;\n &lt;p&gt;&lt;strong&gt;Download:&lt;/strong&gt; &lt;a href=\"${product.downloadUrl}\"&gt;${product.name}&lt;/a&gt;&lt;/p&gt;\n &lt;p&gt;&lt;strong&gt;License Key:&lt;/strong&gt; &lt;code&gt;${product.password}&lt;/code&gt;&lt;/p&gt;\n &lt;p&gt;&lt;strong&gt;How to play:&lt;/strong&gt;&lt;/p&gt;\n &lt;ol&gt;\n &lt;li&gt;Download the .smsg file from the link above&lt;/li&gt;\n &lt;li&gt;Go to &lt;a href=\"https://demo.dapp.fm\"&gt;demo.dapp.fm&lt;/a&gt;&lt;/li&gt;\n &lt;li&gt;Click \"Fan\" tab, then \"Unlock Licensed Content\"&lt;/li&gt;\n &lt;li&gt;Paste the file and enter your license key&lt;/li&gt;\n &lt;/ol&gt;\n &lt;p&gt;This is your permanent license - save this email!&lt;/p&gt;\n `\n });\n}\n\napp.listen(3000);\n</code></pre>"},{"location":"payment-integration/#option-2-stripe-checkout-session-more-control","title":"Option 2: Stripe Checkout Session (More Control)","text":"<pre><code>const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);\n\n// Create checkout session\napp.post('/create-checkout', async (req, res) =&gt; {\n const { productId } = req.body;\n\n const session = await stripe.checkout.sessions.create({\n payment_method_types: ['card'],\n line_items: [{\n price: 'price_ABC123', // Your Stripe price ID\n quantity: 1,\n }],\n mode: 'payment',\n success_url: 'https://yoursite.com/success?session_id={CHECKOUT_SESSION_ID}',\n cancel_url: 'https://yoursite.com/cancel',\n metadata: {\n product_id: productId\n }\n });\n\n res.json({ url: session.url });\n});\n\n// Success page - show license after payment\napp.get('/success', async (req, res) =&gt; {\n const session = await stripe.checkout.sessions.retrieve(req.query.session_id);\n\n if (session.payment_status === 'paid') {\n const product = PRODUCTS[session.metadata.product_id];\n res.send(`\n &lt;h1&gt;Thank you!&lt;/h1&gt;\n &lt;p&gt;Download: &lt;a href=\"${product.downloadUrl}\"&gt;${product.name}&lt;/a&gt;&lt;/p&gt;\n &lt;p&gt;License Key: &lt;code&gt;${product.password}&lt;/code&gt;&lt;/p&gt;\n `);\n } else {\n res.send('Payment not completed');\n }\n});\n</code></pre>"},{"location":"payment-integration/#gumroad-integration","title":"Gumroad Integration","text":"<p>Gumroad is perfect for artists - handles payments, delivery, and customer management.</p>"},{"location":"payment-integration/#setup","title":"Setup","text":"<ol> <li>Create a Digital Product on Gumroad</li> <li>Upload a text file or PDF containing the password</li> <li>Set your <code>.smsg</code> download URL in the product description</li> <li>Gumroad delivers the password file on purchase</li> </ol>"},{"location":"payment-integration/#product-setup","title":"Product Setup","text":"<p>Product Description:</p> <pre><code>My Album - Encrypted Digital Download\n\nAfter purchase, you'll receive:\n1. A license key (in the download)\n2. Download link for the .smsg file\n\nHow to play:\n1. Download the .smsg file: https://ipfs.io/ipfs/QmYourCID\n2. Go to https://demo.dapp.fm\n3. Click \"Fan\" \u2192 \"Unlock Licensed Content\"\n4. Enter your license key from the PDF\n</code></pre> <p>Delivered File (license.txt):</p> <pre><code>Your License Key: PMVXogAJNVe_DDABfTmLYztaJAzsD0R7\n\nDownload your content: https://ipfs.io/ipfs/QmYourCID\n\nThis is your permanent license - keep this file safe!\nThe content works offline forever with this key.\n\nNeed help? Visit https://demo.dapp.fm\n</code></pre>"},{"location":"payment-integration/#gumroad-ping-webhook","title":"Gumroad Ping (Webhook)","text":"<p>For automated delivery, use Gumroad's Ping feature:</p> <pre><code>const express = require('express');\nconst app = express();\n\napp.use(express.urlencoded({ extended: true }));\n\n// Gumroad sends POST to this endpoint on sale\napp.post('/gumroad-ping', (req, res) =&gt; {\n const {\n seller_id,\n product_id,\n email,\n full_name,\n purchaser_id\n } = req.body;\n\n // Verify it's from Gumroad (check seller_id matches yours)\n if (seller_id !== process.env.GUMROAD_SELLER_ID) {\n return res.status(403).send('Invalid seller');\n }\n\n const product = PRODUCTS[product_id];\n if (product) {\n // Send custom email with password\n sendLicenseEmail(email, product);\n }\n\n res.send('OK');\n});\n</code></pre>"},{"location":"payment-integration/#paypal-integration","title":"PayPal Integration","text":""},{"location":"payment-integration/#paypal-buttons-ipn","title":"PayPal Buttons + IPN","text":"<pre><code>&lt;!-- PayPal Buy Button --&gt;\n&lt;form action=\"https://www.paypal.com/cgi-bin/webscr\" method=\"post\"&gt;\n &lt;input type=\"hidden\" name=\"cmd\" value=\"_xclick\"&gt;\n &lt;input type=\"hidden\" name=\"business\" value=\"artist@example.com\"&gt;\n &lt;input type=\"hidden\" name=\"item_name\" value=\"My Album - Digital Download\"&gt;\n &lt;input type=\"hidden\" name=\"item_number\" value=\"album-001\"&gt;\n &lt;input type=\"hidden\" name=\"amount\" value=\"9.99\"&gt;\n &lt;input type=\"hidden\" name=\"currency_code\" value=\"USD\"&gt;\n &lt;input type=\"hidden\" name=\"notify_url\" value=\"https://yoursite.com/paypal-ipn\"&gt;\n &lt;input type=\"hidden\" name=\"return\" value=\"https://yoursite.com/thank-you\"&gt;\n &lt;input type=\"submit\" value=\"Buy Now - $9.99\"&gt;\n&lt;/form&gt;\n</code></pre> <p>IPN Handler:</p> <pre><code>const express = require('express');\nconst axios = require('axios');\n\napp.post('/paypal-ipn', express.urlencoded({ extended: true }), async (req, res) =&gt; {\n // Verify with PayPal\n const verifyUrl = 'https://ipnpb.paypal.com/cgi-bin/webscr';\n const verifyBody = 'cmd=_notify-validate&amp;' + new URLSearchParams(req.body).toString();\n\n const response = await axios.post(verifyUrl, verifyBody);\n\n if (response.data === 'VERIFIED' &amp;&amp; req.body.payment_status === 'Completed') {\n const email = req.body.payer_email;\n const itemNumber = req.body.item_number;\n const product = PRODUCTS[itemNumber];\n\n if (product) {\n await sendLicenseEmail(email, product);\n }\n }\n\n res.send('OK');\n});\n</code></pre>"},{"location":"payment-integration/#ko-fi-integration","title":"Ko-fi Integration","text":"<p>Ko-fi is great for tips and single purchases.</p>"},{"location":"payment-integration/#setup_1","title":"Setup","text":"<ol> <li>Enable \"Commissions\" or \"Shop\" on Ko-fi</li> <li>Create a product with the license key in the thank-you message</li> <li>Link to your .smsg download</li> </ol> <p>Ko-fi Thank You Message:</p> <pre><code>Thank you for your purchase!\n\nYour License Key: PMVXogAJNVe_DDABfTmLYztaJAzsD0R7\n\nDownload: https://ipfs.io/ipfs/QmYourCID\n\nPlay at: https://demo.dapp.fm (Fan \u2192 Unlock Licensed Content)\n</code></pre>"},{"location":"payment-integration/#serverless-options","title":"Serverless Options","text":""},{"location":"payment-integration/#vercelnetlify-functions","title":"Vercel/Netlify Functions","text":"<p>No server needed - use serverless functions:</p> <pre><code>// api/stripe-webhook.js (Vercel)\nimport Stripe from 'stripe';\nimport { Resend } from 'resend';\n\nconst stripe = new Stripe(process.env.STRIPE_SECRET_KEY);\nconst resend = new Resend(process.env.RESEND_API_KEY);\n\nexport default async function handler(req, res) {\n if (req.method !== 'POST') {\n return res.status(405).end();\n }\n\n const sig = req.headers['stripe-signature'];\n const event = stripe.webhooks.constructEvent(\n req.body,\n sig,\n process.env.STRIPE_WEBHOOK_SECRET\n );\n\n if (event.type === 'checkout.session.completed') {\n const session = event.data.object;\n\n await resend.emails.send({\n from: 'artist@yoursite.com',\n to: session.customer_details.email,\n subject: 'Your License Key',\n html: `\n &lt;p&gt;Download: &lt;a href=\"https://ipfs.io/ipfs/QmYourCID\"&gt;My Album&lt;/a&gt;&lt;/p&gt;\n &lt;p&gt;License Key: &lt;code&gt;PMVXogAJNVe_DDABfTmLYztaJAzsD0R7&lt;/code&gt;&lt;/p&gt;\n `\n });\n }\n\n res.json({ received: true });\n}\n\nexport const config = {\n api: { bodyParser: false }\n};\n</code></pre>"},{"location":"payment-integration/#manual-workflow-no-code","title":"Manual Workflow (No Code)","text":"<p>For artists who don't want to set up webhooks:</p>"},{"location":"payment-integration/#using-email","title":"Using Email","text":"<ol> <li>Gumroad/Ko-fi: Set product to require email</li> <li>Manual delivery: Check sales daily, email passwords manually</li> <li>Template:</li> </ol> <pre><code>Subject: Your License for [Album Name]\n\nHi [Name],\n\nThank you for your purchase!\n\nDownload: [IPFS/CDN link]\nLicense Key: [password]\n\nHow to play:\n1. Download the .smsg file\n2. Go to demo.dapp.fm\n3. Fan tab \u2192 Unlock Licensed Content\n4. Enter your license key\n\nEnjoy! This license works forever.\n\n[Artist Name]\n</code></pre>"},{"location":"payment-integration/#using-discordtelegram","title":"Using Discord/Telegram","text":"<ol> <li>Sell via Gumroad (free tier)</li> <li>Require customers join your Discord/Telegram</li> <li>Bot or manual delivery of license keys</li> <li>Community building bonus!</li> </ol>"},{"location":"payment-integration/#security-best-practices","title":"Security Best Practices","text":""},{"location":"payment-integration/#1-one-password-per-product","title":"1. One Password Per Product","text":"<p>Don't reuse passwords across products:</p> <pre><code>const PRODUCTS = {\n 'album-2024': { password: 'unique-key-1' },\n 'album-2023': { password: 'unique-key-2' },\n 'single-summer': { password: 'unique-key-3' }\n};\n</code></pre>"},{"location":"payment-integration/#2-environment-variables","title":"2. Environment Variables","text":"<p>Never hardcode passwords in source:</p> <pre><code># .env\nALBUM_2024_PASSWORD=PMVXogAJNVe_DDABfTmLYztaJAzsD0R7\nSTRIPE_SECRET_KEY=sk_live_...\n</code></pre>"},{"location":"payment-integration/#3-webhook-verification","title":"3. Webhook Verification","text":"<p>Always verify webhooks are from the payment provider:</p> <pre><code>// Stripe\nstripe.webhooks.constructEvent(body, sig, secret);\n\n// Gumroad\nif (seller_id !== MY_SELLER_ID) reject();\n\n// PayPal\nverify with IPN endpoint\n</code></pre>"},{"location":"payment-integration/#4-https-only","title":"4. HTTPS Only","text":"<p>All webhook endpoints must use HTTPS.</p>"},{"location":"payment-integration/#pricing-strategies","title":"Pricing Strategies","text":""},{"location":"payment-integration/#direct-sale-perpetual-license","title":"Direct Sale (Perpetual License)","text":"<ul> <li>Customer pays once, owns forever</li> <li>Single password for all buyers</li> <li>Best for: Albums, films, books</li> </ul>"},{"location":"payment-integration/#time-limited-streamingrental","title":"Time-Limited (Streaming/Rental)","text":"<p>Use dapp.fm Re-Key feature:</p> <ol> <li>Encrypt master copy with master password</li> <li>On purchase, re-key with customer-specific password + expiry</li> <li>Deliver unique password per customer</li> </ol> <pre><code>// On purchase webhook\nconst customerPassword = generateUniquePassword();\nconst expiry = Date.now() + (24 * 60 * 60 * 1000); // 24 hours\n\n// Use WASM or Go to re-key\nconst customerVersion = await rekeyContent(masterSmsg, masterPassword, customerPassword, expiry);\n\n// Deliver customer-specific file + password\n</code></pre>"},{"location":"payment-integration/#tiered-access","title":"Tiered Access","text":"<p>Different passwords for different tiers:</p> <pre><code>const TIERS = {\n 'preview': { password: 'preview-key', expiry: '30s' },\n 'rental': { password: 'rental-key', expiry: '7d' },\n 'own': { password: 'perpetual-key', expiry: null }\n};\n</code></pre>"},{"location":"payment-integration/#example-complete-stripe-setup","title":"Example: Complete Stripe Setup","text":"<pre><code># 1. Create your content\ngo run ./cmd/mkdemo album.mp4 album.smsg\n# Password: PMVXogAJNVe_DDABfTmLYztaJAzsD0R7\n\n# 2. Upload to IPFS\nipfs add album.smsg\n# QmAlbumCID\n\n# 3. Create Stripe product\n# Dashboard \u2192 Products \u2192 Add Product\n# Name: My Album\n# Price: $9.99\n\n# 4. Create Payment Link\n# Dashboard \u2192 Payment Links \u2192 New\n# Select your product\n# Get link: https://buy.stripe.com/xxx\n\n# 5. Set up webhook\n# Dashboard \u2192 Developers \u2192 Webhooks \u2192 Add endpoint\n# URL: https://yoursite.com/api/stripe-webhook\n# Events: checkout.session.completed\n\n# 6. Deploy webhook handler (Vercel example)\nvercel deploy\n\n# 7. Share payment link\n# Fans click \u2192 Pay \u2192 Get email with password \u2192 Download \u2192 Play forever\n</code></pre>"},{"location":"payment-integration/#resources","title":"Resources","text":"<ul> <li>Stripe Webhooks</li> <li>Gumroad Ping</li> <li>PayPal IPN</li> <li>Resend (Email API)</li> <li>Vercel Functions</li> </ul>"},{"location":"releasing/","title":"Releasing","text":"<p>This project is configured for GoReleaser.</p>"},{"location":"releasing/#prerequisites","title":"Prerequisites","text":"<ul> <li>Create a GitHub personal access token with <code>repo</code> scope and export as <code>GITHUB_TOKEN</code> in your shell.</li> <li>Ensure a clean working tree and a tagged commit.</li> <li>Install goreleaser: https://goreleaser.com/install/</li> </ul>"},{"location":"releasing/#snapshot-builds","title":"Snapshot builds","text":"<p>Generate local artifacts without publishing:</p> <ul> <li><code>goreleaser release --snapshot --clean</code></li> </ul> <p>Artifacts appear under <code>dist/</code>.</p>"},{"location":"releasing/#full-release","title":"Full release","text":"<ol> <li>Tag a new version:</li> <li><code>git tag -a v0.1.0 -m \"v0.1.0\"</code></li> <li><code>git push origin v0.1.0</code></li> <li>Run GoReleaser:</li> <li><code>GITHUB_TOKEN=... goreleaser release --clean</code></li> </ol> <p>This will: - Build binaries for multiple OS/ARCH - Produce checksums and archives - Create/update a GitHub Release with changelog - Optionally publish a Homebrew formula (if repository exists and permissions allow)</p>"},{"location":"releasing/#notes","title":"Notes","text":"<ul> <li>The Go toolchain version is 1.25 (see go.mod and go.work).</li> </ul>"}]}