Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

### Go Backend Development (in `/go/` directory)
```bash
# Build
go install -tags production github.com/keybase/client/go/keybase

# Test
make test # Run all tests
make testclean # Clean test environment
make cover # Run tests with coverage

# Lint & Format
make fmt # Format Go code
make vet # Run go vet
make golangci-lint-nonkbfs # Lint non-KBFS code
make golangci-lint-kbfs # Lint KBFS code

# Service
keybase service # Run local service
keybase ctl --help # Control service
```

### React/React Native Development (in `/shared/` directory)
```bash
# Desktop Development
yarn start # Start desktop app in dev mode
yarn start-hot # Start with hot reloading
yarn build-dev # Build development version
yarn build-prod # Build production version

# Mobile Development
yarn rn-start # Start React Native packager
yarn rn-gobuild-ios # Build Go components for iOS
yarn rn-gobuild-android # Build Go components for Android
yarn pod-install # Install iOS CocoaPods

# Code Quality
yarn lint # ESLint with TypeScript
yarn prettier-write-all # Format all code
yarn tsc # TypeScript compilation check
yarn coverage # TypeScript coverage
```

### Protocol Generation (in `/protocol/` directory)
```bash
make build # Generate protocol files from AVDL
make clean # Clean generated files
```

## Architecture Overview

**Keybase is a cryptographic communication platform with a service-based architecture:**

1. **Local Service Pattern**: A persistent Go service runs locally, handling all cryptographic operations, user management, and data storage. Multiple clients (CLI, desktop GUI, mobile apps) connect to this single service via RPC.

2. **Protocol-Driven Communication**: All inter-component communication uses AVDL (Avro IDL) defined protocols in `/protocol/`. These definitions auto-generate code for Go, TypeScript, and other languages, ensuring type-safe RPC across all platforms.

3. **Component Structure**:
- `/go/`: Core service implementation including crypto operations, chat backend, KBFS (encrypted filesystem), Git integration, and Stellar wallet
- `/shared/`: React-based frontend code shared between desktop (Electron) and mobile (React Native) apps
- `/protocol/`: AVDL protocol definitions that generate bindings for all platforms
- `/osx/`, `/android/`, `/ios/`: Platform-specific native code and build configurations

4. **Key Design Patterns**:
- **Monorepo**: All platforms developed in single repository for consistency
- **Code Generation**: Protocol definitions generate type-safe bindings automatically
- **Service Abstraction**: Frontend apps are thin clients; all business logic in Go service
- **Cross-Platform Code Sharing**: React components shared between desktop and mobile

5. **Security Architecture**:
- All cryptographic operations handled by Go service (never in frontend)
- Local key storage with platform-specific secure storage integration
- Export-controlled cryptographic software with code signing for releases

When making changes:
- Protocol changes require regenerating bindings (`make build` in `/protocol/`)
- Go service changes may require restarting the service
- Frontend changes in `/shared/` affect both desktop and mobile apps
- Always run appropriate linters before committing (Go: `make fmt vet`, JS/TS: `yarn lint`)
89 changes: 67 additions & 22 deletions browser/js/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,83 @@ const bel = bundle.bel;
const morphdom = bundle.morphdom;
const asset = chrome.runtime.getURL;

let spaMonitorTrailer = false;

