Skip to content
Closed
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
127 changes: 127 additions & 0 deletions FEATURE_TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Auth Wallet Feature

Adding NUT-21/22 authentication support (CAT + BAT) to coco-cashu.

## Background

BAT (Blind Auth Token) is a standard ecash proof with `unit:'auth', amount:1`.
Most of the existing proof infrastructure can be reused; only OIDC session management is new.
cashu-ts provides `AuthManager` which handles the full CAT/BAT lifecycle internally.

## Completed

### 1. AuthSession Model
- [x] `models/AuthSession.ts` — session data structure
- [x] `models/index.ts` export

### 2. Repository
- [x] `repositories/index.ts` — `AuthSessionRepository` interface
- [x] `RepositoriesBase` — `authSessionRepository` field
- [x] `repositories/memory/MemoryAuthSessionRepository.ts` — in-memory implementation for tests
- [x] `repositories/memory/index.ts` export
- [x] `MemoryRepositories.ts` wiring

### 3. Errors
- [x] `models/Error.ts` — `AuthSessionError`, `AuthSessionExpiredError`

### 4. Events
- [x] `events/types.ts` CoreEvents:
- `auth-session:updated`
- `auth-session:deleted`
- `auth-session:expired`

### 5. Service
- [x] `services/AuthSessionService.ts` — session CRUD + expiration validation
- [x] `services/index.ts` export

### 6. Tests
- [x] `test/unit/AuthSessionService.test.ts` — unit tests

### 7. API Integration (AuthApi + Manager + MintAdapter)
- [x] `api/AuthApi.ts` — orchestrates cashu-ts AuthManager per mint
- `startDeviceAuth(mintUrl)` — OIDC Device Code Flow
- `login(mintUrl, tokens)` — manual login with externally obtained tokens
- `restore(mintUrl)` — restore session on app restart
- `logout(mintUrl)` — delete session + disconnect AuthProvider
- `getSession(mintUrl)` / `hasSession(mintUrl)` — session queries
- `getAuthProvider(mintUrl)` — access cashu-ts AuthProvider
- [x] `Manager.ts` — `readonly auth: AuthApi` property
- [x] `MintAdapter.ts` — `setAuthProvider`/`clearAuthProvider`
- Passes `authProvider` to cashu-ts `Mint` constructor
- [x] `api/index.ts` export
- [x] `services/AuthSessionService.ts` — fixed Logger import (`@nestjs/common` → `@core/logging`)
- [x] `test/unit/AuthApi.test.ts` — 9 unit tests
- [x] `test/integration/auth-session.test.ts` — end-to-end via `mgr.auth.startDeviceAuth()`

## Remaining Work

### Phase 1: Storage Adapters (Persistence)

Memory adapter is test-only. Platform adapters needed for real persistence:

- [ ] `packages/sqlite3/` — Node/CLI
- [ ] `packages/indexeddb/` — Web browser
- [ ] `packages/expo-sqlite/` — React Native
- [ ] `packages/adapter-tests/` — contract tests

### Phase 2: BAT Persistence (after multi-unit support)

Persist the cashu-ts AuthManager BAT pool across app restarts:

- [ ] `AuthManager.exportPool()` → save to repository
- [ ] `AuthManager.importPool()` → restore on startup
- [ ] Parameterize WalletService unit (`unit:'auth'` keyset support)
- [ ] Wire authProvider into `WalletService.buildWallet()` Mint/Wallet instances

### Phase 3: React Wrapper (optional)

- [ ] `packages/react/` auth hooks
- `useAuthSession()`
- `useBatPool()`

## Architecture (Current)

```
mgr.auth.startDeviceAuth(mintUrl)
→ Creates AuthManager + OIDCAuth (cashu-ts)
→ oidc.startDeviceAuth() → user authorizes → poll()
→ AuthSessionService.saveSession() (persistence)
→ MintAdapter.setAuthProvider() (injects authProvider into Mint)
→ All subsequent Mint requests auto-include CAT/BAT headers

AuthManager (built into cashu-ts):
- CAT storage / retrieval / auto-refresh via OIDCAuth
- BAT auto-minting / pool management / DLEQ validation
- Auto-detects NUT-21 (CAT) vs NUT-22 (BAT) per endpoint
```

## Reference Patterns

| New | Existing Pattern |
|-----|-----------------|
| AuthSessionRepository | MintQuoteRepository |
| AuthSessionService | MintQuoteService |

## Conventions

- **Normalize mint URLs**: always pass through `normalizeMintUrl()` before storage/comparison
- **Emit events**: emit EventBus events on every state change
- **Domain errors**: use `models/Error.ts` classes instead of plain `new Error()`
- **Include cause**: preserve original error when wrapping

