Some checks failed
Go / build-and-release (push) Has been cancelled
- Implement NIP-NRC protocol for remote relay access through public relay tunnel - Add NRC bridge service with NIP-44 encrypted message tunneling - Add NRC client library for applications - Add session management with subscription tracking and expiry - Add URI parsing for nostr+relayconnect:// scheme with secret and CAT auth - Add NRC API endpoints for connection management (create/list/delete/get-uri) - Add RelayConnectView.svelte component for managing NRC connections in web UI - Add NRC database storage for connection secrets and labels - Add NRC CLI commands (generate, list, revoke) - Add support for Cashu Access Tokens (CAT) in NRC URIs - Add ScopeNRC constant for Cashu token scope - Add wasm build infrastructure and stub files Files modified: - app/config/config.go: NRC configuration options - app/handle-nrc.go: New API handlers for NRC connections - app/main.go: NRC bridge startup integration - app/server.go: Register NRC API routes - app/web/src/App.svelte: Add Relay Connect tab - app/web/src/RelayConnectView.svelte: New NRC management component - app/web/src/api.js: NRC API client functions - main.go: NRC CLI command handlers - pkg/bunker/acl_adapter.go: Add NRC scope mapping - pkg/cashu/token/token.go: Add ScopeNRC constant - pkg/database/nrc.go: NRC connection storage - pkg/protocol/nrc/: New NRC protocol implementation - docs/NIP-NRC.md: NIP specification document 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
203 lines
4.3 KiB
Go
203 lines
4.3 KiB
Go
//go:build js && wasm
|
|
|
|
package database
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
// TokenHashes extracts unique word hashes (8-byte truncated sha256) from content.
|
|
// Rules:
|
|
// - Unicode-aware: words are sequences of letters or numbers.
|
|
// - Lowercased using unicode case mapping.
|
|
// - Ignore URLs (starting with http://, https://, www., or containing "://").
|
|
// - Ignore nostr: URIs and #[n] mentions.
|
|
// - Ignore words shorter than 2 runes.
|
|
// - Exclude 64-character hexadecimal strings (likely IDs/pubkeys).
|
|
func TokenHashes(content []byte) [][]byte {
|
|
s := string(content)
|
|
var out [][]byte
|
|
seen := make(map[string]struct{})
|
|
|
|
i := 0
|
|
for i < len(s) {
|
|
r, size := rune(s[i]), 1
|
|
if r >= 0x80 {
|
|
r, size = utf8DecodeRuneInString(s[i:])
|
|
}
|
|
|
|
// Skip whitespace
|
|
if unicode.IsSpace(r) {
|
|
i += size
|
|
continue
|
|
}
|
|
|
|
// Skip URLs and schemes
|
|
if hasPrefixFold(s[i:], "http://") || hasPrefixFold(s[i:], "https://") || hasPrefixFold(s[i:], "nostr:") || hasPrefixFold(s[i:], "www.") {
|
|
i = skipUntilSpace(s, i)
|
|
continue
|
|
}
|
|
// If token contains "://" ahead, treat as URL and skip to space
|
|
if j := strings.Index(s[i:], "://"); j == 0 || (j > 0 && isWordStart(r)) {
|
|
// Only if it's at start of token
|
|
before := s[i : i+j]
|
|
if len(before) == 0 || allAlphaNum(before) {
|
|
i = skipUntilSpace(s, i)
|
|
continue
|
|
}
|
|
}
|
|
// Skip #[n] mentions
|
|
if r == '#' && i+size < len(s) && s[i+size] == '[' {
|
|
end := strings.IndexByte(s[i:], ']')
|
|
if end >= 0 {
|
|
i += end + 1
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Collect a word
|
|
start := i
|
|
var runes []rune
|
|
for i < len(s) {
|
|
r2, size2 := rune(s[i]), 1
|
|
if r2 >= 0x80 {
|
|
r2, size2 = utf8DecodeRuneInString(s[i:])
|
|
}
|
|
if unicode.IsLetter(r2) || unicode.IsNumber(r2) {
|
|
// Normalize decorative unicode (small caps, fraktur) to ASCII
|
|
// before lowercasing for consistent indexing
|
|
runes = append(runes, unicode.ToLower(normalizeRune(r2)))
|
|
i += size2
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
// If we didn't consume any rune for a word, advance by one rune to avoid stalling
|
|
if i == start {
|
|
_, size2 := utf8DecodeRuneInString(s[i:])
|
|
i += size2
|
|
continue
|
|
}
|
|
if len(runes) >= 2 {
|
|
w := string(runes)
|
|
// Exclude 64-char hex strings
|
|
if isHex64(w) {
|
|
continue
|
|
}
|
|
if _, ok := seen[w]; !ok {
|
|
seen[w] = struct{}{}
|
|
h := sha256.Sum256([]byte(w))
|
|
out = append(out, h[:8])
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func hasPrefixFold(s, prefix string) bool {
|
|
if len(s) < len(prefix) {
|
|
return false
|
|
}
|
|
for i := 0; i < len(prefix); i++ {
|
|
c := s[i]
|
|
p := prefix[i]
|
|
if c == p {
|
|
continue
|
|
}
|
|
// ASCII case-insensitive
|
|
if 'A' <= c && c <= 'Z' {
|
|
c = c - 'A' + 'a'
|
|
}
|
|
if 'A' <= p && p <= 'Z' {
|
|
p = p - 'A' + 'a'
|
|
}
|
|
if c != p {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func skipUntilSpace(s string, i int) int {
|
|
for i < len(s) {
|
|
r, size := rune(s[i]), 1
|
|
if r >= 0x80 {
|
|
r, size = utf8DecodeRuneInString(s[i:])
|
|
}
|
|
if unicode.IsSpace(r) {
|
|
return i
|
|
}
|
|
i += size
|
|
}
|
|
return i
|
|
}
|
|
|
|
func allAlphaNum(s string) bool {
|
|
for _, r := range s {
|
|
if !(unicode.IsLetter(r) || unicode.IsNumber(r)) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isWordStart(r rune) bool { return unicode.IsLetter(r) || unicode.IsNumber(r) }
|
|
|
|
// utf8DecodeRuneInString decodes the first UTF-8 rune from s.
|
|
// Returns the rune and the number of bytes consumed.
|
|
func utf8DecodeRuneInString(s string) (r rune, size int) {
|
|
if len(s) == 0 {
|
|
return 0, 0
|
|
}
|
|
// ASCII fast path
|
|
b := s[0]
|
|
if b < 0x80 {
|
|
return rune(b), 1
|
|
}
|
|
// Multi-byte: determine expected length from first byte
|
|
var expectedLen int
|
|
switch {
|
|
case b&0xE0 == 0xC0: // 110xxxxx - 2 bytes
|
|
expectedLen = 2
|
|
case b&0xF0 == 0xE0: // 1110xxxx - 3 bytes
|
|
expectedLen = 3
|
|
case b&0xF8 == 0xF0: // 11110xxx - 4 bytes
|
|
expectedLen = 4
|
|
default:
|
|
// Invalid UTF-8 start byte
|
|
return 0xFFFD, 1
|
|
}
|
|
if len(s) < expectedLen {
|
|
return 0xFFFD, 1
|
|
}
|
|
// Decode using Go's built-in rune conversion (simple and correct)
|
|
runes := []rune(s[:expectedLen])
|
|
if len(runes) == 0 {
|
|
return 0xFFFD, 1
|
|
}
|
|
return runes[0], expectedLen
|
|
}
|
|
|
|
// isHex64 returns true if s is exactly 64 hex characters (0-9, a-f)
|
|
func isHex64(s string) bool {
|
|
if len(s) != 64 {
|
|
return false
|
|
}
|
|
for i := 0; i < 64; i++ {
|
|
c := s[i]
|
|
if c >= '0' && c <= '9' {
|
|
continue
|
|
}
|
|
if c >= 'a' && c <= 'f' {
|
|
continue
|
|
}
|
|
if c >= 'A' && c <= 'F' {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|