Browse Source

repo page filter

fix repo name entry
restructure docs
main
Silberengel 4 weeks ago
parent
commit
3e3ecc0938
  1. 18
      docs/01.md
  2. 95
      docs/02.md
  3. 26
      docs/07.md
  4. 23
      docs/09.md
  5. 23
      docs/10.md
  6. 26
      docs/19.md
  7. 37
      docs/22.md
  8. 42
      docs/34.md
  9. 30
      docs/65.md
  10. 44
      docs/84.md
  11. 44
      docs/98.md
  12. 237
      docs/CustomKinds.md
  13. 1147
      docs/NIP_COMPLIANCE.md
  14. 52
      src/app.css
  15. 2
      src/lib/types/nostr.ts
  16. 270
      src/routes/+page.svelte
  17. 6
      src/routes/signup/+page.svelte

18
docs/01.md

@ -176,3 +176,21 @@ This NIP defines no rules for how `NOTICE` messages should be sent or treated.
* `["CLOSED", "sub1", "error: could not connect to the database"]` * `["CLOSED", "sub1", "error: could not connect to the database"]`
* `["CLOSED", "sub1", "error: shutting down idle subscription"]` * `["CLOSED", "sub1", "error: shutting down idle subscription"]`
- The standardized machine-readable prefixes for `OK` and `CLOSED` are: `duplicate`, `pow`, `blocked`, `rate-limited`, `invalid`, `restricted`, `mute` and `error` for when none of that fits. - The standardized machine-readable prefixes for `OK` and `CLOSED` are: `duplicate`, `pow`, `blocked`, `rate-limited`, `invalid`, `restricted`, `mute` and `error` for when none of that fits.
## GitRepublic Usage
GitRepublic uses NIP-01 as the foundation for all Nostr event handling. All events follow the standard event structure defined in NIP-01.
### Event Structure
All repository-related events (announcements, PRs, issues, patches, etc.) follow the NIP-01 event format:
- Events are serialized according to NIP-01 rules (UTF-8, no whitespace, proper escaping)
- Event IDs are computed as SHA256 of the serialized event
- Signatures use Schnorr signatures on secp256k1
- The `nostr-tools` library is used for event serialization, ID computation, and signature verification
### Kind 1 (Text Note) Usage
GitRepublic uses kind 1 events as a fallback mechanism for relay write proofs when NIP-98 authentication events are not available. This ensures git operations can still be authenticated even if the NIP-98 flow fails.
**Implementation**: `src/lib/services/nostr/nostr-client.ts`, `src/lib/types/nostr.ts`

95
docs/02.md

@ -76,3 +76,98 @@ and another from `a8bb3d884d5d90b413d9891fe4c4e46d` that says
When the user sees `21df6d143fb96c2ec9d63726bf9edc71` the client can show _erin_ instead; When the user sees `21df6d143fb96c2ec9d63726bf9edc71` the client can show _erin_ instead;
When the user sees `a8bb3d884d5d90b413d9891fe4c4e46d` the client can show _david.erin_ instead; When the user sees `a8bb3d884d5d90b413d9891fe4c4e46d` the client can show _david.erin_ instead;
When the user sees `f57f54057d2a7af0efecc8b0b66f5708` the client can show _frank.david.erin_ instead. When the user sees `f57f54057d2a7af0efecc8b0b66f5708` the client can show _frank.david.erin_ instead.
## GitRepublic Usage
GitRepublic uses KIND 3 (Contact List) events to enable repository filtering based on social connections.
### Repository Filtering
When a user enables the "Show only my repos and those of my contacts" filter on the landing page, GitRepublic:
1. **Fetches the user's contact list**: Retrieves the latest KIND 3 event published by the logged-in user
2. **Extracts contact pubkeys**: Parses all `p` tags from the contact list event to build a set of followed pubkeys
3. **Includes the user's own pubkey**: Automatically adds the logged-in user's pubkey to the filter set
4. **Filters repositories**: Shows only repositories where:
- The repository owner (event `pubkey`) is in the contact set, OR
- Any maintainer (from `maintainers` tags) is in the contact set
### Implementation Details
- **Pubkey normalization**: GitRepublic handles both hex-encoded pubkeys and bech32-encoded npubs in contact lists
- **Maintainer matching**: The filter checks all maintainers listed in repository announcement `maintainers` tags
- **Real-time updates**: The contact list is fetched when the user logs in and can be refreshed by reloading the page
### Example Use Case
A user follows several developers on Nostr. By enabling the contact filter:
- They see repositories owned by people they follow
- They see repositories where their contacts are maintainers
- They see their own repositories
- They don't see repositories from people they don't follow
This creates a personalized, social discovery experience for finding relevant code repositories based on trust relationships established through Nostr's follow mechanism.
### Technical Implementation
```typescript
// Fetch user's kind 3 contact list
const contactEvents = await nostrClient.fetchEvents([
{
kinds: [KIND.CONTACT_LIST], // KIND 3
authors: [userPubkey],
limit: 1
}
]);
// Extract pubkeys from 'p' tags
const contactPubkeys = new Set<string>();
contactPubkeys.add(userPubkey); // Include user's own repos
if (contactEvents.length > 0) {
const contactEvent = contactEvents[0];
for (const tag of contactEvent.tags) {
if (tag[0] === 'p' && tag[1]) {
let pubkey = tag[1];
// Decode npub if needed
try {
const decoded = nip19.decode(pubkey);
if (decoded.type === 'npub') {
pubkey = decoded.data as string;
}
} catch {
// Assume it's already a hex pubkey
}
contactPubkeys.add(pubkey);
}
}
}
// Filter repositories
const filteredRepos = allRepos.filter(event => {
// Check if owner is in contacts
if (contactPubkeys.has(event.pubkey)) return true;
// Check if any maintainer is in contacts
const maintainerTags = event.tags.filter(t => t[0] === 'maintainers');
for (const tag of maintainerTags) {
for (let i = 1; i < tag.length; i++) {
let maintainerPubkey = tag[i];
// Decode npub if needed
try {
const decoded = nip19.decode(maintainerPubkey);
if (decoded.type === 'npub') {
maintainerPubkey = decoded.data as string;
}
} catch {
// Assume it's already a hex pubkey
}
if (contactPubkeys.has(maintainerPubkey)) return true;
}
}
return false;
});
```
This implementation provides a seamless way for users to discover repositories from their social network while maintaining the decentralized, trustless nature of Nostr.