function init() {
chrome.storage.sync.get(function(options) {
if (options === undefined) {
// Backfill for Firefox
options = {};
}
if (location.hostname.endsWith('twitter.com') || location.hostname.endsWith('facebook.com')) {
// SPA hack: Monitor location for changes and re-init. Twitter and Facebook
// are single-page-apps that don't often don't refresh when navigating
// so that makes it difficult to hook into.
// The hack is to poll every second and if the location changed
// re-scan to place the button.
// Facebook is so slow to load after the location change that for each
// location we re-init when noticed and 1s later.
// FIXME: This is sad. An alternative would be very desirable.
// Subscribing to `popstate` does not work.
let loc = window.location.pathname;
function spaMonitor() {
if (spaMonitorTrailer || window.location.pathname != loc) {
// Path changed (or trailer), force fresh init
spaMonitorTrailer = !spaMonitorTrailer;
init();
return
// Monitor SPAs for navigation changes using MutationObserver instead of polling
let currentPath = window.location.pathname;

// Watch for URL changes using MutationObserver on document.body
const observer = new MutationObserver(() => {
if (window.location.pathname !== currentPath) {
currentPath = window.location.pathname;
// Debounce the re-initialization to avoid multiple triggers
clearTimeout(observer.reinitTimeout);
observer.reinitTimeout = setTimeout(() => {
init();
// For Facebook's slow loading, trigger again after a delay
if (location.hostname.endsWith('facebook.com')) {
setTimeout(init, 1000);
}
}, 100);
}
// We use RAF to avoid spamming checks when the tab is not active.
requestAnimationFrame(function() {
setTimeout(spaMonitor, 1000);
});

// Start observing when body is available
if (document.body) {
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
} else {
// Wait for body to be available
const bodyWatcher = new MutationObserver(() => {
if (document.body) {
bodyWatcher.disconnect();
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
}
});
bodyWatcher.observe(document.documentElement, {childList: true});
}
setTimeout(spaMonitor, 1000);

// Also listen to popstate and pushstate events as backup
window.addEventListener('popstate', () => {
if (window.location.pathname !== currentPath) {
currentPath = window.location.pathname;
init();
}
});

// Intercept pushState and replaceState
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;

history.pushState = function() {
originalPushState.apply(history, arguments);
if (window.location.pathname !== currentPath) {
currentPath = window.location.pathname;
setTimeout(init, 100);
}
};

history.replaceState = function() {
originalReplaceState.apply(history, arguments);
if (window.location.pathname !== currentPath) {
currentPath = window.location.pathname;
setTimeout(init, 100);
}
};
}

const user = matchService(window.location, document);
Expand Down
33 changes: 1 addition & 32 deletions go/client/cmd_currency.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package client

import (
"errors"
"fmt"

"github.com/keybase/cli"
Expand Down Expand Up @@ -33,36 +32,6 @@ func NewCmdCurrency(cl *libcmdline.CommandLine, g *libkb.GlobalContext) cli.Comm
}
}

func NewCmdBTC(cl *libcmdline.CommandLine, g *libkb.GlobalContext) cli.Command {
return cli.Command{
Name: "btc",
Action: func(c *cli.Context) {
cl.ChooseCommand(NewCmdBTCRunner(g), "btc", c)
},
}
}

type CmdBTC struct {
libkb.Contextified
}

func NewCmdBTCRunner(g *libkb.GlobalContext) *CmdBTC {
return &CmdBTC{
Contextified: libkb.NewContextified(g),
}
}

func (c *CmdBTC) Run() (err error) {
return errors.New("this command is deprecated; use `keybase currency add` instead")
}

func (c *CmdBTC) GetUsage() libkb.Usage {
return libkb.Usage{}
}

func (c *CmdBTC) ParseArgv(ctx *cli.Context) error {
return nil
}

func (c *CmdCurrencyAdd) ParseArgv(ctx *cli.Context) error {
if len(ctx.Args()) != 1 {
Expand All @@ -72,7 +41,7 @@ func (c *CmdCurrencyAdd) ParseArgv(ctx *cli.Context) error {
c.force = ctx.Bool("force")
w := ctx.String("type")
if !(w == "bitcoin" || w == "zcash" || w == "") {
return fmt.Errorf("Bad address type; can only handle 'zcash' or 'bitcoin")
return fmt.Errorf("Bad address type; can only handle 'zcash' or 'bitcoin'")
}
c.wanted = w
return nil
Expand Down
1 change: 0 additions & 1 deletion go/client/commands_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ func GetCommands(cl *libcmdline.CommandLine, g *libkb.GlobalContext) []cli.Comma
NewCmdBase62(cl, g),
NewCmdBlocks(cl, g),
NewCmdBot(cl, g),
NewCmdBTC(cl, g),
NewCmdCA(cl, g),
NewCmdChat(cl, g),
NewCmdCompatDir(cl, g),
Expand Down
18 changes: 14 additions & 4 deletions go/teams/member_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,27 @@ func newMemberSetChange(ctx context.Context, g *libkb.GlobalContext, req keybase
}

func (m *memberSet) recipientUids() []keybase1.UID {
uids := make([]keybase1.UID, 0, len(m.recipients))
if len(m.recipients) == 0 {
return nil
}
uids := make([]keybase1.UID, len(m.recipients))
i := 0
for uv := range m.recipients {
uids = append(uids, uv.Uid)
uids[i] = uv.Uid
i++
}
return uids
}

func (m *memberSet) restrictedBotRecipientUids() []keybase1.UID {
uids := make([]keybase1.UID, 0, len(m.restrictedBotRecipients))
if len(m.restrictedBotRecipients) == 0 {
return nil
}
uids := make([]keybase1.UID, len(m.restrictedBotRecipients))
i := 0
for uv := range m.restrictedBotRecipients {
uids = append(uids, uv.Uid)
uids[i] = uv.Uid
i++
}
return uids
}
Expand Down
2 changes: 1 addition & 1 deletion rnmodules/react-native-drop-view/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const DropViewWrapper = (p: Props) => {
}, new Array<DropItems[0]>())
onDropped(cleanedUp)
} catch (e) {
console.log('drop view error', e)
// Silently handle drop errors - items will be empty
}
},
[onDropped]
Expand Down
36 changes: 0 additions & 36 deletions shared/app/runtime-stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,28 +54,6 @@ const dbTypeString = (s: T.RPCGen.DbType) => {
}
}

// let destroyRadar: (() => void) | undefined
// let radarNode: HTMLDivElement | undefined
// const radarSize = 30

// const makeRadar = (show: boolean) => {
// if (destroyRadar) {
// destroyRadar()
// destroyRadar = undefined
// }
// if (!radarNode || !show) {
// return
// }

// destroyRadar = lagRadar({
// frames: 5,
// inset: 1,
// parent: radarNode,
// size: radarSize,
// speed: 0.0017 * 0.7,
// })
// }

// simple bucketing of incoming log lines, we have a queue of incoming items, we bucket them
// and choose a max to show. We use refs a lot since we only want to figure stuff out based on an interval
// TODO mobile
Expand Down Expand Up @@ -206,20 +184,6 @@ const LogStats = (props: {num?: number}) => {
}

const RuntimeStatsDesktop = ({stats}: Props) => {
// const [showRadar, setShowRadar] = React.useState(false)
// const refContainer = React.useCallback(
// node => {
// radarNode = node
// makeRadar(showRadar)
// },
// [showRadar]
// )
// const toggleRadar = () => {
// const show = !showRadar
// setShowRadar(show)
// makeRadar(show)
// }

const [moreLogs, setMoreLogs] = React.useState(false)

return (
Expand Down
Loading