## Running Tests

```sh
# All core tests
bun run --filter='coco-cashu-core' test

# AuthSessionService only
cd packages/core
bun test test/unit/AuthSessionService.test.ts

# AuthApi only
bun test test/unit/AuthApi.test.ts

# Integration test (requires running mint with NUT-21/22 + manual OIDC authorization)
MINT_URL=http://localhost:8085 bun test test/integration/auth-session.test.ts --timeout 300000
```
129 changes: 91 additions & 38 deletions bun.lock

Large diffs are not rendered by default.

21 changes: 19 additions & 2 deletions packages/core/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
KeyRingService,
TransactionService,
PaymentRequestService,
AuthSessionService,
} from './services';
import { SendOperationService } from './operations/send/SendOperationService';
import { MeltOperationService } from './operations/melt/MeltOperationService';
Expand All @@ -34,7 +35,7 @@ import {
} from './infra';
import { EventBus, type CoreEvents } from './events';
import { type Logger, NullLogger } from './logging';
import { MintApi, WalletApi, QuotesApi, HistoryApi, KeyRingApi, SendApi } from './api';
import { MintApi, WalletApi, QuotesApi, HistoryApi, KeyRingApi, SendApi, AuthApi } from './api';
import { SubscriptionApi } from './api/SubscriptionApi.ts';
import { PluginHost } from './plugins/PluginHost.ts';
import type { Plugin, ServiceMap, PluginExtensions } from './plugins/types.ts';
Expand Down Expand Up @@ -164,6 +165,7 @@ export class Manager {
readonly subscription: SubscriptionApi;
readonly history: HistoryApi;
readonly send: SendApi;
readonly auth: AuthApi;
readonly ext: PluginExtensions;
private mintService: MintService;
private walletService: WalletService;
Expand All @@ -184,6 +186,7 @@ export class Manager {
private counterService: CounterService;
private transactionService: TransactionService;
private paymentRequestService: PaymentRequestService;
private authSessionService: AuthSessionService;
private sendOperationService: SendOperationService;
private sendOperationRepository: SendOperationRepository;
private meltOperationService: MeltOperationService;
Expand Down Expand Up @@ -241,6 +244,7 @@ export class Manager {
this.sendOperationRepository = core.sendOperationRepository;
this.meltOperationService = core.meltOperationService;
this.meltOperationRepository = core.meltOperationRepository;
this.authSessionService = core.authSessionService;
this.proofRepository = repositories.proofRepository;
const apis = this.buildApis();
this.mint = apis.mint;
Expand All @@ -250,6 +254,7 @@ export class Manager {
this.subscription = apis.subscription;
this.history = apis.history;
this.send = apis.send;
this.auth = apis.auth;

// Point ext to pluginHost's extensions storage
this.ext = this.pluginHost.getExtensions() as PluginExtensions;
Expand Down Expand Up @@ -549,6 +554,7 @@ export class Manager {
sendOperationRepository: SendOperationRepository;
meltOperationService: MeltOperationService;
meltOperationRepository: MeltOperationRepository;
authSessionService: AuthSessionService;
} {
const mintLogger = this.getChildLogger('MintService');
const walletLogger = this.getChildLogger('WalletService');
Expand Down Expand Up @@ -672,6 +678,13 @@ export class Manager {
paymentRequestLogger,
);

const authSessionLogger = this.getChildLogger('AuthSessionService');
const authSessionService = new AuthSessionService(
repositories.authSessionRepository,
authSessionLogger,
this.eventBus,
);

return {
mintService,
seedService,
Expand All @@ -690,6 +703,7 @@ export class Manager {
sendOperationRepository,
meltOperationService,
meltOperationRepository,
authSessionService,
};
}

Expand All @@ -701,6 +715,7 @@ export class Manager {
subscription: SubscriptionApi;
history: HistoryApi;
send: SendApi;
auth: AuthApi;
} {
const walletApiLogger = this.getChildLogger('WalletApi');
const subscriptionApiLogger = this.getChildLogger('SubscriptionApi');
Expand All @@ -724,6 +739,8 @@ export class Manager {
const subscription = new SubscriptionApi(this.subscriptions, subscriptionApiLogger);
const history = new HistoryApi(this.historyService);
const send = new SendApi(this.sendOperationService);
return { mint, wallet, quotes, keyring, subscription, history, send };
const authApiLogger = this.getChildLogger('AuthApi');
const auth = new AuthApi(this.authSessionService, this.mintAdapter, authApiLogger);
return { mint, wallet, quotes, keyring, subscription, history, send, auth };
}
}
Loading