26
docs/07.md

@ -30,3 +30,29 @@ To make sure that the `window.nostr` is available to nostr clients on page load,
### Implementation ### Implementation
See https://github.com/aljazceru/awesome-nostr#nip-07-browser-extensions. See https://github.com/aljazceru/awesome-nostr#nip-07-browser-extensions.
## GitRepublic Usage
NIP-07 is the primary authentication method for GitRepublic. All user interactions that require signing events use the NIP-07 browser extension interface.
### Authentication Flow
1. **Availability Check**: GitRepublic checks for `window.nostr` availability on page load
2. **Public Key Retrieval**: When users need to authenticate, `getPublicKey()` is called to get their pubkey
3. **Event Signing**: All repository announcements, PRs, issues, and other events are signed using `signEvent()`
### Key Features
- **Repository Creation**: Users sign repository announcement events (kind 30617) using NIP-07
- **Repository Updates**: Settings changes, maintainer additions, and other updates are signed via NIP-07
- **Pull Requests**: PR creation and updates are signed by the PR author
- **Issues**: Issue creation and comments are signed by the author
- **Commit Signatures**: Git commits can be signed using NIP-07 (client-side only, keys never leave browser)
### Security
- Keys never leave the browser - all signing happens client-side
- No server-side key storage required
- Users maintain full control of their private keys
**Implementation**: `src/lib/services/nostr/nip07-signer.ts`

23
docs/09.md

@ -51,3 +51,26 @@ Relays MAY validate that a deletion request event only references events that ha
## Deletion Request of a Deletion Request ## Deletion Request of a Deletion Request
Publishing a deletion request event against a deletion request has no effect. Clients and relays are not obliged to support "unrequest deletion" functionality. Publishing a deletion request event against a deletion request has no effect. Clients and relays are not obliged to support "unrequest deletion" functionality.
## GitRepublic Usage
GitRepublic uses NIP-09 deletion requests for repository fork failures and other error scenarios where an event needs to be removed.
### Fork Failure Deletion
When a repository fork operation fails (e.g., ownership transfer event cannot be published), GitRepublic creates a deletion request to remove the failed fork announcement:
```typescript
{
kind: 5,
tags: [
['a', `30617:${userPubkeyHex}:${forkRepoName}`],
['k', KIND.REPO_ANNOUNCEMENT.toString()]
],
content: 'Fork failed: ownership transfer event could not be published...'
}
```
This ensures that failed fork attempts don't leave orphaned repository announcements in the system.
**Implementation**: `src/routes/api/repos/[npub]/[repo]/fork/+server.ts`

23
docs/10.md

@ -82,3 +82,26 @@ Where:
* Many "e" tags: `["e", <root-id>]` `["e", <mention-id>]`, ..., `["e", <reply-id>]`<br> * Many "e" tags: `["e", <root-id>]` `["e", <mention-id>]`, ..., `["e", <reply-id>]`<br>
There may be any number of `<mention-ids>`. These are the ids of events which may, or may not be in the reply chain. There may be any number of `<mention-ids>`. These are the ids of events which may, or may not be in the reply chain.
They are citing from this event. `root-id` and `reply-id` are as above. They are citing from this event. `root-id` and `reply-id` are as above.
## GitRepublic Usage
GitRepublic uses NIP-10 event references primarily in patch events (kind 1617) to create patch series. Patches in a patch set use `e` tags with `reply` markers to link to previous patches in the series.
### Patch Series Threading
When multiple patches are sent as part of a patch series, each patch after the first includes an `e` tag with a `reply` marker pointing to the previous patch:
```jsonc
{
"kind": 1617,
"tags": [
["a", "30617:owner:repo"],
["e", "previous_patch_event_id", "wss://relay.example.com", "reply"],
// ... other tags
]
}
```
This creates a threaded chain of patches that can be applied in sequence.
**Implementation**: Used in patch creation and processing throughout the codebase

26
docs/19.md

