Compare commits

...

5 Commits

Author SHA1 Message Date
woikos
e28ab948b0 Add multi-token support for bunker client connections (v0.44.3)
Some checks failed
Go / build-and-release (push) Has been cancelled
- Each client device now gets its own CAT token
- Tokens can be individually named (editable, defaults to cute names like "jolly-jellyfish")
- Tokens can be individually revoked
- Expandable table rows show QR code and full bunker URL per token
- Separate service token for ORLY's own relay connection
- Add Token button to create additional client tokens

Files modified:
- app/web/src/BunkerView.svelte: Token list UI with expandable details

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 15:02:09 +01:00
woikos
3f34eb288d Update nostr lib v1.0.12 with TLS URL scheme fix for NIP-98 2025-12-29 14:33:12 +01:00
woikos
8424f0ca44 Add debugging for NIP-98 auth in cashu mint 2025-12-29 14:17:50 +01:00
woikos
48c6739d25 Enable Cashu access tokens automatically when ACL is active (v0.44.2)
Some checks failed
Go / build-and-release (push) Has been cancelled
- Add automatic Cashu issuer/verifier initialization when ACL mode is not 'none'
- Use memory store for keyset management with proper TTL configuration
- Import cashuiface package for AllowAllChecker implementation
- ACL handles authorization; CAT provides token-based authentication

Files modified:
- app/main.go: Add Cashu system initialization when ACL active
- pkg/version/version: Bump to v0.44.2

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 14:01:54 +01:00
woikos
b837dcb5f0 Fix UTF-8 encoding error in compact event tag marshaling (v0.44.1)
Some checks failed
Go / build-and-release (push) Has been cancelled
- Fix binary pubkey/event ID values not being detected by tag.Marshal
- Compact event decoder now returns 33-byte values with null terminator
- This allows tag.Marshal to detect and hex-encode binary values correctly
- Fixes "Could not decode a text frame as UTF-8" WebSocket errors

Files modified:
- pkg/database/compact_event.go: Return 33-byte binary with null terminator

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 13:39:49 +01:00
11 changed files with 523 additions and 95 deletions

View File

@@ -6,7 +6,6 @@ import (
"net/http" "net/http"
"time" "time"
"lol.mleku.dev/chk"
"lol.mleku.dev/log" "lol.mleku.dev/log"
"git.mleku.dev/mleku/nostr/httpauth" "git.mleku.dev/mleku/nostr/httpauth"
@@ -35,13 +34,24 @@ type CashuMintResponse struct {
func (s *Server) handleCashuMint(w http.ResponseWriter, r *http.Request) { func (s *Server) handleCashuMint(w http.ResponseWriter, r *http.Request) {
// Check if Cashu is enabled // Check if Cashu is enabled
if s.CashuIssuer == nil { if s.CashuIssuer == nil {
log.W.F("Cashu mint request but issuer not initialized")
http.Error(w, "Cashu tokens not enabled", http.StatusNotImplemented) http.Error(w, "Cashu tokens not enabled", http.StatusNotImplemented)
return return
} }
// Require NIP-98 authentication // Require NIP-98 authentication
valid, pubkey, err := httpauth.CheckAuth(r) valid, pubkey, err := httpauth.CheckAuth(r)
if chk.E(err) || !valid { if err != nil {
authHeader := r.Header.Get("Authorization")
if len(authHeader) > 100 {
authHeader = authHeader[:100] + "..."
}
log.W.F("Cashu mint NIP-98 auth error: %v (valid=%v, authHeader=%q)", err, valid, authHeader)
http.Error(w, "NIP-98 authentication required", http.StatusUnauthorized)
return
}
if !valid {
log.W.F("Cashu mint NIP-98 auth invalid signature")
http.Error(w, "NIP-98 authentication required", http.StatusUnauthorized) http.Error(w, "NIP-98 authentication required", http.StatusUnauthorized)
return return
} }

View File

@@ -23,6 +23,10 @@ import (
"next.orly.dev/pkg/protocol/nip43" "next.orly.dev/pkg/protocol/nip43"
"next.orly.dev/pkg/protocol/publish" "next.orly.dev/pkg/protocol/publish"
"next.orly.dev/pkg/bunker" "next.orly.dev/pkg/bunker"
"next.orly.dev/pkg/cashu/issuer"
"next.orly.dev/pkg/cashu/keyset"
"next.orly.dev/pkg/cashu/verifier"
cashuiface "next.orly.dev/pkg/interfaces/cashu"
"next.orly.dev/pkg/ratelimit" "next.orly.dev/pkg/ratelimit"
"next.orly.dev/pkg/spider" "next.orly.dev/pkg/spider"
dsync "next.orly.dev/pkg/sync" dsync "next.orly.dev/pkg/sync"
@@ -162,6 +166,27 @@ func Run(
} }
} }
// Initialize Cashu access token system when ACL is active
if cfg.ACLMode != "none" {
// Create keyset manager with memory store (keys are regenerated each restart)
keysetStore := keyset.NewMemoryStore()
keysetManager := keyset.NewManager(keysetStore, keyset.DefaultActiveWindow, keyset.DefaultVerifyWindow)
// Initialize keyset manager (creates initial keyset)
if err := keysetManager.Init(); err != nil {
log.E.F("failed to initialize Cashu keyset manager: %v", err)
} else {
// Create issuer with permissive checker (ACL handles authorization)
issuerCfg := issuer.DefaultConfig()
l.CashuIssuer = issuer.New(keysetManager, cashuiface.AllowAllChecker{}, issuerCfg)
// Create verifier for validating tokens
l.CashuVerifier = verifier.New(keysetManager, cashuiface.AllowAllChecker{}, verifier.DefaultConfig())
log.I.F("Cashu access token system enabled (ACL mode: %s)", cfg.ACLMode)
}
}
// Initialize spider manager based on mode (only for Badger backend) // Initialize spider manager based on mode (only for Badger backend)
if badgerDB, ok := db.(*database.D); ok && cfg.SpiderMode != "none" { if badgerDB, ok := db.(*database.D); ok && cfg.SpiderMode != "none" {
if l.spiderManager, err = spider.New(ctx, badgerDB, l.publishers, cfg.SpiderMode); chk.E(err) { if l.spiderManager, err = spider.New(ctx, badgerDB, l.publishers, cfg.SpiderMode); chk.E(err) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -28,8 +28,110 @@
let isServiceActive = false; let isServiceActive = false;
let isStartingService = false; let isStartingService = false;
let connectedClients = []; let connectedClients = [];
let catToken = null; let serviceCatToken = null; // Token for ORLY's own relay connection
let catTokenEncoded = "";
// Client tokens list - each device gets its own token
let clientTokens = []; // [{id, name, token, encoded, createdAt, isEditing}]
let selectedTokenId = null; // Currently selected token for the QR code
// Two-word name generator
const adjectives = ["brave", "calm", "clever", "cosmic", "cozy", "daring", "eager", "fancy", "gentle", "happy", "jolly", "keen", "lively", "merry", "nimble", "peppy", "quick", "rustic", "shiny", "swift", "tender", "vivid", "witty", "zesty"];
const nouns = ["badger", "bunny", "coral", "dolphin", "falcon", "gecko", "heron", "iguana", "jaguar", "koala", "lemur", "mango", "narwhal", "otter", "panda", "quail", "rabbit", "salmon", "turtle", "urchin", "viper", "walrus", "yak", "zebra"];
function generateTokenName() {
const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
const noun = nouns[Math.floor(Math.random() * nouns.length)];
return `${adj}-${noun}`;
}
function generateTokenId() {
return crypto.randomUUID().split('-')[0];
}
// Add a new client token
async function addClientToken(mintInfo, signHttpAuth) {
const token = await requestToken(
mintInfo.mintUrl,
TokenScope.NIP46,
hexToBytes(userPubkey),
signHttpAuth,
[24133]
);
const encoded = encodeToken(token);
const id = generateTokenId();
const newToken = {
id,
name: generateTokenName(),
token,
encoded,
createdAt: Date.now(),
isExpanded: false
};
clientTokens = [...clientTokens, newToken];
// Select the new token if none selected
if (!selectedTokenId) {
selectedTokenId = id;
}
console.log(`Client token "${newToken.name}" created, expires:`, new Date(token.expiry * 1000).toISOString());
return newToken;
}
// Add a new token (called from UI)
async function handleAddToken() {
if (!bunkerInfo?.cashu_enabled) return;
try {
const mintInfo = await getMintInfo(bunkerInfo.relay_url);
if (!mintInfo) return;
const signHttpAuth = async (url, method) => {
const header = await createNIP98Auth(userSigner, userPubkey, method, url);
return `Nostr ${header}`;
};
await addClientToken(mintInfo, signHttpAuth);
// Regenerate QR for newly selected token
await generateQRCodes();
} catch (err) {
console.error("Failed to add token:", err);
error = err.message || "Failed to add token";
}
}
// Revoke/remove a client token
function revokeToken(tokenId) {
clientTokens = clientTokens.filter(t => t.id !== tokenId);
// If we removed the selected token, select another
if (selectedTokenId === tokenId) {
selectedTokenId = clientTokens.length > 0 ? clientTokens[0].id : null;
}
generateQRCodes();
}
// Toggle token details expansion
function toggleTokenExpand(tokenId) {
clientTokens = clientTokens.map(t =>
t.id === tokenId ? { ...t, isExpanded: !t.isExpanded } : t
);
}
// Update token name
function updateTokenName(tokenId, newName) {
clientTokens = clientTokens.map(t =>
t.id === tokenId ? { ...t, name: newName } : t
);
}
// Generate QR code for a specific token
async function generateTokenQR(token) {
if (!bunkerInfo || !userPubkey) return null;
const url = `bunker://${userPubkey}?relay=${encodeURIComponent(bunkerInfo.relay_url)}${bunkerSecret ? `&secret=${bunkerSecret}` : ''}&cat=${token.encoded}`;
return await QRCode.toDataURL(url, {
width: 200,
margin: 2,
color: { dark: "#000000", light: "#ffffff" }
});
}
$: canAccess = isLoggedIn && userPubkey && ( $: canAccess = isLoggedIn && userPubkey && (
currentEffectiveRole === "write" || currentEffectiveRole === "write" ||
@@ -38,8 +140,10 @@
); );
// Generate bunker URLs when bunkerInfo and userPubkey are available // Generate bunker URLs when bunkerInfo and userPubkey are available
$: clientBunkerURL = bunkerInfo && userPubkey ? // Get selected token for the bunker URL
`bunker://${userPubkey}?relay=${encodeURIComponent(bunkerInfo.relay_url)}${bunkerSecret ? `&secret=${bunkerSecret}` : ''}${catTokenEncoded ? `&cat=${catTokenEncoded}` : ''}` : ""; $: selectedToken = clientTokens.find(t => t.id === selectedTokenId);
$: clientBunkerURL = bunkerInfo && userPubkey && selectedToken ?
`bunker://${userPubkey}?relay=${encodeURIComponent(bunkerInfo.relay_url)}${bunkerSecret ? `&secret=${bunkerSecret}` : ''}&cat=${selectedToken.encoded}` : "";
$: signerBunkerURL = bunkerInfo ? $: signerBunkerURL = bunkerInfo ?
`nostr+connect://${bunkerInfo.relay_url}` : ""; `nostr+connect://${bunkerInfo.relay_url}` : "";
@@ -68,9 +172,9 @@
error = ""; error = "";
try { try {
// Check if CAT is required and mint one // Check if CAT is required and mint tokens
if (bunkerInfo.cashu_enabled) { if (bunkerInfo.cashu_enabled) {
console.log("CAT required, minting token..."); console.log("CAT required, minting tokens...");
const mintInfo = await getMintInfo(bunkerInfo.relay_url); const mintInfo = await getMintInfo(bunkerInfo.relay_url);
if (mintInfo) { if (mintInfo) {
// Create NIP-98 auth function // Create NIP-98 auth function
@@ -79,16 +183,18 @@
return `Nostr ${header}`; return `Nostr ${header}`;
}; };
// Request NIP-46 scoped token // 1. Token for ORLY's BunkerService relay connection
catToken = await requestToken( serviceCatToken = await requestToken(
mintInfo.mintUrl, mintInfo.mintUrl,
TokenScope.NIP46, TokenScope.NIP46,
hexToBytes(userPubkey), hexToBytes(userPubkey),
signHttpAuth, signHttpAuth,
[24133] [24133]
); );
catTokenEncoded = encodeToken(catToken); console.log("Service CAT token acquired, expires:", new Date(serviceCatToken.expiry * 1000).toISOString());
console.log("CAT token acquired, expires:", new Date(catToken.expiry * 1000).toISOString());
// 2. Create first client token
await addClientToken(mintInfo, signHttpAuth);
} }
} }
@@ -104,9 +210,9 @@
bunkerService.addAllowedSecret(bunkerSecret); bunkerService.addAllowedSecret(bunkerSecret);
} }
// Set CAT token if available // Set CAT token for service connection
if (catToken) { if (serviceCatToken) {
bunkerService.setCatToken(catToken); bunkerService.setCatToken(serviceCatToken);
} }
// Set up callbacks // Set up callbacks
@@ -149,8 +255,9 @@
} }
isServiceActive = false; isServiceActive = false;
connectedClients = []; connectedClients = [];
catToken = null; serviceCatToken = null;
catTokenEncoded = ""; clientTokens = [];
selectedTokenId = null;
// Regenerate QR codes without CAT token // Regenerate QR codes without CAT token
generateQRCodes(); generateQRCodes();
} }
@@ -299,52 +406,99 @@
</div> </div>
{/if} {/if}
{#if catToken}
<div class="cat-info">
<span class="cat-badge">CAT Token Active</span>
<span class="cat-expiry">Expires: {new Date(catToken.expiry * 1000).toLocaleString()}</span>
</div>
{/if}
{/if} {/if}
</div> </div>
<div class="qr-sections"> <!-- Client Tokens Table -->
<!-- Client QR Code --> {#if isServiceActive && clientTokens.length > 0}
<section class="qr-section"> <div class="tokens-section">
<h4>Bunker URL for Client Apps</h4> <div class="tokens-header">
<p class="section-desc"> <h4>Client Tokens</h4>
{#if isServiceActive} <button class="add-token-btn" on:click={handleAddToken}>+ Add Token</button>
Scan or copy this URL in your Nostr client (e.g., Smesh) to connect: </div>
{:else} <p class="tokens-desc">Each device/app gets its own token. Tokens can be individually revoked.</p>
Start the bunker service above to generate a connection URL.
{/if}
</p>
<div <div class="tokens-table">
class="qr-container clickable" {#each clientTokens as tokenEntry (tokenEntry.id)}
on:click={() => copyToClipboard(clientBunkerURL, "client")} <div class="token-row" class:expanded={tokenEntry.isExpanded}>
on:keypress={(e) => e.key === 'Enter' && copyToClipboard(clientBunkerURL, "client")} <div class="token-main" on:click={() => toggleTokenExpand(tokenEntry.id)} on:keypress={(e) => e.key === 'Enter' && toggleTokenExpand(tokenEntry.id)} role="button" tabindex="0">
role="button" <span class="expand-icon">{tokenEntry.isExpanded ? '▼' : '▶'}</span>
tabindex="0" <input
title="Click to copy bunker URL" type="text"
> class="token-name-input"
{#if clientQrDataUrl} value={tokenEntry.name}
<img src={clientQrDataUrl} alt="Client Bunker QR Code" class="qr-code" /> on:input={(e) => updateTokenName(tokenEntry.id, e.target.value)}
<div class="qr-overlay" class:visible={copiedItem === "client"}> on:click|stopPropagation
Copied! placeholder="Token name"
/>
<span class="token-created">
{new Date(tokenEntry.createdAt).toLocaleDateString()}
</span>
<span class="token-expiry">
Expires: {new Date(tokenEntry.token.expiry * 1000).toLocaleDateString()}
</span>
<button
class="revoke-btn"
on:click|stopPropagation={() => revokeToken(tokenEntry.id)}
title="Revoke this token"
>
Revoke
</button>
</div>
{#if tokenEntry.isExpanded}
<div class="token-details">
{#await generateTokenQR(tokenEntry)}
<div class="qr-placeholder small">Loading QR...</div>
{:then qrDataUrl}
<div class="token-detail-content">
<div
class="qr-container small clickable"
on:click={() => {
const url = `bunker://${userPubkey}?relay=${encodeURIComponent(bunkerInfo.relay_url)}${bunkerSecret ? `&secret=${bunkerSecret}` : ''}&cat=${tokenEntry.encoded}`;
copyToClipboard(url, tokenEntry.id);
}}
on:keypress={(e) => {
if (e.key === 'Enter') {
const url = `bunker://${userPubkey}?relay=${encodeURIComponent(bunkerInfo.relay_url)}${bunkerSecret ? `&secret=${bunkerSecret}` : ''}&cat=${tokenEntry.encoded}`;
copyToClipboard(url, tokenEntry.id);
}
}}
role="button"
tabindex="0"
title="Click to copy bunker URL"
>
<img src={qrDataUrl} alt="Token QR Code" class="qr-code small" />
<div class="qr-overlay" class:visible={copiedItem === tokenEntry.id}>
Copied!
</div>
</div>
<div class="token-info">
<div class="info-item">
<span class="label">Created:</span>
<span>{new Date(tokenEntry.createdAt).toLocaleString()}</span>
</div>
<div class="info-item">
<span class="label">Expires:</span>
<span>{new Date(tokenEntry.token.expiry * 1000).toLocaleString()}</span>
</div>
<div class="info-item url-item">
<span class="label">Bunker URL:</span>
<code class="bunker-url small">{`bunker://${userPubkey}?relay=${encodeURIComponent(bunkerInfo.relay_url)}${bunkerSecret ? `&secret=${bunkerSecret}` : ''}&cat=${tokenEntry.encoded}`}</code>
</div>
<div class="copy-hint">Click QR code to copy URL</div>
</div>
</div>
{:catch}
<div class="error-message">Failed to generate QR</div>
{/await}
</div>
{/if}
</div> </div>
{:else} {/each}
<div class="qr-placeholder">Generating QR...</div>
{/if}
</div> </div>
</div>
<div class="url-display"> {/if}
<code class="bunker-url">{clientBunkerURL}</code>
</div>
<div class="copy-hint">Click QR code to copy</div>
</section>
</div>
<!-- Connection Info --> <!-- Connection Info -->
<div class="connection-info"> <div class="connection-info">
@@ -821,6 +975,187 @@
background-color: var(--accent-hover-color); background-color: var(--accent-hover-color);
} }
/* Token table styles */
.tokens-section {
background-color: var(--card-bg);
padding: 1.25em;
border-radius: 8px;
margin-bottom: 1.5em;
}
.tokens-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5em;
}
.tokens-header h4 {
margin: 0;
color: var(--text-color);
}
.tokens-desc {
margin: 0 0 1em 0;
font-size: 0.9em;
color: var(--text-color);
opacity: 0.7;
}
.add-token-btn {
background-color: var(--primary);
color: var(--text-color);
border: none;
padding: 0.4em 0.8em;
border-radius: 4px;
cursor: pointer;
font-size: 0.85em;
}
.add-token-btn:hover {
background-color: var(--accent-hover-color);
}
.tokens-table {
display: flex;
flex-direction: column;
gap: 0.5em;
}
.token-row {
background-color: var(--bg-color);
border-radius: 6px;
overflow: hidden;
}
.token-row.expanded {
border: 1px solid var(--border-color);
}
.token-main {
display: flex;
align-items: center;
gap: 0.75em;
padding: 0.75em;
cursor: pointer;
transition: background-color 0.15s;
}
.token-main:hover {
background-color: var(--card-bg);
}
.expand-icon {
font-size: 0.7em;
color: var(--text-color);
opacity: 0.6;
width: 1em;
}
.token-name-input {
flex: 1;
min-width: 100px;
max-width: 180px;
background-color: transparent;
border: 1px solid transparent;
border-radius: 4px;
padding: 0.3em 0.5em;
font-size: 0.95em;
font-weight: 500;
color: var(--text-color);
}
.token-name-input:hover {
border-color: var(--border-color);
}
.token-name-input:focus {
outline: none;
border-color: var(--primary);
background-color: var(--card-bg);
}
.token-created, .token-expiry {
font-size: 0.8em;
color: var(--text-color);
opacity: 0.7;
}
.token-expiry {
margin-left: auto;
}
.revoke-btn {
background-color: #ef4444;
color: white;
border: none;
padding: 0.3em 0.6em;
border-radius: 4px;
font-size: 0.8em;
cursor: pointer;
}
.revoke-btn:hover {
background-color: #dc2626;
}
.token-details {
padding: 1em;
border-top: 1px solid var(--border-color);
background-color: var(--card-bg);
}
.token-detail-content {
display: flex;
gap: 1.5em;
align-items: flex-start;
}
.qr-container.small {
flex-shrink: 0;
}
.qr-code.small {
width: 150px;
height: 150px;
}
.qr-placeholder.small {
width: 150px;
height: 150px;
font-size: 0.85em;
}
.token-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5em;
}
.info-item {
display: flex;
gap: 0.5em;
font-size: 0.9em;
}
.info-item .label {
color: var(--text-color);
opacity: 0.7;
min-width: 70px;
}
.info-item.url-item {
flex-direction: column;
gap: 0.25em;
}
.bunker-url.small {
font-size: 0.7em;
padding: 0.5em;
word-break: break-all;
}
@media (max-width: 600px) { @media (max-width: 600px) {
.qr-sections { .qr-sections {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -834,5 +1169,42 @@
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
} }
.token-main {
flex-wrap: wrap;
gap: 0.5em;
}
.token-name-input {
order: 1;
flex: 1 1 100%;
max-width: none;
}
.expand-icon {
order: 0;
}
.token-created {
order: 2;
}
.token-expiry {
order: 3;
margin-left: 0;
}
.revoke-btn {
order: 4;
}
.token-detail-content {
flex-direction: column;
align-items: center;
}
.token-info {
width: 100%;
}
} }
</style> </style>

View File

@@ -12,7 +12,7 @@
*/ */
export async function createNIP98Auth(signer, pubkey, method, url) { export async function createNIP98Auth(signer, pubkey, method, url) {
if (!signer || !pubkey) { if (!signer || !pubkey) {
console.log("No signer or pubkey available"); console.log("createNIP98Auth: No signer or pubkey available", { hasSigner: !!signer, hasPubkey: !!pubkey });
return null; return null;
} }
@@ -23,17 +23,30 @@ export async function createNIP98Auth(signer, pubkey, method, url) {
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
tags: [ tags: [
["u", url], ["u", url],
["method", method], ["method", method.toUpperCase()],
], ],
content: "", content: "",
}; };
console.log("createNIP98Auth: Signing event for", method, url);
// Sign using the signer // Sign using the signer
const signedEvent = await signer.signEvent(authEvent); const signedEvent = await signer.signEvent(authEvent);
console.log("createNIP98Auth: Signed event:", {
id: signedEvent.id,
pubkey: signedEvent.pubkey,
kind: signedEvent.kind,
created_at: signedEvent.created_at,
tags: signedEvent.tags,
hasSig: !!signedEvent.sig
});
// Use URL-safe base64 encoding (replace + with -, / with _) // Use URL-safe base64 encoding (replace + with -, / with _)
return btoa(JSON.stringify(signedEvent)).replace(/\+/g, '-').replace(/\//g, '_'); const json = JSON.stringify(signedEvent);
const base64 = btoa(json).replace(/\+/g, '-').replace(/\//g, '_');
return base64;
} catch (error) { } catch (error) {
console.error("Error creating NIP-98 auth:", error); console.error("createNIP98Auth: Error:", error);
return null; return null;
} }
} }

2
go.mod
View File

@@ -3,7 +3,7 @@ module next.orly.dev
go 1.25.3 go 1.25.3
require ( require (
git.mleku.dev/mleku/nostr v1.0.11 git.mleku.dev/mleku/nostr v1.0.12
github.com/adrg/xdg v0.5.3 github.com/adrg/xdg v0.5.3
github.com/alexflint/go-arg v1.6.1 github.com/alexflint/go-arg v1.6.1
github.com/aperturerobotics/go-indexeddb v0.2.3 github.com/aperturerobotics/go-indexeddb v0.2.3

4
go.sum
View File

@@ -1,5 +1,5 @@
git.mleku.dev/mleku/nostr v1.0.11 h1:xQ+rKPzTblerX/kRLDimOsH3rQK7/n9wYdG4DBKGcsg= git.mleku.dev/mleku/nostr v1.0.12 h1:bjsFUh1Q3fGpU7qsqxggGgrGGUt2OBdu1w8hjDM4gJE=
git.mleku.dev/mleku/nostr v1.0.11/go.mod h1:kJwSMmLRnAJ7QJtgXDv2wGgceFU0luwVqrgAL3MI93M= git.mleku.dev/mleku/nostr v1.0.12/go.mod h1:kJwSMmLRnAJ7QJtgXDv2wGgceFU0luwVqrgAL3MI93M=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg= github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=

View File

@@ -355,7 +355,7 @@ func decodeTagElement(r io.Reader, resolver SerialResolver) (elem []byte, err er
return elem, nil return elem, nil
case TagElementPubkeySerial: case TagElementPubkeySerial:
// Pubkey serial: 5 bytes -> lookup full pubkey -> return as 32-byte binary // Pubkey serial: 5 bytes -> lookup full pubkey -> return as 33-byte binary
serial, err := readUint40(r) serial, err := readUint40(r)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -364,11 +364,14 @@ func decodeTagElement(r io.Reader, resolver SerialResolver) (elem []byte, err er
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Return as 32-byte binary (nostr library optimized format) // Return as 33-byte binary (32 bytes + null terminator) for tag.Marshal detection
return pubkey, nil result := make([]byte, 33)
copy(result, pubkey)
result[32] = 0 // null terminator
return result, nil
case TagElementEventSerial: case TagElementEventSerial:
// Event serial: 5 bytes -> lookup full event ID -> return as 32-byte binary // Event serial: 5 bytes -> lookup full event ID -> return as 33-byte binary
serial, err := readUint40(r) serial, err := readUint40(r)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -377,15 +380,20 @@ func decodeTagElement(r io.Reader, resolver SerialResolver) (elem []byte, err er
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Return as 32-byte binary // Return as 33-byte binary (32 bytes + null terminator) for tag.Marshal detection
return eventId, nil result := make([]byte, 33)
copy(result, eventId)
result[32] = 0 // null terminator
return result, nil
case TagElementEventIdFull: case TagElementEventIdFull:
// Full event ID: 32 bytes (for unknown/forward references) // Full event ID: 32 bytes (for unknown/forward references)
elem = make([]byte, 32) // Return as 33-byte binary (32 bytes + null terminator) for tag.Marshal detection
if _, err = io.ReadFull(r, elem); err != nil { elem = make([]byte, 33)
if _, err = io.ReadFull(r, elem[:32]); err != nil {
return nil, err return nil, err
} }
elem[32] = 0 // null terminator
return elem, nil return elem, nil
default: default:

View File

@@ -1 +1 @@
v0.44.0 v0.44.3