@ -67,3 +67,29 @@ These possible standardized `TLV` types are indicated here:
- `npub` keys MUST NOT be used in NIP-01 events or in NIP-05 JSON responses, only the hex format is supported there. - `npub` keys MUST NOT be used in NIP-01 events or in NIP-05 JSON responses, only the hex format is supported there.
- When decoding a bech32-formatted string, TLVs that are not recognized or supported should be ignored, rather than causing an error. - When decoding a bech32-formatted string, TLVs that are not recognized or supported should be ignored, rather than causing an error.
## GitRepublic Usage
GitRepublic extensively uses NIP-19 bech32 encoding for user-friendly display and input of Nostr entities throughout the application.
### User Interface
- **Repository URLs**: Repositories are accessed via URLs like `/repos/{npub}/{repo-name}` where `npub` is the bech32-encoded public key
- **User Profiles**: User profile pages use npub in URLs: `/users/{npub}`
- **Search**: Users can search for repositories using npub, naddr, or nevent formats
- **Repository References**: The "Load Existing Repository" feature accepts hex event IDs, nevent, or naddr formats
### Internal Handling
- **Conversion**: All npub values are converted to hex format internally for event creation and filtering
- **Maintainers**: Maintainer lists accept both npub and hex formats, converting as needed
- **Contact Lists**: When processing kind 3 contact lists, GitRepublic handles both npub and hex pubkey formats
### Search Support
The repository search feature can decode and search by:
- **npub**: User public keys (converted to hex for filtering)
- **naddr**: Repository announcement addresses (decoded to extract pubkey, kind, and d-tag)
- **nevent**: Event references (decoded to extract event ID)
**Implementation**: `src/lib/services/nostr/nip19-utils.ts`, used throughout the codebase for encoding/decoding

37
docs/22.md

@ -198,3 +198,40 @@ A reply to a podcast comment:
// other fields // other fields
} }
``` ```
## GitRepublic Usage
GitRepublic uses NIP-22 comments for threaded discussions on pull requests, issues, and patches. Comments enable collaborative code review and issue discussion.
### Comment Threading
Comments on PRs, issues, and patches use NIP-22's uppercase/lowercase tag convention:
- **Uppercase tags** (`E`, `K`, `P`, `A`) reference the root event (PR, issue, or patch)
- **Lowercase tags** (`e`, `k`, `p`, `a`) reference the parent comment (for reply threads)
### Implementation Details
- **Root Comments**: Comments directly on a PR/issue use uppercase tags pointing to the root event
- **Reply Comments**: Replies to other comments use lowercase tags for the parent comment
- **Relay Hints**: Relay URLs are included in `E` and `e` tags to help clients locate events
- **Plaintext Content**: Comment content is plaintext (no HTML/Markdown) as per NIP-22 spec
### Example: Comment on Pull Request
```jsonc
{
"kind": 1111,
"content": "This looks good, but consider adding error handling here.",
"tags": [
["E", "pr_event_id", "wss://relay.example.com", "pr_author_pubkey"],
["K", "1618"],
["P", "pr_author_pubkey", "wss://relay.example.com"],
["e", "pr_event_id", "wss://relay.example.com", "pr_author_pubkey"],
["k", "1618"],
["p", "pr_author_pubkey", "wss://relay.example.com"]
]
}
```
**Implementation**: `src/lib/services/nostr/highlights-service.ts` (comment creation), used throughout PR and issue discussion features
```

42
docs/34.md

@ -246,3 +246,45 @@ The event SHOULD include a list of `g` tags with grasp service websocket URLs in
## Possible things to be added later ## Possible things to be added later
- inline file comments kind (we probably need one for patches and a different one for merged files) - inline file comments kind (we probably need one for patches and a different one for merged files)
## GitRepublic Usage
NIP-34 is the core NIP that GitRepublic implements. All repository functionality is built on top of NIP-34 event kinds.
### Repository Announcements (Kind 30617)
GitRepublic uses repository announcements to:
- **Discover Repositories**: The landing page fetches all kind 30617 events to display available repositories
- **Repository Creation**: Users create repository announcements via the signup page
- **Repository Updates**: Settings changes update the repository announcement event
- **Fork Detection**: Forks are identified by `a` tags pointing to the original repository
### Pull Requests (Kind 1618)
- **PR Creation**: Users create PRs by pushing commits and creating kind 1618 events
- **PR Updates**: PR updates (kind 1619) change the tip commit of existing PRs
- **PR Display**: PRs are displayed with their full markdown content and metadata
- **PR Merging**: Merging a PR creates a status event (kind 1631) with merge commit information
### Issues (Kind 1621)
- **Issue Creation**: Users create issues for bug reports, feature requests, and questions
- **Issue Threading**: Issues use NIP-22 comments for discussion
- **Issue Status**: Status events (kinds 1630-1633) track issue state (open, resolved, closed, draft)
### Status Events (Kinds 1630-1633)
- **Status Tracking**: PRs and issues use status events to track their state
- **Most Recent Wins**: Only the most recent status event from the author or a maintainer is considered valid
- **Merge Tracking**: When a PR is merged (status 1631), the merge commit ID is included in tags
### Repository State (Kind 30618)
- **Branch Tracking**: Optional repository state events track branch and tag positions
- **State Updates**: Repository state can be updated to reflect current branch/tag positions
**Implementation**:
- Repository announcements: `src/routes/signup/+page.svelte`, `src/lib/services/nostr/repo-polling.ts`
- Pull requests: `src/lib/services/nostr/prs-service.ts`
- Issues: `src/lib/services/nostr/issues-service.ts`
- Status events: Used throughout PR and issue management

30
docs/65.md

@ -41,3 +41,33 @@ Clients SHOULD guide users to keep `kind:10002` lists small (2-4 relays of each
### Discoverability ### Discoverability
Clients SHOULD spread an author's `kind:10002` event to as many relays as viable, paying attention to relays that, at any moment, serve naturally as well-known public indexers for these relay lists (where most other clients and users are connecting to in order to publish and fetch those). Clients SHOULD spread an author's `kind:10002` event to as many relays as viable, paying attention to relays that, at any moment, serve naturally as well-known public indexers for these relay lists (where most other clients and users are connecting to in order to publish and fetch those).
## GitRepublic Usage
GitRepublic uses NIP-65 relay lists to discover user's preferred relays for publishing events. This ensures events are published to relays the user trusts and uses.
### Relay Discovery
When publishing repository announcements, PRs, issues, or other events:
1. **Fetch User's Relay List**: GitRepublic fetches the user's kind 10002 event
2. **Extract Write Relays**: Parses `r` tags with `write` marker or no marker (both read and write)
3. **Combine with Defaults**: Combines user's outbox relays with default relays for redundancy
4. **Publish**: Sends events to the combined relay list
### Fallback to Kind 3
If no kind 10002 event is found, GitRepublic falls back to kind 3 (contact list) for relay discovery, maintaining compatibility with older clients.
### Implementation
```typescript
// Fetch user's relay preferences
const { inbox, outbox } = await getUserRelays(pubkey, nostrClient);
const userRelays = combineRelays(outbox);
// Publish to user's preferred relays
await nostrClient.publishEvent(signedEvent, userRelays);
```
**Implementation**: `src/lib/services/nostr/user-relays.ts`, used when publishing all repository-related events

44
docs/84.md

@ -49,3 +49,47 @@ This is to prevent the creation and multiple notes (highlight + kind 1) for a si
p-tag mentions MUST have a `mention` attribute to distinguish it from authors and editors. p-tag mentions MUST have a `mention` attribute to distinguish it from authors and editors.
r-tag urls from the comment MUST have a `mention` attribute to distinguish from the highlighted source url. The source url MUST have the `source` attribute. r-tag urls from the comment MUST have a `mention` attribute to distinguish from the highlighted source url. The source url MUST have the `source` attribute.
## GitRepublic Usage
GitRepublic extends NIP-84 highlights for code review and code selection features. Highlights enable users to select and comment on specific code sections in pull requests.
### Code Highlighting
When users select code in a PR for review:
1. **Highlight Creation**: A kind 9802 event is created with the selected code in the `content` field
2. **File Context**: Custom tags (`file`, `line-start`, `line-end`) specify which file and lines are highlighted
3. **PR Reference**: The highlight references the PR using `a` and `e` tags
4. **Attribution**: `P` and `p` tags reference the PR author and highlight creator
### Extended Tags
GitRepublic extends NIP-84 with file-specific tags for code context:
- `file`: File path being highlighted
- `line-start`: Starting line number
- `line-end`: Ending line number
- `context`: Optional surrounding context
- `comment`: Optional comment text for quote highlights
### Example: Code Highlight on PR
```jsonc
{
"kind": 9802,
"content": "const result = await fetch(url);",
"tags": [
["a", "1618:pr_author_pubkey.../repo-name"],
["e", "pr_event_id"],
["P", "pr_author_pubkey"],
["K", "1618"],
["file", "src/main.ts"],
["line-start", "42"],
["line-end", "45"],
["context", "// Fetch data from API"],
["p", "pr_author_pubkey", "wss://relay.example.com", "author"]
]
}
```
**Implementation**: `src/lib/services/nostr/highlights-service.ts`

44
docs/98.md

@ -61,3 +61,47 @@ eyJpZCI6ImZlOTY0ZTc1ODkwMzM2MGYyOGQ4NDI0ZDA5MmRhODQ5NGVkMjA3Y2JhODIzMTEwYmUzYTU3
## Reference Implementations ## Reference Implementations
- C# ASP.NET `AuthenticationHandler` [NostrAuth.cs](https://gist.github.com/v0l/74346ae530896115bfe2504c8cd018d3) - C# ASP.NET `AuthenticationHandler` [NostrAuth.cs](https://gist.github.com/v0l/74346ae530896115bfe2504c8cd018d3)
## GitRepublic Usage
NIP-98 is used extensively in GitRepublic for authenticating git operations (clone, push, pull) and API requests. This enables secure git operations without requiring traditional username/password authentication.
### Git Operations Authentication
All git operations (push, pull, clone) use NIP-98 authentication:
1. **Client Creates Auth Event**: User's browser extension creates a kind 27235 event with:
- `u` tag: Absolute URL of the git endpoint
- `method` tag: HTTP method (POST for push, GET for pull/clone)
- `payload` tag: SHA256 hash of request body (for POST requests)
2. **Base64 Encoding**: The event is base64-encoded and sent in `Authorization: Nostr {base64_event}` header
3. **Server Verification**: GitRepublic verifies:
- Event kind is 27235
- Timestamp is within 60 seconds
- URL matches exactly (normalized, trailing slashes removed)
- HTTP method matches
- Payload hash matches (for POST requests)
- Event signature is valid
### API Endpoint Authentication
API endpoints that modify repository state also use NIP-98:
- File creation/editing
- Repository settings updates
- PR/issue creation
- Comment posting
### URL Normalization
GitRepublic normalizes URLs before comparison to handle trailing slashes and ensure consistent matching:
- Removes trailing slashes
- Preserves query parameters
- Handles both HTTP and HTTPS
### Fallback to Kind 1
If NIP-98 authentication fails, GitRepublic can fall back to kind 1 (text note) events for relay write proofs, though this is less secure and not recommended.
**Implementation**: `src/lib/services/nostr/nip98-auth.ts`, used in all git operation endpoints and API routes

237
docs/CustomKinds.md

@ -0,0 +1,237 @@
# Custom Event Kinds
This document describes the custom event kinds used by GitRepublic that are not part of any standard NIP. These may be proposed as NIPs in the future.
## Kind 1640: Commit Signature
**Status**: Custom implementation (not in any NIP)
Git commit signature events are used to cryptographically sign git commits using Nostr keys. This provides cryptographic proof that a commit was made by a specific Nostr user.
### Event Structure
```jsonc
{
"kind": 1640,
"pubkey": "committer_pubkey_hex...",
"created_at": 1234567890,
"content": "Signed commit: <commit-message>\n\n<optional-additional-info>",
"tags": [
["commit", "abc123def456..."], // Final commit hash (added after commit is created)
["author", "John Doe"], // Author name
["author", "john@example.com"], // Author email (second author tag)
["message", "Fix bug in feature"], // Commit message
["e", "nip98_auth_event_id"] // Optional: Reference to NIP-98 auth event
],
"id": "...",
"sig": "..."
}
```
### Tag Descriptions
- **`commit`** (required): The final commit hash after the commit is created. This tag is added after the commit is created, as the hash is not known beforehand.
- **`author`** (required, appears twice): First occurrence contains the author name, second contains the author email.
- **`message`** (required): The commit message text.
- **`e`** (optional): Reference to a NIP-98 authentication event if the commit was made via HTTP git operations.
### Usage in GitRepublic
1. **Client-Side Signing**: When users make commits through the web interface, they can sign commits using NIP-07 (browser extension). The signature event is created client-side and keys never leave the browser.
2. **Server-Side Signing**: For git operations (push via git client), commits can be signed using NIP-98 authentication events. The commit signature event references the NIP-98 event.
3. **Commit Message Embedding**: The signature is embedded in the git commit message as a trailer:
```
Nostr-Signature: <event_id> <signature>
```
4. **Verification**: Commit signatures can be verified by:
- Checking the event signature
- Verifying the commit hash matches
- Confirming the author information matches the commit
### Rationale
Using a dedicated kind (1640) instead of kind 1 (text note) prevents spamming the user's feed with commit signatures. It also provides a clear, searchable way to find all commits signed by a specific user.
**Implementation**: `src/lib/services/git/commit-signer.ts`
---
## Kind 1641: Ownership Transfer
**Status**: Custom implementation (not in any NIP)
Repository ownership transfer events enable transferring repository ownership from one pubkey to another. This is a **non-replaceable event** to maintain an immutable chain of ownership.
### Event Structure
#### Regular Ownership Transfer
```jsonc
{
"kind": 1641,
"pubkey": "old_owner_pubkey_hex...",
"created_at": 1234567890,
"content": "Transferring ownership of repository my-repo to new maintainer",
"tags": [
["a", "30617:old_owner_pubkey.../my-repo"], // Repository address
["p", "new_owner_pubkey_hex..."], // New owner pubkey (hex or npub)
["d", "my-repo"] // Repository identifier
],
"id": "...",
"sig": "..."
}
```
#### Self-Transfer (Initial Ownership Proof)
```jsonc
{
"kind": 1641,
"pubkey": "owner_pubkey_hex...",
"created_at": 1234567890,
"content": "Initial ownership proof for repository my-repo",
"tags": [
["a", "30617:owner_pubkey.../my-repo"],
["p", "owner_pubkey_hex..."], // Same as pubkey (self-transfer)
["d", "my-repo"],
["t", "self-transfer"] // Marker for initial ownership proof
],
"id": "...",
"sig": "..."
}
```
### Tag Descriptions
- **`a`** (required): Repository address in format `30617:<owner-pubkey>:<repo-name>`
- **`p`** (required): New owner pubkey (can be hex or npub format). For self-transfers, this is the same as the event `pubkey`.
- **`d`** (required): Repository identifier (d-tag from repository announcement)
- **`t`** (optional): `"self-transfer"` marker for initial ownership proofs
### Usage in GitRepublic
1. **Initial Ownership**: When a repository is first created, a self-transfer event (owner → owner) is published to establish initial ownership proof. This creates an immutable record that the owner created the repository.
2. **Ownership Transfers**: When transferring ownership to another user:
- The current owner creates a kind 1641 event with the new owner's pubkey
- The new owner must accept the transfer (future enhancement)
- The transfer creates an immutable chain of ownership
3. **Ownership Verification**: To verify current ownership:
- Find the repository announcement (kind 30617)
- Find all ownership transfer events for that repository
- Follow the chain from the initial self-transfer to the most recent transfer
- The most recent transfer's `p` tag indicates the current owner
4. **Fork Operations**: When forking a repository, a self-transfer event is created to prove the fork owner's claim to the forked repository.
### Rationale
NIP-34 doesn't define ownership transfers. This custom kind provides:
- **Immutability**: Non-replaceable events create an unchangeable chain
- **Auditability**: Full history of ownership changes
- **Security**: Cryptographic proof of ownership transfers
- **Fork Integrity**: Ensures forks can be traced back to their origin
**Implementation**: `src/lib/services/nostr/ownership-transfer-service.ts`
---
## Kind 30620: Branch Protection
**Status**: Custom implementation (not in any NIP)
Branch protection rules allow repository owners to enforce policies on specific branches, such as requiring pull requests, reviewers, or status checks before merging.
### Event Structure
```jsonc
{
"kind": 30620,
"pubkey": "owner_pubkey_hex...",
"created_at": 1234567890,
"content": "",
"tags": [
["d", "my-repo"], // Repository name
["a", "30617:owner_pubkey.../my-repo"], // Repository address
// Branch: main
["branch", "main"], // Branch name
["branch", "main", "require-pr"], // Require pull request (no value)
["branch", "main", "require-reviewers", "npub1reviewer..."], // Required reviewer
["branch", "main", "require-reviewers", "npub2reviewer..."], // Another reviewer
["branch", "main", "require-status", "ci"], // Required status check
["branch", "main", "require-status", "lint"], // Another required status
// Branch: develop
["branch", "develop"],
["branch", "develop", "require-pr"],
["branch", "develop", "allow-force-push"], // Allow force push
["branch", "develop", "allowed-maintainers", "npub1maintainer..."] // Can bypass protection
],
"id": "...",
"sig": "..."
}
```
### Tag Descriptions
- **`d`** (required): Repository name/identifier
- **`a`** (required): Repository address in format `30617:<owner>:<repo>`
- **`branch`** (required, appears multiple times): Branch name and protection settings
- `["branch", "<name>"]`: Declares a protected branch
- `["branch", "<name>", "require-pr"]`: Requires pull request before merging
- `["branch", "<name>", "allow-force-push"]`: Allows force push to this branch
- `["branch", "<name>", "require-reviewers", "<pubkey>"]`: Required reviewer (can appear multiple times)
- `["branch", "<name>", "require-status", "<check-name>"]`: Required status check (can appear multiple times)
- `["branch", "<name>", "allowed-maintainers", "<pubkey>"]`: Maintainer who can bypass protection (can appear multiple times)
### Protection Rules
1. **Require Pull Request**: Direct pushes to protected branches are blocked. All changes must come through pull requests.
2. **Require Reviewers**: Pull requests to protected branches must be approved by at least one of the specified reviewers.
3. **Require Status Checks**: All specified status checks must pass before a PR can be merged.
4. **Allow Force Push**: By default, force pushes are blocked on protected branches. This tag allows them.
5. **Allowed Maintainers**: Specified maintainers can bypass protection rules (e.g., for emergency fixes).
### Usage in GitRepublic
1. **Setting Protection**: Repository owners create/update branch protection rules by publishing a kind 30620 event.
2. **Enforcement**: When users attempt to:
- Push directly to a protected branch → Blocked if `require-pr` is set
- Merge a PR to a protected branch → Requires reviewers and status checks if specified
- Force push → Blocked unless `allow-force-push` is set or user is an allowed maintainer
3. **Replaceable Events**: Branch protection events are replaceable (same `d` tag). Publishing a new event replaces all previous rules.
4. **Access Control**: Only the repository owner can create/update branch protection rules.
### Rationale
NIP-34 doesn't define branch protection. This custom kind provides:
- **Code Quality**: Enforces code review and testing before merging
- **Security**: Prevents direct pushes to critical branches
- **Flexibility**: Configurable rules per branch
- **Maintainability**: Clear, auditable protection rules
**Implementation**: `src/lib/services/nostr/branch-protection-service.ts` (if implemented), referenced in repository settings
---
## Summary
| Kind | Name | Replaceable | Purpose |
|------|------|-------------|---------|
| 1640 | Commit Signature | No | Cryptographically sign git commits |
| 1641 | Ownership Transfer | No | Transfer repository ownership (immutable chain) |
| 30620 | Branch Protection | Yes | Enforce branch protection rules |
These custom kinds extend NIP-34's git collaboration features with additional functionality needed for a production git hosting platform. They may be proposed as NIPs in the future to standardize these features across the Nostr ecosystem.

1147
docs/NIP_COMPLIANCE.md

File diff suppressed because it is too large Load Diff

52
src/app.css

@ -782,6 +782,58 @@ pre code {
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.search-section {
margin-bottom: 2rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.search-bar-container {
width: 100%;
}
.search-bar-container .search-input {
width: 100%;
padding: 0.75rem 1rem;
font-size: 1rem;
border: 1px solid var(--border-color);
border-radius: 0.375rem;
background: var(--card-bg);
color: var(--text-primary);
font-family: 'IBM Plex Serif', serif;
transition: all 0.2s ease;
}
.search-bar-container .search-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(126, 40, 94, 0.1);
}
.search-bar-container .search-input:disabled {
opacity: 0.6;
cursor: not-allowed;
background: var(--bg-secondary);
}
.filter-checkbox {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
user-select: none;
color: var(--text-primary);
font-size: 0.9rem;
}
.filter-checkbox input[type="checkbox"] {
cursor: pointer;
width: 1.1rem;
height: 1.1rem;
accent-color: var(--accent);
}
.repos-list { .repos-list {
display: grid; display: grid;
gap: 1.5rem; gap: 1.5rem;

2
src/lib/types/nostr.ts

@ -27,7 +27,7 @@ export interface NostrFilter {
export const KIND = { export const KIND = {
TEXT_NOTE: 1, // NIP-01: Text note (used for relay write proof fallback) TEXT_NOTE: 1, // NIP-01: Text note (used for relay write proof fallback)
CONTACT_LIST: 3, // NIP-02: Contact list CONTACT_LIST: 3, // NIP-02: Contact list - See /docs for GitRepublic usage documentation
DELETION_REQUEST: 5, // NIP-09: Event deletion request DELETION_REQUEST: 5, // NIP-09: Event deletion request
REPO_ANNOUNCEMENT: 30617, // NIP-34: Repository announcement REPO_ANNOUNCEMENT: 30617, // NIP-34: Repository announcement
REPO_STATE: 30618, // NIP-34: Repository state REPO_STATE: 30618, // NIP-34: Repository state

270
src/routes/+page.svelte

@ -7,11 +7,17 @@
import type { NostrEvent } from '../lib/types/nostr.js'; import type { NostrEvent } from '../lib/types/nostr.js';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { ForkCountService } from '../lib/services/nostr/fork-count-service.js'; import { ForkCountService } from '../lib/services/nostr/fork-count-service.js';
import { getPublicKeyWithNIP07, isNIP07Available } from '../lib/services/nostr/nip07-signer.js';
let repos = $state<NostrEvent[]>([]); let repos = $state<NostrEvent[]>([]);
let allRepos = $state<NostrEvent[]>([]); // Store all repos for filtering
let loading = $state(true); let loading = $state(true);
let error = $state<string | null>(null); let error = $state<string | null>(null);
let forkCounts = $state<Map<string, number>>(new Map()); let forkCounts = $state<Map<string, number>>(new Map());
let searchQuery = $state('');
let showOnlyMyContacts = $state(false);
let userPubkey = $state<string | null>(null);
let contactPubkeys = $state<Set<string>>(new Set());
import { DEFAULT_NOSTR_RELAYS } from '../lib/config.js'; import { DEFAULT_NOSTR_RELAYS } from '../lib/config.js';
const forkCountService = new ForkCountService(DEFAULT_NOSTR_RELAYS); const forkCountService = new ForkCountService(DEFAULT_NOSTR_RELAYS);
@ -20,8 +26,53 @@
onMount(async () => { onMount(async () => {
await loadRepos(); await loadRepos();
await loadUserAndContacts();
}); });
async function loadUserAndContacts() {
if (!isNIP07Available()) {
return;
}
try {
userPubkey = await getPublicKeyWithNIP07();
contactPubkeys.add(userPubkey); // Include user's own repos
// Fetch user's kind 3 contact list
const contactEvents = await nostrClient.fetchEvents([
{
kinds: [KIND.CONTACT_LIST],
authors: [userPubkey],
limit: 1
}
]);
if (contactEvents.length > 0) {
const contactEvent = contactEvents[0];
// Extract pubkeys from 'p' tags
for (const tag of contactEvent.tags) {
if (tag[0] === 'p' && tag[1]) {
let pubkey = tag[1];
// Try to decode if it's an npub
try {
const decoded = nip19.decode(pubkey);
if (decoded.type === 'npub') {
pubkey = decoded.data as string;
}
} catch {
// Assume it's already a hex pubkey
}
if (pubkey) {
contactPubkeys.add(pubkey);
}
}
}
}
} catch (err) {
console.warn('Failed to load user or contacts:', err);
}
}
async function loadRepos() { async function loadRepos() {
loading = true; loading = true;
error = null; error = null;
@ -55,6 +106,7 @@
// Sort by created_at descending // Sort by created_at descending
repos.sort((a, b) => b.created_at - a.created_at); repos.sort((a, b) => b.created_at - a.created_at);
allRepos = [...repos]; // Store all repos for filtering
// Load fork counts for all repos (in parallel, but don't block) // Load fork counts for all repos (in parallel, but don't block)
loadForkCounts(repos).catch(err => { loadForkCounts(repos).catch(err => {
@ -195,6 +247,201 @@
url?: string; url?: string;
ogType?: string; ogType?: string;
}; };
interface SearchResult {
repo: NostrEvent;
score: number;
matchType: string;
}
function performSearch() {
if (!searchQuery.trim()) {
repos = [...allRepos];
return;
}
const query = searchQuery.trim().toLowerCase();
const results: SearchResult[] = [];
// Filter by contacts if enabled
let reposToSearch = allRepos;
if (showOnlyMyContacts && contactPubkeys.size > 0) {
reposToSearch = allRepos.filter(event => {
// Check if owner is in contacts
if (contactPubkeys.has(event.pubkey)) return true;
// Check if any maintainer is in contacts
const maintainerTags = event.tags.filter(t => t[0] === 'maintainers');
for (const tag of maintainerTags) {
for (let i = 1; i < tag.length; i++) {
let maintainerPubkey = tag[i];
try {
const decoded = nip19.decode(maintainerPubkey);
if (decoded.type === 'npub') {
maintainerPubkey = decoded.data as string;
}
} catch {
// Assume it's already a hex pubkey
}
if (contactPubkeys.has(maintainerPubkey)) return true;
}
}
return false;
});
}
for (const repo of reposToSearch) {
let score = 0;
let matchType = '';
// Extract repo fields
const name = getRepoName(repo).toLowerCase();
const dTag = repo.tags.find(t => t[0] === 'd')?.[1]?.toLowerCase() || '';
const description = getRepoDescription(repo).toLowerCase();
const cloneUrls = getCloneUrls(repo).map(url => url.toLowerCase());
const maintainerTags = repo.tags.filter(t => t[0] === 'maintainers');
const maintainers: string[] = [];
for (const tag of maintainerTags) {
for (let i = 1; i < tag.length; i++) {
if (tag[i]) maintainers.push(tag[i].toLowerCase());
}
}
// Try to decode query as hex id, naddr, or nevent
let queryHex = '';
try {
const decoded = nip19.decode(query);
if (decoded.type === 'naddr' || decoded.type === 'nevent') {
queryHex = (decoded.data as any).id || '';
}
} catch {
// Not a bech32 encoded value
}
// Check if query is a hex pubkey or npub
let queryPubkey = '';
try {
const decoded = nip19.decode(query);
if (decoded.type === 'npub') {
queryPubkey = decoded.data as string;
}
} catch {
// Check if it's a hex pubkey (64 hex chars)
if (/^[0-9a-f]{64}$/i.test(query)) {
queryPubkey = query;
}
}
// Exact matches get highest score
if (name === query) {
score += 1000;
matchType = 'exact-name';
} else if (dTag === query) {
score += 1000;
matchType = 'exact-d-tag';
} else if (repo.id.toLowerCase() === query || repo.id.toLowerCase() === queryHex) {
score += 1000;
matchType = 'exact-id';
} else if (repo.pubkey.toLowerCase() === queryPubkey.toLowerCase()) {
score += 800;
matchType = 'exact-pubkey';
}
// Name matches
if (name.includes(query)) {
score += name.startsWith(query) ? 100 : 50;
if (!matchType) matchType = 'name';
}
// D-tag matches
if (dTag.includes(query)) {
score += dTag.startsWith(query) ? 100 : 50;
if (!matchType) matchType = 'd-tag';
}
// Description matches
if (description.includes(query)) {
score += 30;
if (!matchType) matchType = 'description';
}
// Pubkey matches (owner)
if (repo.pubkey.toLowerCase().includes(query.toLowerCase()) ||
(queryPubkey && repo.pubkey.toLowerCase() === queryPubkey.toLowerCase())) {
score += 200;
if (!matchType) matchType = 'pubkey';
}
// Maintainer matches
for (const maintainer of maintainers) {
if (maintainer.includes(query.toLowerCase())) {
score += 150;
if (!matchType) matchType = 'maintainer';
break;
}
// Check if maintainer is npub and matches query
try {
const decoded = nip19.decode(maintainer);
if (decoded.type === 'npub') {
const maintainerPubkey = decoded.data as string;
if (maintainerPubkey.toLowerCase().includes(query.toLowerCase()) ||
(queryPubkey && maintainerPubkey.toLowerCase() === queryPubkey.toLowerCase())) {
score += 150;
if (!matchType) matchType = 'maintainer';
break;
}
}
} catch {
// Not an npub, already checked above
}
}
// Clone URL matches
for (const url of cloneUrls) {
if (url.includes(query)) {
score += 40;
if (!matchType) matchType = 'clone-url';
break;
}
}
// Fulltext search in all tags and content
const allText = [
name,
dTag,
description,
...cloneUrls,
...maintainers,
repo.content.toLowerCase()
].join(' ');
if (allText.includes(query)) {
score += 10;
if (!matchType) matchType = 'fulltext';
}
if (score > 0) {
results.push({ repo, score, matchType });
}
}
// Sort by score (descending), then by created_at (descending)
results.sort((a, b) => {
if (b.score !== a.score) {
return b.score - a.score;
}
return b.repo.created_at - a.repo.created_at;
});
repos = results.map(r => r.repo);
}
// Reactive search when query or filter changes
$effect(() => {
if (!loading) {
performSearch();
}
});
</script> </script>
<svelte:head> <svelte:head>
@ -230,6 +477,29 @@
</button> </button>
</div> </div>
<div class="search-section">
<div class="search-bar-container">
<input
type="text"
bind:value={searchQuery}
placeholder="Search by name, d-tag, pubkey, maintainers, clone URL, hex id/naddr/nevent, or fulltext..."
class="search-input"
disabled={loading}
oninput={performSearch}
/>
</div>
{#if isNIP07Available() && userPubkey}
<label class="filter-checkbox">
<input
type="checkbox"
bind:checked={showOnlyMyContacts}
onchange={performSearch}
/>
<span>Show only my repos and those of my contacts</span>
</label>
{/if}
</div>
{#if error} {#if error}
<div class="error"> <div class="error">
Error loading repositories: {error} Error loading repositories: {error}

6
src/routes/signup/+page.svelte

@ -270,13 +270,13 @@
<div class="form-group"> <div class="form-group">
<label for="repo-name"> <label for="repo-name">
Repository Name * Repository Name *
<small>Will be used as the d-tag (normalized to lowercase with hyphens)</small> <small>Enter a normal name (e.g., "My Awesome Repo"). It will be automatically converted to a d-tag format (lowercase with hyphens, such as my-awesome-repo).</small>
</label> </label>
<input <input
id="repo-name" id="repo-name"
type="text" type="text"
bind:value={repoName} bind:value={repoName}
placeholder="my-awesome-repo" placeholder="My Awesome Repo"
required required
disabled={loading} disabled={loading}
/> />
@ -298,7 +298,7 @@
<div class="form-group"> <div class="form-group">
<div class="label"> <div class="label">
Clone URLs Clone URLs
<small>{$page.data.gitDomain || 'localhost:6543'} will be added automatically</small> <small>{$page.data.gitDomain || 'localhost:6543'} will be added automatically, but you can add any existing ones here.</small>
</div> </div>
{#each cloneUrls as url, index} {#each cloneUrls as url, index}
<div class="input-group"> <div class="input-group">

Loading…
Cancel
Save