mirror of
https://github.com/kind-0/nsecbunkerd.git
synced 2026-05-03 07:00:11 +00:00
feat!: Add Admin API methods and NIP-46 compliance
BREAKING CHANGE: Admin API now uses kind 24133 instead of 24134 This release adds 11 new admin methods and ensures full NIP-46 compliance by switching from the non-standard kind 24134 to the spec-compliant 24133. ## New Admin Methods ### Key Management - `get_key` - Get details about a specific key - `delete_key` - Soft-delete a key and its tokens - `rotate_key` - Create new key and migrate permissions ### Policy Management - `get_policy` - Get policy details with rules - `delete_policy` - Soft-delete a policy ### Permission Management - `grant_permission` - Grant user access with a policy - `revoke_permission` - Revoke user's access to a key - `get_permissions` - Get user's permissions on a key ### Token Management - `get_token` - Get token details - `revoke_token` - Soft-delete a token - `validate_token` - Check if a token is valid ## NIP-46 Compliance Changed all admin communication from kind 24134 to kind 24133 (NDKKind.NostrConnect) to comply with the NIP-46 specification. Kind 24134 was never part of the official spec. ## Other Changes - Added Vitest test framework with 93 tests - Fixed TypeScript compilation errors - Updated documentation with breaking change notice - Version bump to 0.11.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
deda485763
commit
16dc15b486
118
README.md
118
README.md
@ -177,6 +177,124 @@ When a bunker provides a wallet and zapping service (`wallet` and `nostdressUrl`
|
||||
}
|
||||
```
|
||||
|
||||
# Admin API
|
||||
|
||||
nsecbunkerd exposes an admin API over Nostr (NIP-46) that allows authorized administrators to manage keys, policies, permissions, and tokens remotely.
|
||||
|
||||
## ⚠️ Breaking Change: NIP-46 Compliance (v0.11.0)
|
||||
|
||||
**Previous versions** of nsecbunkerd used event kind `24134` for admin API responses. This was a custom extension that was never part of the official NIP-46 specification.
|
||||
|
||||
**Starting with v0.11.0**, all admin communication now uses kind `24133` (`NDKKind.NostrConnect`) to be fully compliant with the [NIP-46 specification](https://github.com/nostr-protocol/nips/blob/master/46.md).
|
||||
|
||||
### Why this change?
|
||||
|
||||
1. **NIP-46 Compliance**: The NIP-46 spec only defines kind `24133` for all Nostr Connect communication. Kind `24134` was never standardized and doesn't exist in the spec.
|
||||
|
||||
2. **Interoperability**: Using the standard kind ensures compatibility with other NIP-46 implementations and clients.
|
||||
|
||||
3. **Simplification**: Having a single event kind for all NIP-46 communication (both standard signing requests and admin methods) simplifies the protocol.
|
||||
|
||||
### Migration Guide
|
||||
|
||||
If you have custom clients that communicate with the nsecbunkerd admin API:
|
||||
|
||||
- **Update your client** to listen for responses on kind `24133` instead of `24134`
|
||||
- **No changes needed** for standard NIP-46 signing operations (these already used kind `24133`)
|
||||
- **Admin clients** (like [app.nsecbunker.com](https://app.nsecbunker.com)) will need to be updated to match this change
|
||||
|
||||
## Authentication
|
||||
|
||||
Only npubs listed in the `ADMIN_NPUBS` environment variable can access admin methods. All requests and responses use kind `24133` (NIP-46 NostrConnect).
|
||||
|
||||
## Available Methods
|
||||
|
||||
### Key Management
|
||||
|
||||
| Method | Parameters | Description |
|
||||
|--------|------------|-------------|
|
||||
| `get_keys` | none | List all keys with their status (locked/unlocked) |
|
||||
| `get_key` | `keyName` | Get details about a specific key |
|
||||
| `create_new_key` | `keyName`, `passphrase`, `[nsec]` | Create a new key or import existing nsec |
|
||||
| `unlock_key` | `keyName`, `passphrase` | Unlock an encrypted key |
|
||||
| `delete_key` | `keyName` | Soft-delete a key and its tokens |
|
||||
| `rotate_key` | `oldKeyName`, `newKeyName`, `passphrase` | Create new key and migrate permissions |
|
||||
|
||||
### Policy Management
|
||||
|
||||
| Method | Parameters | Description |
|
||||
|--------|------------|-------------|
|
||||
| `get_policies` | none | List all policies with their rules |
|
||||
| `get_policy` | `policyId` | Get details about a specific policy |
|
||||
| `create_new_policy` | `policyJson` | Create a new policy with rules |
|
||||
| `delete_policy` | `policyId` | Soft-delete a policy |
|
||||
|
||||
**Policy JSON Format:**
|
||||
```json
|
||||
{
|
||||
"name": "signing-policy",
|
||||
"expires_at": "2024-12-31T23:59:59Z",
|
||||
"rules": [
|
||||
{ "method": "sign_event", "kind": 1, "use_count": 100 },
|
||||
{ "method": "sign_event", "kind": 7 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Permission Management
|
||||
|
||||
| Method | Parameters | Description |
|
||||
|--------|------------|-------------|
|
||||
| `get_key_users` | `keyName` | List all users with access to a key |
|
||||
| `grant_permission` | `keyName`, `userPubkey`, `policyId`, `[description]` | Grant user access to a key with a policy |
|
||||
| `revoke_permission` | `keyName`, `userPubkey` | Revoke user's access to a key |
|
||||
| `get_permissions` | `keyName`, `userPubkey` | Get user's permissions on a key |
|
||||
| `rename_key_user` | `userPubkey`, `description` | Update a user's description |
|
||||
| `revoke_user` | `keyUserId` | Revoke user by ID |
|
||||
|
||||
### Token Management
|
||||
|
||||
| Method | Parameters | Description |
|
||||
|--------|------------|-------------|
|
||||
| `get_key_tokens` | `keyName` | List all tokens for a key |
|
||||
| `get_token` | `tokenId` | Get details about a specific token |
|
||||
| `create_new_token` | `keyName`, `clientName`, `policyId`, `[durationInHours]` | Create an access token |
|
||||
| `revoke_token` | `tokenId` | Revoke (soft-delete) a token |
|
||||
| `validate_token` | `tokenString` | Check if a token is valid |
|
||||
|
||||
### Utility
|
||||
|
||||
| Method | Parameters | Description |
|
||||
|--------|------------|-------------|
|
||||
| `ping` | none | Health check - returns "ok" |
|
||||
| `create_account` | `[username]`, `[domain]`, `[email]` | Create a new user account (OAuth flow) |
|
||||
|
||||
## Response Format
|
||||
|
||||
All responses follow NIP-46 format:
|
||||
```json
|
||||
{
|
||||
"id": "<request_id>",
|
||||
"result": "<json_stringified_result>",
|
||||
"error": "<optional_error_string>"
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
# Run tests in watch mode
|
||||
npm run test:watch
|
||||
|
||||
# Run tests with coverage
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
# Authors
|
||||
|
||||
* [pablof7z](nostr:npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft)
|
||||
|
||||
8935
package-lock.json
generated
Normal file
8935
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nsecbunkerd",
|
||||
"version": "0.10.6",
|
||||
"version": "0.11.0",
|
||||
"description": "nsecbunker daemon",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
@ -27,7 +27,10 @@
|
||||
"start": "node ./scripts/start.js",
|
||||
"lfg": "node ./scripts/start.js start",
|
||||
"nsecbunkerd": "node dist/index.js",
|
||||
"client": "node dist/client/client.js"
|
||||
"client": "node dist/client/client.js",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"keywords": [
|
||||
"nostr"
|
||||
@ -68,6 +71,7 @@
|
||||
"prisma": "^5.4.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsup": "^7.2.0",
|
||||
"typescript": "^5.1.3"
|
||||
"typescript": "^5.1.3",
|
||||
"vitest": "^1.6.1"
|
||||
}
|
||||
}
|
||||
|
||||
3772
pnpm-lock.yaml
generated
3772
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
660
project/IMPLEMENTATION_PLAN.md
Normal file
660
project/IMPLEMENTATION_PLAN.md
Normal file
@ -0,0 +1,660 @@
|
||||
# nsecbunkerd Missing Admin Methods Implementation Plan
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document outlines the implementation plan for adding missing admin methods to nsecbunkerd to achieve full compatibility with the nsecbunker-java client library. The implementation follows NIP-46 conventions and the existing nsecbunkerd command patterns.
|
||||
|
||||
---
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Implemented Methods (12 total)
|
||||
|
||||
| Method | File | Description |
|
||||
|--------|------|-------------|
|
||||
| `ping` | `commands/ping.ts` | Health check |
|
||||
| `get_keys` | `admin/index.ts` | List all keys |
|
||||
| `get_key_users` | `admin/index.ts` | Get users of a key |
|
||||
| `get_key_tokens` | `admin/index.ts` | Get tokens for a key |
|
||||
| `get_policies` | `admin/index.ts` | List all policies |
|
||||
| `create_new_key` | `commands/create_new_key.ts` | Create/import a key |
|
||||
| `unlock_key` | `commands/unlock_key.ts` | Unlock an encrypted key |
|
||||
| `create_new_policy` | `commands/create_new_policy.ts` | Create a policy |
|
||||
| `create_new_token` | `commands/create_new_token.ts` | Create an access token |
|
||||
| `rename_key_user` | `commands/rename_key_user.ts` | Update user description |
|
||||
| `revoke_user` | `commands/revoke_user.ts` | Revoke user by keyUserId |
|
||||
| `create_account` | `commands/create_account.ts` | Create user account |
|
||||
|
||||
### Missing Methods (11 total)
|
||||
|
||||
| Category | Method | Priority | Complexity |
|
||||
|----------|--------|----------|------------|
|
||||
| Key Management | `get_key` | High | Low |
|
||||
| Key Management | `delete_key` | Medium | Low |
|
||||
| Key Management | `rotate_key` | Low | Medium |
|
||||
| Policy Management | `get_policy` | High | Low |
|
||||
| Policy Management | `delete_policy` | Medium | Low |
|
||||
| Permission Management | `grant_permission` | High | Medium |
|
||||
| Permission Management | `revoke_permission` | Medium | Low |
|
||||
| Permission Management | `get_permissions` | High | Low |
|
||||
| Token Management | `get_token` | Medium | Low |
|
||||
| Token Management | `revoke_token` | Medium | Low |
|
||||
| Token Management | `validate_token` | Medium | Low |
|
||||
|
||||
---
|
||||
|
||||
## Database Schema Reference
|
||||
|
||||
```prisma
|
||||
model Key {
|
||||
id Int @id @default(autoincrement())
|
||||
keyName String @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
deletedAt DateTime?
|
||||
pubkey String
|
||||
}
|
||||
|
||||
model KeyUser {
|
||||
id Int @id @default(autoincrement())
|
||||
keyName String
|
||||
userPubkey String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
revokedAt DateTime?
|
||||
lastUsedAt DateTime?
|
||||
description String?
|
||||
signingConditions SigningCondition[]
|
||||
Token Token[]
|
||||
@@unique([keyName, userPubkey], name: "unique_key_user")
|
||||
}
|
||||
|
||||
model Policy {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
expiresAt DateTime?
|
||||
deletedAt DateTime?
|
||||
description String?
|
||||
rules PolicyRule[]
|
||||
Token Token[]
|
||||
}
|
||||
|
||||
model Token {
|
||||
id Int @id @default(autoincrement())
|
||||
keyName String
|
||||
token String @unique
|
||||
clientName String
|
||||
createdBy String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
deletedAt DateTime?
|
||||
expiresAt DateTime?
|
||||
redeemedAt DateTime?
|
||||
keyUserId Int?
|
||||
policyId Int?
|
||||
policy Policy? @relation(fields: [policyId], references: [id])
|
||||
KeyUser KeyUser? @relation(fields: [keyUserId], references: [id])
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Key Management Methods (Priority: High)
|
||||
|
||||
**Duration Estimate**: 1-2 days
|
||||
|
||||
#### Task 1.1: Implement `get_key`
|
||||
|
||||
**File**: `src/daemon/admin/commands/get_key.ts`
|
||||
|
||||
**Purpose**: Retrieve detailed information about a single key by name.
|
||||
|
||||
**Parameters**:
|
||||
- `params[0]`: keyName (string)
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"name": "my-key",
|
||||
"npub": "npub1...",
|
||||
"locked": false,
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation Steps**:
|
||||
1. Create new file `src/daemon/admin/commands/get_key.ts`
|
||||
2. Extract keyName from params
|
||||
3. Query Key table and in-memory keys state (for locked status)
|
||||
4. Return JSON response with key details
|
||||
5. Register in `admin/index.ts` switch statement
|
||||
6. Write unit tests
|
||||
|
||||
**Dependencies**: Access to `getKeys()` callback for locked status
|
||||
|
||||
---
|
||||
|
||||
#### Task 1.2: Implement `delete_key`
|
||||
|
||||
**File**: `src/daemon/admin/commands/delete_key.ts`
|
||||
|
||||
**Purpose**: Soft-delete a key (set deletedAt timestamp).
|
||||
|
||||
**Parameters**:
|
||||
- `params[0]`: keyName (string)
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
["ok"]
|
||||
```
|
||||
|
||||
**Implementation Steps**:
|
||||
1. Create new file `src/daemon/admin/commands/delete_key.ts`
|
||||
2. Extract keyName from params
|
||||
3. Update Key record with `deletedAt = new Date()`
|
||||
4. Optionally remove from in-memory keys
|
||||
5. Return `["ok"]` on success
|
||||
6. Register in `admin/index.ts`
|
||||
|
||||
**Considerations**:
|
||||
- Should we also revoke all tokens and permissions for this key?
|
||||
- Should we remove from config file or just database?
|
||||
|
||||
---
|
||||
|
||||
#### Task 1.3: Implement `rotate_key` (Lower Priority)
|
||||
|
||||
**File**: `src/daemon/admin/commands/rotate_key.ts`
|
||||
|
||||
**Purpose**: Create a new key and migrate permissions from an old key.
|
||||
|
||||
**Parameters**:
|
||||
- `params[0]`: oldKeyName (string)
|
||||
- `params[1]`: newKeyName (string)
|
||||
- `params[2]`: passphrase (string)
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"npub": "npub1...",
|
||||
"name": "new-key-name"
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation Steps**:
|
||||
1. Create new file `src/daemon/admin/commands/rotate_key.ts`
|
||||
2. Validate old key exists
|
||||
3. Generate new key with passphrase
|
||||
4. Copy KeyUser records from old key to new key
|
||||
5. Copy Token records from old key to new key
|
||||
6. Soft-delete old key
|
||||
7. Return new key details
|
||||
|
||||
**Considerations**:
|
||||
- This is a complex operation that should be transactional
|
||||
- May want to preserve old key for a grace period
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Policy Management Methods (Priority: High)
|
||||
|
||||
**Duration Estimate**: 1 day
|
||||
|
||||
#### Task 2.1: Implement `get_policy`
|
||||
|
||||
**File**: `src/daemon/admin/commands/get_policy.ts`
|
||||
|
||||
**Purpose**: Retrieve a single policy with its rules.
|
||||
|
||||
**Parameters**:
|
||||
- `params[0]`: policyId (string, will be parsed to int)
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "signing-policy",
|
||||
"description": "Policy for signing",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"rules": [
|
||||
{ "method": "sign_event", "kind": "1" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation Steps**:
|
||||
1. Create new file `src/daemon/admin/commands/get_policy.ts`
|
||||
2. Parse policyId to integer
|
||||
3. Query Policy with included rules
|
||||
4. Return JSON response or error if not found
|
||||
5. Register in `admin/index.ts`
|
||||
|
||||
---
|
||||
|
||||
#### Task 2.2: Implement `delete_policy`
|
||||
|
||||
**File**: `src/daemon/admin/commands/delete_policy.ts`
|
||||
|
||||
**Purpose**: Soft-delete a policy.
|
||||
|
||||
**Parameters**:
|
||||
- `params[0]`: policyId (string)
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
["ok"]
|
||||
```
|
||||
|
||||
**Implementation Steps**:
|
||||
1. Create new file `src/daemon/admin/commands/delete_policy.ts`
|
||||
2. Parse policyId to integer
|
||||
3. Check no active tokens reference this policy (optional)
|
||||
4. Update Policy with `deletedAt = new Date()`
|
||||
5. Return `["ok"]` on success
|
||||
|
||||
**Considerations**:
|
||||
- Should we prevent deletion if tokens are using this policy?
|
||||
- Should we cascade to PolicyRule deletion?
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Permission Management Methods (Priority: High)
|
||||
|
||||
**Duration Estimate**: 2 days
|
||||
|
||||
#### Task 3.1: Implement `grant_permission`
|
||||
|
||||
**File**: `src/daemon/admin/commands/grant_permission.ts`
|
||||
|
||||
**Purpose**: Grant a user permission to use a key with a specific policy.
|
||||
|
||||
**Parameters**:
|
||||
- `params[0]`: keyName (string)
|
||||
- `params[1]`: userPubkey (string, hex or npub)
|
||||
- `params[2]`: policyId (string)
|
||||
- `params[3]`: description (string, optional)
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"key_name": "my-key",
|
||||
"user_pubkey": "abc123...",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"description": "Client app"
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation Steps**:
|
||||
1. Create new file `src/daemon/admin/commands/grant_permission.ts`
|
||||
2. Validate key exists
|
||||
3. Validate policy exists
|
||||
4. Normalize userPubkey (convert npub to hex if needed)
|
||||
5. Create or update KeyUser record
|
||||
6. Create SigningCondition records from policy rules
|
||||
7. Return KeyUser details
|
||||
8. Register in `admin/index.ts`
|
||||
|
||||
**Key Logic**:
|
||||
```typescript
|
||||
// Convert npub to hex if needed
|
||||
if (userPubkey.startsWith('npub1')) {
|
||||
userPubkey = nip19.decode(userPubkey).data as string;
|
||||
}
|
||||
|
||||
// Upsert KeyUser
|
||||
const keyUser = await prisma.keyUser.upsert({
|
||||
where: { unique_key_user: { keyName, userPubkey } },
|
||||
update: { revokedAt: null, description },
|
||||
create: { keyName, userPubkey, description }
|
||||
});
|
||||
|
||||
// Copy policy rules to signing conditions
|
||||
const policy = await prisma.policy.findUnique({ where: { id: policyId }, include: { rules: true } });
|
||||
for (const rule of policy.rules) {
|
||||
await prisma.signingCondition.create({
|
||||
data: {
|
||||
keyUserId: keyUser.id,
|
||||
method: rule.method,
|
||||
kind: rule.kind,
|
||||
allowed: true
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Task 3.2: Implement `revoke_permission`
|
||||
|
||||
**File**: `src/daemon/admin/commands/revoke_permission.ts`
|
||||
|
||||
**Purpose**: Revoke a user's permission for a specific key.
|
||||
|
||||
**Parameters**:
|
||||
- `params[0]`: keyName (string)
|
||||
- `params[1]`: userPubkey (string)
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
["ok"]
|
||||
```
|
||||
|
||||
**Implementation Steps**:
|
||||
1. Create new file `src/daemon/admin/commands/revoke_permission.ts`
|
||||
2. Normalize userPubkey
|
||||
3. Find KeyUser by unique constraint
|
||||
4. Update revokedAt timestamp
|
||||
5. Return `["ok"]`
|
||||
|
||||
**Note**: This differs from existing `revoke_user` which takes keyUserId directly.
|
||||
|
||||
---
|
||||
|
||||
#### Task 3.3: Implement `get_permissions`
|
||||
|
||||
**File**: `src/daemon/admin/commands/get_permissions.ts`
|
||||
|
||||
**Purpose**: Get permissions for a specific user on a key.
|
||||
|
||||
**Parameters**:
|
||||
- `params[0]`: keyName (string)
|
||||
- `params[1]`: userPubkey (string)
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"key_name": "my-key",
|
||||
"user_pubkey": "abc123...",
|
||||
"active": true,
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"signing_conditions": [
|
||||
{ "method": "sign_event", "kind": "1", "allowed": true }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation Steps**:
|
||||
1. Create new file `src/daemon/admin/commands/get_permissions.ts`
|
||||
2. Normalize userPubkey
|
||||
3. Query KeyUser with signingConditions included
|
||||
4. Return user details with conditions
|
||||
5. Include `active: revokedAt === null`
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Token Management Methods (Priority: Medium)
|
||||
|
||||
**Duration Estimate**: 1-2 days
|
||||
|
||||
#### Task 4.1: Implement `get_token`
|
||||
|
||||
**File**: `src/daemon/admin/commands/get_token.ts`
|
||||
|
||||
**Purpose**: Retrieve a single token by ID.
|
||||
|
||||
**Parameters**:
|
||||
- `params[0]`: tokenId (string)
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"key_name": "my-key",
|
||||
"token": "npub1...#abc123",
|
||||
"client_name": "My App",
|
||||
"policy_id": 1,
|
||||
"expires_at": "2024-12-31T23:59:59Z",
|
||||
"redeemed_at": null
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation Steps**:
|
||||
1. Create new file `src/daemon/admin/commands/get_token.ts`
|
||||
2. Parse tokenId to integer
|
||||
3. Query Token with policy included
|
||||
4. Format token string with npub prefix
|
||||
5. Return token details or error if not found
|
||||
|
||||
---
|
||||
|
||||
#### Task 4.2: Implement `revoke_token`
|
||||
|
||||
**File**: `src/daemon/admin/commands/revoke_token.ts`
|
||||
|
||||
**Purpose**: Revoke (soft-delete) a token.
|
||||
|
||||
**Parameters**:
|
||||
- `params[0]`: tokenId (string)
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
["ok"]
|
||||
```
|
||||
|
||||
**Implementation Steps**:
|
||||
1. Create new file `src/daemon/admin/commands/revoke_token.ts`
|
||||
2. Parse tokenId to integer
|
||||
3. Update Token with `deletedAt = new Date()`
|
||||
4. Return `["ok"]`
|
||||
|
||||
---
|
||||
|
||||
#### Task 4.3: Implement `validate_token`
|
||||
|
||||
**File**: `src/daemon/admin/commands/validate_token.ts`
|
||||
|
||||
**Purpose**: Check if a token is valid (not expired, not revoked).
|
||||
|
||||
**Parameters**:
|
||||
- `params[0]`: tokenString (string) - the token value (without npub prefix)
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"key_name": "my-key",
|
||||
"expires_at": "2024-12-31T23:59:59Z"
|
||||
}
|
||||
```
|
||||
or
|
||||
```json
|
||||
{
|
||||
"valid": false,
|
||||
"reason": "Token expired"
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation Steps**:
|
||||
1. Create new file `src/daemon/admin/commands/validate_token.ts`
|
||||
2. Parse token string (may include npub# prefix)
|
||||
3. Query Token by token value
|
||||
4. Check: `deletedAt === null && (expiresAt === null || expiresAt > now)`
|
||||
5. Return validity status with details
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Integration and Testing
|
||||
|
||||
**Duration Estimate**: 2-3 days
|
||||
|
||||
#### Task 5.1: Register All New Commands
|
||||
|
||||
**File**: `src/daemon/admin/index.ts`
|
||||
|
||||
Update the handleRequest switch statement:
|
||||
|
||||
```typescript
|
||||
switch (req.method) {
|
||||
// Existing commands...
|
||||
|
||||
// New Key commands
|
||||
case 'get_key': await getKey(this, req); break;
|
||||
case 'delete_key': await deleteKey(this, req); break;
|
||||
case 'rotate_key': await rotateKey(this, req); break;
|
||||
|
||||
// New Policy commands
|
||||
case 'get_policy': await getPolicy(this, req); break;
|
||||
case 'delete_policy': await deletePolicy(this, req); break;
|
||||
|
||||
// New Permission commands
|
||||
case 'grant_permission': await grantPermission(this, req); break;
|
||||
case 'revoke_permission': await revokePermission(this, req); break;
|
||||
case 'get_permissions': await getPermissions(this, req); break;
|
||||
|
||||
// New Token commands
|
||||
case 'get_token': await getToken(this, req); break;
|
||||
case 'revoke_token': await revokeToken(this, req); break;
|
||||
case 'validate_token': await validateToken(this, req); break;
|
||||
|
||||
default: // ...
|
||||
}
|
||||
```
|
||||
|
||||
#### Task 5.2: Add Imports
|
||||
|
||||
Add imports for all new command modules at the top of `admin/index.ts`.
|
||||
|
||||
#### Task 5.3: Enable E2E Tests in nsecbunker-java
|
||||
|
||||
Remove `@Disabled` annotations from E2E tests:
|
||||
- `SigningFlowE2ETest.shouldSetUpKeyForSigning`
|
||||
- `SigningFlowE2ETest.shouldGrantSigningPermission`
|
||||
- `SigningFlowE2ETest.shouldGenerateSigningToken`
|
||||
- `SigningFlowE2ETest.shouldCompleteFullSigningSetupFlow`
|
||||
- `SigningFlowE2ETest.shouldImportExistingKeyForSigning`
|
||||
|
||||
#### Task 5.4: Run Full E2E Test Suite
|
||||
|
||||
```bash
|
||||
cd /home/eric/IdeaProjects/nsecbunker-java
|
||||
mvn test -pl nsecbunker-e2e -am -Pe2e
|
||||
```
|
||||
|
||||
#### Task 5.5: Build and Test Docker Image
|
||||
|
||||
```bash
|
||||
cd /home/eric/IdeaProjects/nsecbunkerd
|
||||
npm run build
|
||||
docker build -t nsecbunkerd-local:latest .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Structure After Implementation
|
||||
|
||||
```
|
||||
src/daemon/admin/
|
||||
├── index.ts # Main router (updated)
|
||||
├── validations/
|
||||
│ └── request-from-admin.ts
|
||||
└── commands/
|
||||
├── ping.ts
|
||||
├── create_new_key.ts
|
||||
├── get_key.ts # NEW
|
||||
├── delete_key.ts # NEW
|
||||
├── rotate_key.ts # NEW
|
||||
├── unlock_key.ts
|
||||
├── create_new_policy.ts
|
||||
├── get_policy.ts # NEW
|
||||
├── delete_policy.ts # NEW
|
||||
├── create_new_token.ts
|
||||
├── get_token.ts # NEW
|
||||
├── revoke_token.ts # NEW
|
||||
├── validate_token.ts # NEW
|
||||
├── grant_permission.ts # NEW
|
||||
├── revoke_permission.ts # NEW
|
||||
├── get_permissions.ts # NEW
|
||||
├── rename_key_user.ts
|
||||
├── revoke_user.ts
|
||||
├── create_account.ts
|
||||
└── account/
|
||||
└── wallet.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
|------|--------|------------|
|
||||
| Breaking existing behavior | High | Preserve existing response formats |
|
||||
| Database migrations needed | Medium | Use soft deletes, no schema changes required |
|
||||
| Config file modifications | Medium | Only delete_key may need to update config |
|
||||
| Security concerns | High | Validate all admin requests are from authorized npubs |
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- Each command should have corresponding unit tests
|
||||
- Mock Prisma client for database operations
|
||||
- Test error cases (invalid params, not found, etc.)
|
||||
|
||||
### Integration Tests (E2E)
|
||||
- Use nsecbunker-java E2E test suite
|
||||
- Run against local Docker container
|
||||
- Verify round-trip: Java client → nsecbunkerd → response parsing
|
||||
|
||||
### Manual Testing
|
||||
- Use nsecBunker admin UI if available
|
||||
- Test with nostr client tools
|
||||
|
||||
---
|
||||
|
||||
## Rollout Plan
|
||||
|
||||
1. **Development**: Implement in feature branch
|
||||
2. **Local Testing**: Run E2E tests with local build
|
||||
3. **Code Review**: Review for security and correctness
|
||||
4. **Docker Build**: Create new Docker image
|
||||
5. **Staging**: Test with staging nsecbunker instance
|
||||
6. **Release**: Tag version (0.11.0 - MINOR bump for new features)
|
||||
7. **Documentation**: Update README with new admin methods
|
||||
|
||||
---
|
||||
|
||||
## Version Recommendation
|
||||
|
||||
Since this adds new features without breaking existing behavior:
|
||||
- **Current**: 0.10.6
|
||||
- **Proposed**: 0.11.0 (MINOR version bump)
|
||||
|
||||
---
|
||||
|
||||
## Appendix: NIP-46 Reference
|
||||
|
||||
### Request Format (kind: 24133)
|
||||
```json
|
||||
{
|
||||
"id": "<random_string>",
|
||||
"method": "<method_name>",
|
||||
"params": ["<array_of_strings>"]
|
||||
}
|
||||
```
|
||||
|
||||
### Response Format
|
||||
```json
|
||||
{
|
||||
"id": "<request_id>",
|
||||
"result": "<json_stringified_result>",
|
||||
"error": "<optional_error_string>"
|
||||
}
|
||||
```
|
||||
|
||||
### Standard Methods (NIP-46 Core)
|
||||
- `connect`, `sign_event`, `ping`, `get_public_key`
|
||||
- `nip04_encrypt`, `nip04_decrypt`, `nip44_encrypt`, `nip44_decrypt`
|
||||
|
||||
### Admin Methods (nsecbunkerd Extension)
|
||||
- Key: `get_keys`, `get_key`, `create_new_key`, `delete_key`, `unlock_key`, `rotate_key`
|
||||
- Policy: `get_policies`, `get_policy`, `create_new_policy`, `delete_policy`
|
||||
- Permission: `grant_permission`, `revoke_permission`, `get_permissions`, `get_key_users`, `rename_key_user`, `revoke_user`
|
||||
- Token: `get_key_tokens`, `get_token`, `create_new_token`, `revoke_token`, `validate_token`
|
||||
@ -100,7 +100,7 @@ function loadPrivateKey(): string | undefined {
|
||||
} else {
|
||||
// check if we have a @ so we try to get the npub from nip05
|
||||
if (remotePubkey.includes('@')) {
|
||||
const u = await NDKUser.fromNip05(remotePubkey);
|
||||
const u = await NDKUser.fromNip05(remotePubkey, ndk);
|
||||
if (!u) {
|
||||
console.log(`Invalid nip05 ${remotePubkey}`);
|
||||
process.exit(1);
|
||||
|
||||
79
src/daemon/admin/commands/__tests__/create_new_key.test.ts
Normal file
79
src/daemon/admin/commands/__tests__/create_new_key.test.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createMockAdmin, createMockRequest, getResponseResult, resetMocks } from './test-utils';
|
||||
|
||||
// Mock the saveEncrypted function
|
||||
vi.mock('../../../../commands/add.js', () => ({
|
||||
saveEncrypted: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
// Mock the setupSkeletonProfile function
|
||||
vi.mock('../../../lib/profile.js', () => ({
|
||||
setupSkeletonProfile: vi.fn(),
|
||||
}));
|
||||
|
||||
import createNewKey from '../create_new_key';
|
||||
|
||||
describe('create_new_key', () => {
|
||||
beforeEach(() => {
|
||||
resetMocks();
|
||||
});
|
||||
|
||||
it('should throw error when keyName is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest([]);
|
||||
|
||||
await expect(createNewKey(admin as any, req)).rejects.toThrow('Invalid params');
|
||||
});
|
||||
|
||||
it('should throw error when passphrase is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key']);
|
||||
|
||||
await expect(createNewKey(admin as any, req)).rejects.toThrow('Invalid params');
|
||||
});
|
||||
|
||||
it('should throw error when loadNsec is not implemented', async () => {
|
||||
const admin = createMockAdmin({ loadNsec: undefined });
|
||||
const req = createMockRequest(['my-key', 'passphrase']);
|
||||
|
||||
await expect(createNewKey(admin as any, req)).rejects.toThrow('No unlockKey method');
|
||||
});
|
||||
|
||||
it('should generate a new key when no nsec provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key', 'passphrase']);
|
||||
|
||||
await createNewKey(admin as any, req);
|
||||
|
||||
// Verify loadNsec was called with the key name and an nsec
|
||||
expect(admin.loadNsec).toHaveBeenCalledWith(
|
||||
'my-key',
|
||||
expect.stringMatching(/^nsec1/)
|
||||
);
|
||||
|
||||
// Verify response contains npub
|
||||
expect(admin.rpc.sendResponse).toHaveBeenCalledTimes(1);
|
||||
const result = getResponseResult(admin);
|
||||
expect(result.npub).toMatch(/^npub1/);
|
||||
});
|
||||
|
||||
it('should import existing key when nsec provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
// Valid nsec for testing (generates a known pubkey)
|
||||
const testNsec = 'nsec1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqstywftw';
|
||||
const req = createMockRequest(['my-key', 'passphrase', testNsec]);
|
||||
|
||||
await createNewKey(admin as any, req);
|
||||
|
||||
// Verify loadNsec was called
|
||||
expect(admin.loadNsec).toHaveBeenCalledWith(
|
||||
'my-key',
|
||||
expect.stringMatching(/^nsec1/)
|
||||
);
|
||||
|
||||
// Verify response
|
||||
expect(admin.rpc.sendResponse).toHaveBeenCalledTimes(1);
|
||||
const result = getResponseResult(admin);
|
||||
expect(result.npub).toMatch(/^npub1/);
|
||||
});
|
||||
});
|
||||
124
src/daemon/admin/commands/__tests__/create_new_policy.test.ts
Normal file
124
src/daemon/admin/commands/__tests__/create_new_policy.test.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createMockAdmin, createMockRequest, getResponseResult, resetMocks, mockPrisma } from './test-utils';
|
||||
|
||||
// Mock the prisma module
|
||||
vi.mock('../../../../db.js', () => ({
|
||||
default: mockPrisma,
|
||||
}));
|
||||
|
||||
import createNewPolicy from '../create_new_policy';
|
||||
|
||||
describe('create_new_policy', () => {
|
||||
beforeEach(() => {
|
||||
resetMocks();
|
||||
});
|
||||
|
||||
it('should throw error when policy param is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest([]);
|
||||
|
||||
await expect(createNewPolicy(admin as any, req)).rejects.toThrow('Invalid params');
|
||||
});
|
||||
|
||||
it('should throw error when policy is invalid JSON', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['not valid json']);
|
||||
|
||||
await expect(createNewPolicy(admin as any, req)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should create policy without rules', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const policy = {
|
||||
name: 'test-policy',
|
||||
rules: [],
|
||||
};
|
||||
const req = createMockRequest([JSON.stringify(policy)]);
|
||||
|
||||
mockPrisma.policy.create.mockResolvedValue({
|
||||
id: 1,
|
||||
name: 'test-policy',
|
||||
});
|
||||
|
||||
await createNewPolicy(admin as any, req);
|
||||
|
||||
expect(mockPrisma.policy.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
name: 'test-policy',
|
||||
expiresAt: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
expect(admin.rpc.sendResponse).toHaveBeenCalledTimes(1);
|
||||
const result = getResponseResult(admin);
|
||||
expect(result).toEqual(['ok']);
|
||||
});
|
||||
|
||||
it('should create policy with rules', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const policy = {
|
||||
name: 'signing-policy',
|
||||
expires_at: '2024-12-31T23:59:59Z',
|
||||
rules: [
|
||||
{ method: 'sign_event', kind: 1, use_count: 100 },
|
||||
{ method: 'sign_event', kind: 7 },
|
||||
],
|
||||
};
|
||||
const req = createMockRequest([JSON.stringify(policy)]);
|
||||
|
||||
mockPrisma.policy.create.mockResolvedValue({
|
||||
id: 1,
|
||||
name: 'signing-policy',
|
||||
});
|
||||
mockPrisma.policyRule.create.mockResolvedValue({});
|
||||
|
||||
await createNewPolicy(admin as any, req);
|
||||
|
||||
expect(mockPrisma.policy.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
name: 'signing-policy',
|
||||
expiresAt: '2024-12-31T23:59:59Z',
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockPrisma.policyRule.create).toHaveBeenCalledTimes(2);
|
||||
expect(mockPrisma.policyRule.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
Policy: { connect: { id: 1 } },
|
||||
kind: '1',
|
||||
method: 'sign_event',
|
||||
maxUsageCount: 100,
|
||||
currentUsageCount: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const result = getResponseResult(admin);
|
||||
expect(result).toEqual(['ok']);
|
||||
});
|
||||
|
||||
it('should use default method sign_event when not specified', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const policy = {
|
||||
name: 'default-method-policy',
|
||||
rules: [
|
||||
{ kind: 1 }, // No method specified
|
||||
],
|
||||
};
|
||||
const req = createMockRequest([JSON.stringify(policy)]);
|
||||
|
||||
mockPrisma.policy.create.mockResolvedValue({ id: 1 });
|
||||
mockPrisma.policyRule.create.mockResolvedValue({});
|
||||
|
||||
await createNewPolicy(admin as any, req);
|
||||
|
||||
expect(mockPrisma.policyRule.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
Policy: { connect: { id: 1 } },
|
||||
kind: '1',
|
||||
method: 'sign_event',
|
||||
maxUsageCount: undefined,
|
||||
currentUsageCount: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
128
src/daemon/admin/commands/__tests__/create_new_token.test.ts
Normal file
128
src/daemon/admin/commands/__tests__/create_new_token.test.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createMockAdmin, createMockRequest, getResponseResult, resetMocks, mockPrisma } from './test-utils';
|
||||
|
||||
// Mock the prisma module
|
||||
vi.mock('../../../../db.js', () => ({
|
||||
default: mockPrisma,
|
||||
}));
|
||||
|
||||
import createNewToken from '../create_new_token';
|
||||
|
||||
describe('create_new_token', () => {
|
||||
beforeEach(() => {
|
||||
resetMocks();
|
||||
});
|
||||
|
||||
it('should throw error when clientName is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key']);
|
||||
|
||||
await expect(createNewToken(admin as any, req)).rejects.toThrow('Invalid params');
|
||||
});
|
||||
|
||||
it('should throw error when policyId is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key', 'Test App']);
|
||||
|
||||
await expect(createNewToken(admin as any, req)).rejects.toThrow('Invalid params');
|
||||
});
|
||||
|
||||
it('should throw error when policy is not found', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key', 'Test App', '999']);
|
||||
|
||||
mockPrisma.policy.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(createNewToken(admin as any, req)).rejects.toThrow('Policy not found');
|
||||
});
|
||||
|
||||
it('should create token without expiration', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key', 'Test App', '1']);
|
||||
|
||||
mockPrisma.policy.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
name: 'test-policy',
|
||||
rules: [],
|
||||
});
|
||||
|
||||
mockPrisma.token.create.mockResolvedValue({
|
||||
id: 1,
|
||||
token: 'generated-token',
|
||||
});
|
||||
|
||||
await createNewToken(admin as any, req);
|
||||
|
||||
expect(mockPrisma.token.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
keyName: 'my-key',
|
||||
clientName: 'Test App',
|
||||
policyId: '1',
|
||||
createdBy: 'test-pubkey-hex',
|
||||
token: expect.any(String),
|
||||
},
|
||||
});
|
||||
|
||||
expect(admin.rpc.sendResponse).toHaveBeenCalledTimes(1);
|
||||
const result = getResponseResult(admin);
|
||||
expect(result).toEqual(['ok']);
|
||||
});
|
||||
|
||||
it('should create token with expiration', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key', 'Test App', '1', '24']); // 24 hours
|
||||
|
||||
mockPrisma.policy.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
name: 'test-policy',
|
||||
rules: [],
|
||||
});
|
||||
|
||||
mockPrisma.token.create.mockResolvedValue({
|
||||
id: 1,
|
||||
token: 'generated-token',
|
||||
});
|
||||
|
||||
await createNewToken(admin as any, req);
|
||||
|
||||
expect(mockPrisma.token.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
keyName: 'my-key',
|
||||
clientName: 'Test App',
|
||||
policyId: '1',
|
||||
createdBy: 'test-pubkey-hex',
|
||||
token: expect.any(String),
|
||||
expiresAt: expect.any(Date),
|
||||
},
|
||||
});
|
||||
|
||||
// Verify expiration is approximately 24 hours from now
|
||||
const createCall = mockPrisma.token.create.mock.calls[0][0];
|
||||
const expiresAt = createCall.data.expiresAt as Date;
|
||||
const expectedExpiry = Date.now() + (24 * 60 * 60 * 1000);
|
||||
expect(expiresAt.getTime()).toBeCloseTo(expectedExpiry, -4); // Within 10 seconds
|
||||
});
|
||||
|
||||
it('should generate 64-character hex token', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key', 'Test App', '1']);
|
||||
|
||||
mockPrisma.policy.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
name: 'test-policy',
|
||||
rules: [],
|
||||
});
|
||||
|
||||
mockPrisma.token.create.mockResolvedValue({
|
||||
id: 1,
|
||||
token: 'generated-token',
|
||||
});
|
||||
|
||||
await createNewToken(admin as any, req);
|
||||
|
||||
const createCall = mockPrisma.token.create.mock.calls[0][0];
|
||||
const token = createCall.data.token as string;
|
||||
expect(token).toHaveLength(64);
|
||||
expect(token).toMatch(/^[0-9a-f]+$/);
|
||||
});
|
||||
});
|
||||
74
src/daemon/admin/commands/__tests__/delete_key.test.ts
Normal file
74
src/daemon/admin/commands/__tests__/delete_key.test.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createMockAdmin, createMockRequest, getResponseResult, resetMocks, mockPrisma } from './test-utils';
|
||||
|
||||
// Mock the prisma module
|
||||
vi.mock('../../../../db.js', () => ({
|
||||
default: mockPrisma,
|
||||
}));
|
||||
|
||||
import deleteKey from '../delete_key';
|
||||
|
||||
describe('delete_key', () => {
|
||||
beforeEach(() => {
|
||||
resetMocks();
|
||||
});
|
||||
|
||||
it('should throw error when keyName is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest([]);
|
||||
|
||||
await expect(deleteKey(admin as any, req)).rejects.toThrow('Invalid params: keyName required');
|
||||
});
|
||||
|
||||
it('should throw error when key is not found', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key']);
|
||||
|
||||
mockPrisma.key.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(deleteKey(admin as any, req)).rejects.toThrow("Key 'my-key' not found");
|
||||
});
|
||||
|
||||
it('should throw error when key is already deleted', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key']);
|
||||
|
||||
mockPrisma.key.findUnique.mockResolvedValue({
|
||||
keyName: 'my-key',
|
||||
deletedAt: new Date('2024-01-01'),
|
||||
});
|
||||
|
||||
await expect(deleteKey(admin as any, req)).rejects.toThrow("Key 'my-key' is already deleted");
|
||||
});
|
||||
|
||||
it('should soft-delete key and its tokens', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key']);
|
||||
|
||||
mockPrisma.key.findUnique.mockResolvedValue({
|
||||
keyName: 'my-key',
|
||||
deletedAt: null,
|
||||
});
|
||||
mockPrisma.key.update.mockResolvedValue({});
|
||||
mockPrisma.token.updateMany.mockResolvedValue({ count: 2 });
|
||||
|
||||
await deleteKey(admin as any, req);
|
||||
|
||||
// Verify key was soft-deleted
|
||||
expect(mockPrisma.key.update).toHaveBeenCalledWith({
|
||||
where: { keyName: 'my-key' },
|
||||
data: { deletedAt: expect.any(Date) },
|
||||
});
|
||||
|
||||
// Verify tokens were soft-deleted
|
||||
expect(mockPrisma.token.updateMany).toHaveBeenCalledWith({
|
||||
where: { keyName: 'my-key', deletedAt: null },
|
||||
data: { deletedAt: expect.any(Date) },
|
||||
});
|
||||
|
||||
// Verify response
|
||||
expect(admin.rpc.sendResponse).toHaveBeenCalledTimes(1);
|
||||
const result = getResponseResult(admin);
|
||||
expect(result).toEqual(['ok']);
|
||||
});
|
||||
});
|
||||
76
src/daemon/admin/commands/__tests__/delete_policy.test.ts
Normal file
76
src/daemon/admin/commands/__tests__/delete_policy.test.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createMockAdmin, createMockRequest, getResponseResult, resetMocks, mockPrisma } from './test-utils';
|
||||
|
||||
// Mock the prisma module
|
||||
vi.mock('../../../../db.js', () => ({
|
||||
default: mockPrisma,
|
||||
}));
|
||||
|
||||
import deletePolicy from '../delete_policy';
|
||||
|
||||
describe('delete_policy', () => {
|
||||
beforeEach(() => {
|
||||
resetMocks();
|
||||
});
|
||||
|
||||
it('should throw error when policyId is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest([]);
|
||||
|
||||
await expect(deletePolicy(admin as any, req)).rejects.toThrow('Invalid params: policyId required');
|
||||
});
|
||||
|
||||
it('should throw error when policyId is not a number', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['invalid']);
|
||||
|
||||
await expect(deletePolicy(admin as any, req)).rejects.toThrow('Invalid params: policyId must be a number');
|
||||
});
|
||||
|
||||
it('should throw error when policy is not found', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['1']);
|
||||
|
||||
mockPrisma.policy.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(deletePolicy(admin as any, req)).rejects.toThrow("Policy with id '1' not found");
|
||||
});
|
||||
|
||||
it('should throw error when policy is already deleted', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['1']);
|
||||
|
||||
mockPrisma.policy.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
name: 'test-policy',
|
||||
deletedAt: new Date('2024-01-01'),
|
||||
});
|
||||
|
||||
await expect(deletePolicy(admin as any, req)).rejects.toThrow("Policy with id '1' is already deleted");
|
||||
});
|
||||
|
||||
it('should soft-delete policy successfully', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['1']);
|
||||
|
||||
mockPrisma.policy.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
name: 'test-policy',
|
||||
deletedAt: null,
|
||||
});
|
||||
mockPrisma.policy.update.mockResolvedValue({});
|
||||
|
||||
await deletePolicy(admin as any, req);
|
||||
|
||||
// Verify policy was soft-deleted
|
||||
expect(mockPrisma.policy.update).toHaveBeenCalledWith({
|
||||
where: { id: 1 },
|
||||
data: { deletedAt: expect.any(Date) },
|
||||
});
|
||||
|
||||
// Verify response
|
||||
expect(admin.rpc.sendResponse).toHaveBeenCalledTimes(1);
|
||||
const result = getResponseResult(admin);
|
||||
expect(result).toEqual(['ok']);
|
||||
});
|
||||
});
|
||||
85
src/daemon/admin/commands/__tests__/get_key.test.ts
Normal file
85
src/daemon/admin/commands/__tests__/get_key.test.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createMockAdmin, createMockRequest, getResponseResult, resetMocks, mockPrisma } from './test-utils';
|
||||
|
||||
// Mock the prisma module
|
||||
vi.mock('../../../../db.js', () => ({
|
||||
default: mockPrisma,
|
||||
}));
|
||||
|
||||
import getKey from '../get_key';
|
||||
|
||||
describe('get_key', () => {
|
||||
beforeEach(() => {
|
||||
resetMocks();
|
||||
});
|
||||
|
||||
it('should throw error when keyName is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest([]);
|
||||
|
||||
await expect(getKey(admin as any, req)).rejects.toThrow('Invalid params: keyName required');
|
||||
});
|
||||
|
||||
it('should throw error when getKeys is not implemented', async () => {
|
||||
const admin = createMockAdmin({ getKeys: undefined });
|
||||
const req = createMockRequest(['my-key']);
|
||||
|
||||
await expect(getKey(admin as any, req)).rejects.toThrow('getKeys() not implemented');
|
||||
});
|
||||
|
||||
it('should throw error when key is not found', async () => {
|
||||
const admin = createMockAdmin({
|
||||
getKeys: vi.fn().mockResolvedValue([
|
||||
{ name: 'other-key', npub: 'npub1abc' },
|
||||
]),
|
||||
});
|
||||
const req = createMockRequest(['my-key']);
|
||||
|
||||
await expect(getKey(admin as any, req)).rejects.toThrow("Key 'my-key' not found");
|
||||
});
|
||||
|
||||
it('should return key details for an unlocked key', async () => {
|
||||
const admin = createMockAdmin({
|
||||
getKeys: vi.fn().mockResolvedValue([
|
||||
{ name: 'my-key', npub: 'npub1xyz123' },
|
||||
]),
|
||||
});
|
||||
const req = createMockRequest(['my-key']);
|
||||
|
||||
mockPrisma.key.findUnique.mockResolvedValue({
|
||||
keyName: 'my-key',
|
||||
createdAt: new Date('2024-01-01'),
|
||||
updatedAt: new Date('2024-01-02'),
|
||||
});
|
||||
|
||||
await getKey(admin as any, req);
|
||||
|
||||
expect(admin.rpc.sendResponse).toHaveBeenCalledTimes(1);
|
||||
const result = getResponseResult(admin);
|
||||
expect(result.name).toBe('my-key');
|
||||
expect(result.npub).toBe('npub1xyz123');
|
||||
expect(result.locked).toBe(false);
|
||||
});
|
||||
|
||||
it('should return locked status for a locked key', async () => {
|
||||
const admin = createMockAdmin({
|
||||
getKeys: vi.fn().mockResolvedValue([
|
||||
{ name: 'locked-key', npub: undefined }, // No npub means locked
|
||||
]),
|
||||
});
|
||||
const req = createMockRequest(['locked-key']);
|
||||
|
||||
mockPrisma.key.findUnique.mockResolvedValue({
|
||||
keyName: 'locked-key',
|
||||
createdAt: new Date('2024-01-01'),
|
||||
updatedAt: new Date('2024-01-02'),
|
||||
});
|
||||
|
||||
await getKey(admin as any, req);
|
||||
|
||||
const result = getResponseResult(admin);
|
||||
expect(result.name).toBe('locked-key');
|
||||
expect(result.npub).toBeNull();
|
||||
expect(result.locked).toBe(true);
|
||||
});
|
||||
});
|
||||
93
src/daemon/admin/commands/__tests__/get_permissions.test.ts
Normal file
93
src/daemon/admin/commands/__tests__/get_permissions.test.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createMockAdmin, createMockRequest, getResponseResult, resetMocks, mockPrisma } from './test-utils';
|
||||
|
||||
// Mock the prisma module
|
||||
vi.mock('../../../../db.js', () => ({
|
||||
default: mockPrisma,
|
||||
}));
|
||||
|
||||
import getPermissions from '../get_permissions';
|
||||
|
||||
describe('get_permissions', () => {
|
||||
beforeEach(() => {
|
||||
resetMocks();
|
||||
});
|
||||
|
||||
it('should throw error when keyName is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest([]);
|
||||
|
||||
await expect(getPermissions(admin as any, req)).rejects.toThrow('Invalid params: keyName and userPubkey required');
|
||||
});
|
||||
|
||||
it('should throw error when userPubkey is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key']);
|
||||
|
||||
await expect(getPermissions(admin as any, req)).rejects.toThrow('Invalid params: keyName and userPubkey required');
|
||||
});
|
||||
|
||||
it('should throw error when permission is not found', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key', 'pubkey123']);
|
||||
|
||||
mockPrisma.keyUser.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(getPermissions(admin as any, req)).rejects.toThrow("Permission not found for user on key 'my-key'");
|
||||
});
|
||||
|
||||
it('should return permissions for active user', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key', 'pubkey123']);
|
||||
|
||||
mockPrisma.keyUser.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
keyName: 'my-key',
|
||||
userPubkey: 'pubkey123',
|
||||
createdAt: new Date('2024-01-01'),
|
||||
updatedAt: new Date('2024-01-02'),
|
||||
revokedAt: null,
|
||||
lastUsedAt: new Date('2024-01-03'),
|
||||
description: 'Test User',
|
||||
signingConditions: [
|
||||
{ id: 1, method: 'sign_event', kind: '1', content: null, allowed: true },
|
||||
{ id: 2, method: 'sign_event', kind: '7', content: null, allowed: true },
|
||||
],
|
||||
});
|
||||
|
||||
await getPermissions(admin as any, req);
|
||||
|
||||
expect(admin.rpc.sendResponse).toHaveBeenCalledTimes(1);
|
||||
const result = getResponseResult(admin);
|
||||
expect(result.id).toBe(1);
|
||||
expect(result.key_name).toBe('my-key');
|
||||
expect(result.user_pubkey).toBe('pubkey123');
|
||||
expect(result.active).toBe(true);
|
||||
expect(result.description).toBe('Test User');
|
||||
expect(result.signing_conditions).toHaveLength(2);
|
||||
expect(result.signing_conditions[0].method).toBe('sign_event');
|
||||
});
|
||||
|
||||
it('should return inactive status for revoked user', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key', 'pubkey123']);
|
||||
|
||||
mockPrisma.keyUser.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
keyName: 'my-key',
|
||||
userPubkey: 'pubkey123',
|
||||
createdAt: new Date('2024-01-01'),
|
||||
updatedAt: new Date('2024-01-02'),
|
||||
revokedAt: new Date('2024-01-05'),
|
||||
lastUsedAt: new Date('2024-01-03'),
|
||||
description: 'Revoked User',
|
||||
signingConditions: [],
|
||||
});
|
||||
|
||||
await getPermissions(admin as any, req);
|
||||
|
||||
const result = getResponseResult(admin);
|
||||
expect(result.active).toBe(false);
|
||||
expect(result.revoked_at).toBeDefined();
|
||||
});
|
||||
});
|
||||
82
src/daemon/admin/commands/__tests__/get_policy.test.ts
Normal file
82
src/daemon/admin/commands/__tests__/get_policy.test.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createMockAdmin, createMockRequest, getResponseResult, resetMocks, mockPrisma } from './test-utils';
|
||||
|
||||
// Mock the prisma module
|
||||
vi.mock('../../../../db.js', () => ({
|
||||
default: mockPrisma,
|
||||
}));
|
||||
|
||||
import getPolicy from '../get_policy';
|
||||
|
||||
describe('get_policy', () => {
|
||||
beforeEach(() => {
|
||||
resetMocks();
|
||||
});
|
||||
|
||||
it('should throw error when policyId is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest([]);
|
||||
|
||||
await expect(getPolicy(admin as any, req)).rejects.toThrow('Invalid params: policyId required');
|
||||
});
|
||||
|
||||
it('should throw error when policyId is not a number', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['not-a-number']);
|
||||
|
||||
await expect(getPolicy(admin as any, req)).rejects.toThrow('Invalid params: policyId must be a number');
|
||||
});
|
||||
|
||||
it('should throw error when policy is not found', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['1']);
|
||||
|
||||
mockPrisma.policy.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(getPolicy(admin as any, req)).rejects.toThrow("Policy with id '1' not found");
|
||||
});
|
||||
|
||||
it('should throw error when policy is deleted', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['1']);
|
||||
|
||||
mockPrisma.policy.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
name: 'test-policy',
|
||||
deletedAt: new Date('2024-01-01'),
|
||||
rules: [],
|
||||
});
|
||||
|
||||
await expect(getPolicy(admin as any, req)).rejects.toThrow("Policy with id '1' has been deleted");
|
||||
});
|
||||
|
||||
it('should return policy details with rules', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['1']);
|
||||
|
||||
mockPrisma.policy.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
name: 'signing-policy',
|
||||
description: 'A test policy',
|
||||
createdAt: new Date('2024-01-01'),
|
||||
updatedAt: new Date('2024-01-02'),
|
||||
expiresAt: null,
|
||||
deletedAt: null,
|
||||
rules: [
|
||||
{ id: 1, method: 'sign_event', kind: '1', maxUsageCount: 100, currentUsageCount: 5 },
|
||||
{ id: 2, method: 'sign_event', kind: '7', maxUsageCount: null, currentUsageCount: 0 },
|
||||
],
|
||||
});
|
||||
|
||||
await getPolicy(admin as any, req);
|
||||
|
||||
expect(admin.rpc.sendResponse).toHaveBeenCalledTimes(1);
|
||||
const result = getResponseResult(admin);
|
||||
expect(result.id).toBe(1);
|
||||
expect(result.name).toBe('signing-policy');
|
||||
expect(result.description).toBe('A test policy');
|
||||
expect(result.rules).toHaveLength(2);
|
||||
expect(result.rules[0].method).toBe('sign_event');
|
||||
expect(result.rules[0].kind).toBe('1');
|
||||
});
|
||||
});
|
||||
132
src/daemon/admin/commands/__tests__/get_token.test.ts
Normal file
132
src/daemon/admin/commands/__tests__/get_token.test.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createMockAdmin, createMockRequest, getResponseResult, resetMocks, mockPrisma } from './test-utils';
|
||||
|
||||
// Mock the prisma module
|
||||
vi.mock('../../../../db.js', () => ({
|
||||
default: mockPrisma,
|
||||
}));
|
||||
|
||||
import getToken from '../get_token';
|
||||
|
||||
describe('get_token', () => {
|
||||
beforeEach(() => {
|
||||
resetMocks();
|
||||
});
|
||||
|
||||
it('should throw error when tokenId is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest([]);
|
||||
|
||||
await expect(getToken(admin as any, req)).rejects.toThrow('Invalid params: tokenId required');
|
||||
});
|
||||
|
||||
it('should throw error when tokenId is not a number', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['invalid']);
|
||||
|
||||
await expect(getToken(admin as any, req)).rejects.toThrow('Invalid params: tokenId must be a number');
|
||||
});
|
||||
|
||||
it('should throw error when token is not found', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['1']);
|
||||
|
||||
mockPrisma.token.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(getToken(admin as any, req)).rejects.toThrow("Token with id '1' not found");
|
||||
});
|
||||
|
||||
it('should return token details with npub prefix', async () => {
|
||||
const admin = createMockAdmin({
|
||||
getKeys: vi.fn().mockResolvedValue([
|
||||
{ name: 'my-key', npub: 'npub1xyz123' },
|
||||
]),
|
||||
});
|
||||
const req = createMockRequest(['1']);
|
||||
|
||||
mockPrisma.token.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
keyName: 'my-key',
|
||||
token: 'abc123token',
|
||||
clientName: 'Test App',
|
||||
createdBy: 'admin-pubkey',
|
||||
policyId: 1,
|
||||
createdAt: new Date('2024-01-01'),
|
||||
updatedAt: new Date('2024-01-02'),
|
||||
expiresAt: new Date('2024-12-31'),
|
||||
deletedAt: null,
|
||||
redeemedAt: null,
|
||||
policy: { name: 'signing-policy' },
|
||||
KeyUser: null,
|
||||
});
|
||||
|
||||
await getToken(admin as any, req);
|
||||
|
||||
expect(admin.rpc.sendResponse).toHaveBeenCalledTimes(1);
|
||||
const result = getResponseResult(admin);
|
||||
expect(result.id).toBe(1);
|
||||
expect(result.key_name).toBe('my-key');
|
||||
expect(result.token).toBe('npub1xyz123#abc123token');
|
||||
expect(result.client_name).toBe('Test App');
|
||||
expect(result.policy_name).toBe('signing-policy');
|
||||
});
|
||||
|
||||
it('should return token without npub prefix if key not found', async () => {
|
||||
const admin = createMockAdmin({
|
||||
getKeys: vi.fn().mockResolvedValue([]),
|
||||
});
|
||||
const req = createMockRequest(['1']);
|
||||
|
||||
mockPrisma.token.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
keyName: 'unknown-key',
|
||||
token: 'abc123token',
|
||||
clientName: 'Test App',
|
||||
createdBy: 'admin-pubkey',
|
||||
policyId: null,
|
||||
createdAt: new Date('2024-01-01'),
|
||||
updatedAt: new Date('2024-01-02'),
|
||||
expiresAt: null,
|
||||
deletedAt: null,
|
||||
redeemedAt: null,
|
||||
policy: null,
|
||||
KeyUser: null,
|
||||
});
|
||||
|
||||
await getToken(admin as any, req);
|
||||
|
||||
const result = getResponseResult(admin);
|
||||
expect(result.token).toBe('abc123token');
|
||||
});
|
||||
|
||||
it('should include redeemed_by when token is redeemed', async () => {
|
||||
const admin = createMockAdmin({
|
||||
getKeys: vi.fn().mockResolvedValue([
|
||||
{ name: 'my-key', npub: 'npub1xyz123' },
|
||||
]),
|
||||
});
|
||||
const req = createMockRequest(['1']);
|
||||
|
||||
mockPrisma.token.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
keyName: 'my-key',
|
||||
token: 'abc123token',
|
||||
clientName: 'Test App',
|
||||
createdBy: 'admin-pubkey',
|
||||
policyId: 1,
|
||||
createdAt: new Date('2024-01-01'),
|
||||
updatedAt: new Date('2024-01-02'),
|
||||
expiresAt: null,
|
||||
deletedAt: null,
|
||||
redeemedAt: new Date('2024-01-05'),
|
||||
policy: { name: 'signing-policy' },
|
||||
KeyUser: { description: 'Redeemed by Test Client' },
|
||||
});
|
||||
|
||||
await getToken(admin as any, req);
|
||||
|
||||
const result = getResponseResult(admin);
|
||||
expect(result.redeemed_at).toBeDefined();
|
||||
expect(result.redeemed_by).toBe('Redeemed by Test Client');
|
||||
});
|
||||
});
|
||||
169
src/daemon/admin/commands/__tests__/grant_permission.test.ts
Normal file
169
src/daemon/admin/commands/__tests__/grant_permission.test.ts
Normal file
@ -0,0 +1,169 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createMockAdmin, createMockRequest, getResponseResult, resetMocks, mockPrisma } from './test-utils';
|
||||
|
||||
// Mock the prisma module
|
||||
vi.mock('../../../../db.js', () => ({
|
||||
default: mockPrisma,
|
||||
}));
|
||||
|
||||
import grantPermission from '../grant_permission';
|
||||
|
||||
describe('grant_permission', () => {
|
||||
beforeEach(() => {
|
||||
resetMocks();
|
||||
});
|
||||
|
||||
it('should throw error when keyName is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest([]);
|
||||
|
||||
await expect(grantPermission(admin as any, req)).rejects.toThrow('Invalid params: keyName, userPubkey, and policyId required');
|
||||
});
|
||||
|
||||
it('should throw error when userPubkey is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key']);
|
||||
|
||||
await expect(grantPermission(admin as any, req)).rejects.toThrow('Invalid params: keyName, userPubkey, and policyId required');
|
||||
});
|
||||
|
||||
it('should throw error when policyId is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key', 'pubkey123']);
|
||||
|
||||
await expect(grantPermission(admin as any, req)).rejects.toThrow('Invalid params: keyName, userPubkey, and policyId required');
|
||||
});
|
||||
|
||||
it('should throw error when policyId is not a number', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key', 'pubkey123', 'invalid']);
|
||||
|
||||
await expect(grantPermission(admin as any, req)).rejects.toThrow('Invalid params: policyId must be a number');
|
||||
});
|
||||
|
||||
it('should throw error when policy is not found', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key', 'pubkey123', '1']);
|
||||
|
||||
mockPrisma.policy.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(grantPermission(admin as any, req)).rejects.toThrow("Policy with id '1' not found");
|
||||
});
|
||||
|
||||
it('should throw error when policy is deleted', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key', 'pubkey123', '1']);
|
||||
|
||||
mockPrisma.policy.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
deletedAt: new Date('2024-01-01'),
|
||||
rules: [],
|
||||
});
|
||||
|
||||
await expect(grantPermission(admin as any, req)).rejects.toThrow("Policy with id '1' has been deleted");
|
||||
});
|
||||
|
||||
it('should grant permission successfully with hex pubkey', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key', 'abc123def456', '1', 'Test User']);
|
||||
|
||||
mockPrisma.policy.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
deletedAt: null,
|
||||
rules: [
|
||||
{ method: 'sign_event', kind: '1' },
|
||||
],
|
||||
});
|
||||
|
||||
mockPrisma.keyUser.upsert.mockResolvedValue({
|
||||
id: 1,
|
||||
keyName: 'my-key',
|
||||
userPubkey: 'abc123def456',
|
||||
createdAt: new Date('2024-01-01'),
|
||||
description: 'Test User',
|
||||
});
|
||||
|
||||
mockPrisma.signingCondition.deleteMany.mockResolvedValue({ count: 0 });
|
||||
mockPrisma.signingCondition.create.mockResolvedValue({});
|
||||
|
||||
await grantPermission(admin as any, req);
|
||||
|
||||
// Verify KeyUser was upserted
|
||||
expect(mockPrisma.keyUser.upsert).toHaveBeenCalledWith({
|
||||
where: {
|
||||
unique_key_user: {
|
||||
keyName: 'my-key',
|
||||
userPubkey: 'abc123def456',
|
||||
},
|
||||
},
|
||||
update: {
|
||||
revokedAt: null,
|
||||
description: 'Test User',
|
||||
},
|
||||
create: {
|
||||
keyName: 'my-key',
|
||||
userPubkey: 'abc123def456',
|
||||
description: 'Test User',
|
||||
},
|
||||
});
|
||||
|
||||
// Verify old signing conditions were deleted
|
||||
expect(mockPrisma.signingCondition.deleteMany).toHaveBeenCalledWith({
|
||||
where: { keyUserId: 1 },
|
||||
});
|
||||
|
||||
// Verify new signing conditions were created
|
||||
expect(mockPrisma.signingCondition.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
keyUserId: 1,
|
||||
method: 'sign_event',
|
||||
kind: '1',
|
||||
allowed: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Verify response
|
||||
expect(admin.rpc.sendResponse).toHaveBeenCalledTimes(1);
|
||||
const result = getResponseResult(admin);
|
||||
expect(result.id).toBe(1);
|
||||
expect(result.key_name).toBe('my-key');
|
||||
expect(result.description).toBe('Test User');
|
||||
});
|
||||
|
||||
it('should convert npub to hex pubkey', async () => {
|
||||
const admin = createMockAdmin();
|
||||
// Use a properly encoded npub (this is a valid bech32 encoded pubkey)
|
||||
// npub for hex pubkey: 0000000000000000000000000000000000000000000000000000000000000001
|
||||
const req = createMockRequest(['my-key', 'npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqshp52w2', '1']);
|
||||
|
||||
mockPrisma.policy.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
deletedAt: null,
|
||||
rules: [],
|
||||
});
|
||||
|
||||
mockPrisma.keyUser.upsert.mockResolvedValue({
|
||||
id: 1,
|
||||
keyName: 'my-key',
|
||||
userPubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
createdAt: new Date('2024-01-01'),
|
||||
description: null,
|
||||
});
|
||||
|
||||
mockPrisma.signingCondition.deleteMany.mockResolvedValue({ count: 0 });
|
||||
|
||||
await grantPermission(admin as any, req);
|
||||
|
||||
// Verify the pubkey was converted from npub (should be hex, not npub)
|
||||
expect(mockPrisma.keyUser.upsert).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: {
|
||||
unique_key_user: {
|
||||
keyName: 'my-key',
|
||||
userPubkey: expect.not.stringMatching(/^npub1/),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
25
src/daemon/admin/commands/__tests__/ping.test.ts
Normal file
25
src/daemon/admin/commands/__tests__/ping.test.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createMockAdmin, createMockRequest, resetMocks } from './test-utils';
|
||||
|
||||
import ping from '../ping';
|
||||
|
||||
describe('ping', () => {
|
||||
beforeEach(() => {
|
||||
resetMocks();
|
||||
});
|
||||
|
||||
it('should respond with ok', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest([]);
|
||||
|
||||
await ping(admin as any, req);
|
||||
|
||||
expect(admin.rpc.sendResponse).toHaveBeenCalledTimes(1);
|
||||
expect(admin.rpc.sendResponse).toHaveBeenCalledWith(
|
||||
req.id,
|
||||
req.pubkey,
|
||||
'ok',
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
71
src/daemon/admin/commands/__tests__/rename_key_user.test.ts
Normal file
71
src/daemon/admin/commands/__tests__/rename_key_user.test.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createMockAdmin, createMockRequest, getResponseResult, resetMocks, mockPrisma } from './test-utils';
|
||||
|
||||
// Mock the prisma module
|
||||
vi.mock('../../../../db.js', () => ({
|
||||
default: mockPrisma,
|
||||
}));
|
||||
|
||||
import renameKeyUser from '../rename_key_user';
|
||||
|
||||
describe('rename_key_user', () => {
|
||||
beforeEach(() => {
|
||||
resetMocks();
|
||||
});
|
||||
|
||||
it('should throw error when keyUserPubkey is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest([]);
|
||||
|
||||
await expect(renameKeyUser(admin as any, req)).rejects.toThrow('Invalid params');
|
||||
});
|
||||
|
||||
it('should throw error when name is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['pubkey123']);
|
||||
|
||||
await expect(renameKeyUser(admin as any, req)).rejects.toThrow('Invalid params');
|
||||
});
|
||||
|
||||
it('should throw error when key user is not found', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['pubkey123', 'New Name']);
|
||||
|
||||
mockPrisma.keyUser.findFirst.mockResolvedValue(null);
|
||||
|
||||
await expect(renameKeyUser(admin as any, req)).rejects.toThrow('Key user not found');
|
||||
});
|
||||
|
||||
it('should update key user description', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['pubkey123', 'New Description']);
|
||||
|
||||
mockPrisma.keyUser.findFirst.mockResolvedValue({
|
||||
id: 1,
|
||||
userPubkey: 'pubkey123',
|
||||
description: 'Old Description',
|
||||
});
|
||||
|
||||
mockPrisma.keyUser.update.mockResolvedValue({
|
||||
id: 1,
|
||||
description: 'New Description',
|
||||
});
|
||||
|
||||
await renameKeyUser(admin as any, req);
|
||||
|
||||
expect(mockPrisma.keyUser.findFirst).toHaveBeenCalledWith({
|
||||
where: {
|
||||
userPubkey: 'pubkey123',
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockPrisma.keyUser.update).toHaveBeenCalledWith({
|
||||
where: { id: 1 },
|
||||
data: { description: 'New Description' },
|
||||
});
|
||||
|
||||
expect(admin.rpc.sendResponse).toHaveBeenCalledTimes(1);
|
||||
const result = getResponseResult(admin);
|
||||
expect(result).toEqual(['ok']);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,78 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createMockAdmin, createMockRequest, getResponseResult, resetMocks, mockPrisma } from './test-utils';
|
||||
|
||||
// Mock the prisma module
|
||||
vi.mock('../../../../db.js', () => ({
|
||||
default: mockPrisma,
|
||||
}));
|
||||
|
||||
import revokePermission from '../revoke_permission';
|
||||
|
||||
describe('revoke_permission', () => {
|
||||
beforeEach(() => {
|
||||
resetMocks();
|
||||
});
|
||||
|
||||
it('should throw error when keyName is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest([]);
|
||||
|
||||
await expect(revokePermission(admin as any, req)).rejects.toThrow('Invalid params: keyName and userPubkey required');
|
||||
});
|
||||
|
||||
it('should throw error when userPubkey is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key']);
|
||||
|
||||
await expect(revokePermission(admin as any, req)).rejects.toThrow('Invalid params: keyName and userPubkey required');
|
||||
});
|
||||
|
||||
it('should throw error when permission is not found', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key', 'pubkey123']);
|
||||
|
||||
mockPrisma.keyUser.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(revokePermission(admin as any, req)).rejects.toThrow("Permission not found for user on key 'my-key'");
|
||||
});
|
||||
|
||||
it('should throw error when permission is already revoked', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key', 'pubkey123']);
|
||||
|
||||
mockPrisma.keyUser.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
keyName: 'my-key',
|
||||
userPubkey: 'pubkey123',
|
||||
revokedAt: new Date('2024-01-01'),
|
||||
});
|
||||
|
||||
await expect(revokePermission(admin as any, req)).rejects.toThrow('Permission already revoked');
|
||||
});
|
||||
|
||||
it('should revoke permission successfully', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key', 'pubkey123']);
|
||||
|
||||
mockPrisma.keyUser.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
keyName: 'my-key',
|
||||
userPubkey: 'pubkey123',
|
||||
revokedAt: null,
|
||||
});
|
||||
mockPrisma.keyUser.update.mockResolvedValue({});
|
||||
|
||||
await revokePermission(admin as any, req);
|
||||
|
||||
// Verify KeyUser was updated with revokedAt
|
||||
expect(mockPrisma.keyUser.update).toHaveBeenCalledWith({
|
||||
where: { id: 1 },
|
||||
data: { revokedAt: expect.any(Date) },
|
||||
});
|
||||
|
||||
// Verify response
|
||||
expect(admin.rpc.sendResponse).toHaveBeenCalledTimes(1);
|
||||
const result = getResponseResult(admin);
|
||||
expect(result).toEqual(['ok']);
|
||||
});
|
||||
});
|
||||
74
src/daemon/admin/commands/__tests__/revoke_token.test.ts
Normal file
74
src/daemon/admin/commands/__tests__/revoke_token.test.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createMockAdmin, createMockRequest, getResponseResult, resetMocks, mockPrisma } from './test-utils';
|
||||
|
||||
// Mock the prisma module
|
||||
vi.mock('../../../../db.js', () => ({
|
||||
default: mockPrisma,
|
||||
}));
|
||||
|
||||
import revokeToken from '../revoke_token';
|
||||
|
||||
describe('revoke_token', () => {
|
||||
beforeEach(() => {
|
||||
resetMocks();
|
||||
});
|
||||
|
||||
it('should throw error when tokenId is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest([]);
|
||||
|
||||
await expect(revokeToken(admin as any, req)).rejects.toThrow('Invalid params: tokenId required');
|
||||
});
|
||||
|
||||
it('should throw error when tokenId is not a number', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['invalid']);
|
||||
|
||||
await expect(revokeToken(admin as any, req)).rejects.toThrow('Invalid params: tokenId must be a number');
|
||||
});
|
||||
|
||||
it('should throw error when token is not found', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['1']);
|
||||
|
||||
mockPrisma.token.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(revokeToken(admin as any, req)).rejects.toThrow("Token with id '1' not found");
|
||||
});
|
||||
|
||||
it('should throw error when token is already revoked', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['1']);
|
||||
|
||||
mockPrisma.token.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
deletedAt: new Date('2024-01-01'),
|
||||
});
|
||||
|
||||
await expect(revokeToken(admin as any, req)).rejects.toThrow("Token with id '1' is already revoked");
|
||||
});
|
||||
|
||||
it('should revoke token successfully', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['1']);
|
||||
|
||||
mockPrisma.token.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
deletedAt: null,
|
||||
});
|
||||
mockPrisma.token.update.mockResolvedValue({});
|
||||
|
||||
await revokeToken(admin as any, req);
|
||||
|
||||
// Verify token was soft-deleted
|
||||
expect(mockPrisma.token.update).toHaveBeenCalledWith({
|
||||
where: { id: 1 },
|
||||
data: { deletedAt: expect.any(Date) },
|
||||
});
|
||||
|
||||
// Verify response
|
||||
expect(admin.rpc.sendResponse).toHaveBeenCalledTimes(1);
|
||||
const result = getResponseResult(admin);
|
||||
expect(result).toEqual(['ok']);
|
||||
});
|
||||
});
|
||||
67
src/daemon/admin/commands/__tests__/revoke_user.test.ts
Normal file
67
src/daemon/admin/commands/__tests__/revoke_user.test.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createMockAdmin, createMockRequest, getResponseResult, resetMocks, mockPrisma } from './test-utils';
|
||||
|
||||
// Mock the prisma module
|
||||
vi.mock('../../../../db.js', () => ({
|
||||
default: mockPrisma,
|
||||
}));
|
||||
|
||||
import revokeUser from '../revoke_user';
|
||||
|
||||
describe('revoke_user', () => {
|
||||
beforeEach(() => {
|
||||
resetMocks();
|
||||
});
|
||||
|
||||
it('should throw error when keyUserId is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest([]);
|
||||
|
||||
await expect(revokeUser(admin as any, req)).rejects.toThrow('Invalid params');
|
||||
});
|
||||
|
||||
it('should throw error when keyUserId is not a number', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['not-a-number']);
|
||||
|
||||
await expect(revokeUser(admin as any, req)).rejects.toThrow('Invalid params');
|
||||
});
|
||||
|
||||
it('should revoke user by setting revokedAt', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['1']);
|
||||
|
||||
mockPrisma.keyUser.update.mockResolvedValue({
|
||||
id: 1,
|
||||
revokedAt: new Date(),
|
||||
});
|
||||
|
||||
await revokeUser(admin as any, req);
|
||||
|
||||
expect(mockPrisma.keyUser.update).toHaveBeenCalledWith({
|
||||
where: { id: 1 },
|
||||
data: { revokedAt: expect.any(Date) },
|
||||
});
|
||||
|
||||
expect(admin.rpc.sendResponse).toHaveBeenCalledTimes(1);
|
||||
const result = getResponseResult(admin);
|
||||
expect(result).toEqual(['ok']);
|
||||
});
|
||||
|
||||
it('should parse string id to integer', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['42']);
|
||||
|
||||
mockPrisma.keyUser.update.mockResolvedValue({
|
||||
id: 42,
|
||||
revokedAt: new Date(),
|
||||
});
|
||||
|
||||
await revokeUser(admin as any, req);
|
||||
|
||||
expect(mockPrisma.keyUser.update).toHaveBeenCalledWith({
|
||||
where: { id: 42 },
|
||||
data: { revokedAt: expect.any(Date) },
|
||||
});
|
||||
});
|
||||
});
|
||||
123
src/daemon/admin/commands/__tests__/rotate_key.test.ts
Normal file
123
src/daemon/admin/commands/__tests__/rotate_key.test.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createMockAdmin, createMockRequest, getResponseResult, resetMocks, mockPrisma } from './test-utils';
|
||||
|
||||
// Mock the prisma module
|
||||
vi.mock('../../../../db.js', () => ({
|
||||
default: mockPrisma,
|
||||
}));
|
||||
|
||||
// Mock the saveEncrypted function
|
||||
vi.mock('../../../../commands/add.js', () => ({
|
||||
saveEncrypted: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
import rotateKey from '../rotate_key';
|
||||
|
||||
describe('rotate_key', () => {
|
||||
beforeEach(() => {
|
||||
resetMocks();
|
||||
});
|
||||
|
||||
it('should throw error when oldKeyName is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest([]);
|
||||
|
||||
await expect(rotateKey(admin as any, req)).rejects.toThrow('Invalid params: oldKeyName, newKeyName, and passphrase required');
|
||||
});
|
||||
|
||||
it('should throw error when newKeyName is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['old-key']);
|
||||
|
||||
await expect(rotateKey(admin as any, req)).rejects.toThrow('Invalid params: oldKeyName, newKeyName, and passphrase required');
|
||||
});
|
||||
|
||||
it('should throw error when passphrase is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['old-key', 'new-key']);
|
||||
|
||||
await expect(rotateKey(admin as any, req)).rejects.toThrow('Invalid params: oldKeyName, newKeyName, and passphrase required');
|
||||
});
|
||||
|
||||
it('should throw error when loadNsec is not implemented', async () => {
|
||||
const admin = createMockAdmin({ loadNsec: undefined });
|
||||
const req = createMockRequest(['old-key', 'new-key', 'passphrase']);
|
||||
|
||||
await expect(rotateKey(admin as any, req)).rejects.toThrow('No loadNsec method');
|
||||
});
|
||||
|
||||
it('should throw error when old key is not found', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['old-key', 'new-key', 'passphrase']);
|
||||
|
||||
mockPrisma.key.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(rotateKey(admin as any, req)).rejects.toThrow("Key 'old-key' not found");
|
||||
});
|
||||
|
||||
it('should throw error when old key is already deleted', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['old-key', 'new-key', 'passphrase']);
|
||||
|
||||
mockPrisma.key.findUnique.mockResolvedValueOnce({
|
||||
keyName: 'old-key',
|
||||
deletedAt: new Date('2024-01-01'),
|
||||
});
|
||||
|
||||
await expect(rotateKey(admin as any, req)).rejects.toThrow("Key 'old-key' is already deleted");
|
||||
});
|
||||
|
||||
it('should throw error when new key name already exists', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['old-key', 'new-key', 'passphrase']);
|
||||
|
||||
mockPrisma.key.findUnique
|
||||
.mockResolvedValueOnce({ keyName: 'old-key', deletedAt: null }) // old key exists
|
||||
.mockResolvedValueOnce({ keyName: 'new-key' }); // new key also exists
|
||||
|
||||
await expect(rotateKey(admin as any, req)).rejects.toThrow("Key 'new-key' already exists");
|
||||
});
|
||||
|
||||
it('should rotate key successfully', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['old-key', 'new-key', 'passphrase']);
|
||||
|
||||
// Old key exists, new key doesn't
|
||||
mockPrisma.key.findUnique
|
||||
.mockResolvedValueOnce({ keyName: 'old-key', deletedAt: null })
|
||||
.mockResolvedValueOnce(null);
|
||||
|
||||
// No existing key users to migrate
|
||||
mockPrisma.keyUser.findMany.mockResolvedValue([]);
|
||||
|
||||
// Mock create operations
|
||||
mockPrisma.key.create.mockResolvedValue({});
|
||||
mockPrisma.key.update.mockResolvedValue({});
|
||||
mockPrisma.token.updateMany.mockResolvedValue({ count: 0 });
|
||||
|
||||
await rotateKey(admin as any, req);
|
||||
|
||||
// Verify new key was created
|
||||
expect(mockPrisma.key.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
keyName: 'new-key',
|
||||
pubkey: expect.any(String),
|
||||
},
|
||||
});
|
||||
|
||||
// Verify old key was soft-deleted
|
||||
expect(mockPrisma.key.update).toHaveBeenCalledWith({
|
||||
where: { keyName: 'old-key' },
|
||||
data: { deletedAt: expect.any(Date) },
|
||||
});
|
||||
|
||||
// Verify loadNsec was called
|
||||
expect(admin.loadNsec).toHaveBeenCalledWith('new-key', expect.stringMatching(/^nsec1/));
|
||||
|
||||
// Verify response
|
||||
expect(admin.rpc.sendResponse).toHaveBeenCalledTimes(1);
|
||||
const result = getResponseResult(admin);
|
||||
expect(result.name).toBe('new-key');
|
||||
expect(result.npub).toMatch(/^npub1/);
|
||||
});
|
||||
});
|
||||
108
src/daemon/admin/commands/__tests__/test-utils.ts
Normal file
108
src/daemon/admin/commands/__tests__/test-utils.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { vi } from 'vitest';
|
||||
import type { NDKRpcRequest } from '@nostr-dev-kit/ndk';
|
||||
import type AdminInterface from '../../index.js';
|
||||
|
||||
// Mock Prisma client
|
||||
export const mockPrisma = {
|
||||
key: {
|
||||
findUnique: vi.fn(),
|
||||
findMany: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
keyUser: {
|
||||
findUnique: vi.fn(),
|
||||
findFirst: vi.fn(),
|
||||
findMany: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
upsert: vi.fn(),
|
||||
count: vi.fn(),
|
||||
},
|
||||
policy: {
|
||||
findUnique: vi.fn(),
|
||||
findMany: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
policyRule: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
token: {
|
||||
findUnique: vi.fn(),
|
||||
findMany: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
updateMany: vi.fn(),
|
||||
count: vi.fn(),
|
||||
},
|
||||
signingCondition: {
|
||||
create: vi.fn(),
|
||||
deleteMany: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
// Reset all mocks
|
||||
export function resetMocks() {
|
||||
vi.clearAllMocks();
|
||||
}
|
||||
|
||||
// Create a mock NDKRpcRequest
|
||||
export function createMockRequest(params: string[] = []): NDKRpcRequest {
|
||||
return {
|
||||
id: 'test-request-id',
|
||||
pubkey: 'test-pubkey-hex',
|
||||
method: 'test-method',
|
||||
params,
|
||||
event: {
|
||||
kind: 24133, // NIP-46 NostrConnect kind
|
||||
},
|
||||
} as NDKRpcRequest;
|
||||
}
|
||||
|
||||
// Create a mock AdminInterface
|
||||
export function createMockAdmin(overrides: Partial<MockAdminInterface> = {}): MockAdminInterface {
|
||||
const sendResponseMock = vi.fn();
|
||||
|
||||
return {
|
||||
configFile: '/tmp/test-config.json',
|
||||
rpc: {
|
||||
sendResponse: sendResponseMock,
|
||||
},
|
||||
getKeys: vi.fn().mockResolvedValue([]),
|
||||
getKeyUsers: vi.fn().mockResolvedValue([]),
|
||||
unlockKey: vi.fn().mockResolvedValue(true),
|
||||
loadNsec: vi.fn(),
|
||||
config: vi.fn().mockResolvedValue({}),
|
||||
...overrides,
|
||||
} as MockAdminInterface;
|
||||
}
|
||||
|
||||
export interface MockAdminInterface {
|
||||
configFile: string;
|
||||
rpc: {
|
||||
sendResponse: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
getKeys?: ReturnType<typeof vi.fn>;
|
||||
getKeyUsers?: ReturnType<typeof vi.fn>;
|
||||
unlockKey?: ReturnType<typeof vi.fn>;
|
||||
loadNsec?: ReturnType<typeof vi.fn>;
|
||||
config?: ReturnType<typeof vi.fn>;
|
||||
}
|
||||
|
||||
// Helper to extract the result from sendResponse mock call
|
||||
export function getResponseResult(admin: MockAdminInterface): any {
|
||||
const calls = admin.rpc.sendResponse.mock.calls;
|
||||
if (calls.length === 0) return null;
|
||||
const lastCall = calls[calls.length - 1];
|
||||
return JSON.parse(lastCall[2]); // result is the 3rd argument
|
||||
}
|
||||
|
||||
// Helper to check if response was an error
|
||||
export function getResponseError(admin: MockAdminInterface): string | null {
|
||||
const calls = admin.rpc.sendResponse.mock.calls;
|
||||
if (calls.length === 0) return null;
|
||||
const lastCall = calls[calls.length - 1];
|
||||
return lastCall[4] || null; // error is the 5th argument
|
||||
}
|
||||
61
src/daemon/admin/commands/__tests__/unlock_key.test.ts
Normal file
61
src/daemon/admin/commands/__tests__/unlock_key.test.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createMockAdmin, createMockRequest, getResponseResult, resetMocks } from './test-utils';
|
||||
|
||||
import unlockKey from '../unlock_key';
|
||||
|
||||
describe('unlock_key', () => {
|
||||
beforeEach(() => {
|
||||
resetMocks();
|
||||
});
|
||||
|
||||
it('should throw error when keyName is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest([]);
|
||||
|
||||
await expect(unlockKey(admin as any, req)).rejects.toThrow('Invalid params');
|
||||
});
|
||||
|
||||
it('should throw error when passphrase is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['my-key']);
|
||||
|
||||
await expect(unlockKey(admin as any, req)).rejects.toThrow('Invalid params');
|
||||
});
|
||||
|
||||
it('should throw error when unlockKey method is not implemented', async () => {
|
||||
const admin = createMockAdmin({ unlockKey: undefined });
|
||||
const req = createMockRequest(['my-key', 'passphrase']);
|
||||
|
||||
await expect(unlockKey(admin as any, req)).rejects.toThrow('No unlockKey method');
|
||||
});
|
||||
|
||||
it('should return success true when unlock succeeds', async () => {
|
||||
const admin = createMockAdmin({
|
||||
unlockKey: vi.fn().mockResolvedValue(true),
|
||||
});
|
||||
const req = createMockRequest(['my-key', 'correct-passphrase']);
|
||||
|
||||
await unlockKey(admin as any, req);
|
||||
|
||||
expect(admin.unlockKey).toHaveBeenCalledWith('my-key', 'correct-passphrase');
|
||||
expect(admin.rpc.sendResponse).toHaveBeenCalledTimes(1);
|
||||
|
||||
const result = getResponseResult(admin);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should return success false with error when unlock fails', async () => {
|
||||
const admin = createMockAdmin({
|
||||
unlockKey: vi.fn().mockRejectedValue(new Error('Wrong passphrase')),
|
||||
});
|
||||
const req = createMockRequest(['my-key', 'wrong-passphrase']);
|
||||
|
||||
await unlockKey(admin as any, req);
|
||||
|
||||
expect(admin.rpc.sendResponse).toHaveBeenCalledTimes(1);
|
||||
|
||||
const result = getResponseResult(admin);
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Wrong passphrase');
|
||||
});
|
||||
});
|
||||
138
src/daemon/admin/commands/__tests__/validate_token.test.ts
Normal file
138
src/daemon/admin/commands/__tests__/validate_token.test.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createMockAdmin, createMockRequest, getResponseResult, resetMocks, mockPrisma } from './test-utils';
|
||||
|
||||
// Mock the prisma module
|
||||
vi.mock('../../../../db.js', () => ({
|
||||
default: mockPrisma,
|
||||
}));
|
||||
|
||||
import validateToken from '../validate_token';
|
||||
|
||||
describe('validate_token', () => {
|
||||
beforeEach(() => {
|
||||
resetMocks();
|
||||
});
|
||||
|
||||
it('should throw error when token is not provided', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest([]);
|
||||
|
||||
await expect(validateToken(admin as any, req)).rejects.toThrow('Invalid params: token required');
|
||||
});
|
||||
|
||||
it('should return invalid when token is not found', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['nonexistent-token']);
|
||||
|
||||
mockPrisma.token.findUnique.mockResolvedValue(null);
|
||||
|
||||
await validateToken(admin as any, req);
|
||||
|
||||
const result = getResponseResult(admin);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.reason).toBe('Token not found');
|
||||
});
|
||||
|
||||
it('should return invalid when token is revoked', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['revoked-token']);
|
||||
|
||||
mockPrisma.token.findUnique.mockResolvedValue({
|
||||
token: 'revoked-token',
|
||||
deletedAt: new Date('2024-01-01'),
|
||||
expiresAt: null,
|
||||
});
|
||||
|
||||
await validateToken(admin as any, req);
|
||||
|
||||
const result = getResponseResult(admin);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.reason).toBe('Token has been revoked');
|
||||
});
|
||||
|
||||
it('should return invalid when token is expired', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['expired-token']);
|
||||
|
||||
mockPrisma.token.findUnique.mockResolvedValue({
|
||||
token: 'expired-token',
|
||||
deletedAt: null,
|
||||
expiresAt: new Date('2020-01-01'), // Past date
|
||||
});
|
||||
|
||||
await validateToken(admin as any, req);
|
||||
|
||||
const result = getResponseResult(admin);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.reason).toBe('Token has expired');
|
||||
});
|
||||
|
||||
it('should return valid for active token', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['valid-token']);
|
||||
|
||||
const futureDate = new Date();
|
||||
futureDate.setFullYear(futureDate.getFullYear() + 1);
|
||||
|
||||
mockPrisma.token.findUnique.mockResolvedValue({
|
||||
token: 'valid-token',
|
||||
keyName: 'my-key',
|
||||
clientName: 'Test App',
|
||||
deletedAt: null,
|
||||
expiresAt: futureDate,
|
||||
redeemedAt: null,
|
||||
});
|
||||
|
||||
await validateToken(admin as any, req);
|
||||
|
||||
const result = getResponseResult(admin);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.key_name).toBe('my-key');
|
||||
expect(result.client_name).toBe('Test App');
|
||||
expect(result.redeemed).toBe(false);
|
||||
});
|
||||
|
||||
it('should return valid for token without expiration', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['valid-token']);
|
||||
|
||||
mockPrisma.token.findUnique.mockResolvedValue({
|
||||
token: 'valid-token',
|
||||
keyName: 'my-key',
|
||||
clientName: 'Test App',
|
||||
deletedAt: null,
|
||||
expiresAt: null, // No expiration
|
||||
redeemedAt: new Date('2024-01-05'),
|
||||
});
|
||||
|
||||
await validateToken(admin as any, req);
|
||||
|
||||
const result = getResponseResult(admin);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.redeemed).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse token with npub# prefix', async () => {
|
||||
const admin = createMockAdmin();
|
||||
const req = createMockRequest(['npub1xyz123#actual-token-value']);
|
||||
|
||||
mockPrisma.token.findUnique.mockResolvedValue({
|
||||
token: 'actual-token-value',
|
||||
keyName: 'my-key',
|
||||
clientName: 'Test App',
|
||||
deletedAt: null,
|
||||
expiresAt: null,
|
||||
redeemedAt: null,
|
||||
});
|
||||
|
||||
await validateToken(admin as any, req);
|
||||
|
||||
// Verify the token lookup used the parsed value
|
||||
expect(mockPrisma.token.findUnique).toHaveBeenCalledWith({
|
||||
where: { token: 'actual-token-value' },
|
||||
});
|
||||
|
||||
const result = getResponseResult(admin);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
@ -132,7 +132,7 @@ export default async function createAccount(admin: AdminInterface, req: NDKRpcRe
|
||||
username = payload[0];
|
||||
domain = payload[1];
|
||||
email = payload[2];
|
||||
return createAccountReal(admin, req, username, domain, email);
|
||||
return createAccountReal(admin, req, username!, domain!, email);
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,10 +210,10 @@ export async function createAccountReal(
|
||||
// access it without having to go through an approval flow
|
||||
await grantPermissions(req, keyName);
|
||||
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, generatedUser.pubkey, NDKKind.NostrConnectAdmin);
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, generatedUser.pubkey, NDKKind.NostrConnect);
|
||||
} catch (e: any) {
|
||||
console.trace('error', e);
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, "error", NDKKind.NostrConnectAdmin,
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, "error", NDKKind.NostrConnect,
|
||||
e.message);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import NDK, { NDKEvent, NDKPrivateKeySigner, NDKRpcRequest, type NostrEvent } from "@nostr-dev-kit/ndk";
|
||||
import NDK, { NDKEvent, NDKKind, NDKPrivateKeySigner, NDKRpcRequest, type NostrEvent } from "@nostr-dev-kit/ndk";
|
||||
import AdminInterface from "../index.js";
|
||||
import { saveEncrypted } from "../../../commands/add.js";
|
||||
import { nip19 } from 'nostr-tools';
|
||||
@ -39,5 +39,5 @@ export default async function createNewKey(admin: AdminInterface, req: NDKRpcReq
|
||||
npub: user.npub,
|
||||
});
|
||||
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, 24134);
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import { NDKKind, NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import AdminInterface from "../index.js";
|
||||
import prisma from "../../../db.js";
|
||||
|
||||
@ -29,5 +29,5 @@ export default async function createNewPolicy(admin: AdminInterface, req: NDKRpc
|
||||
}
|
||||
|
||||
const result = JSON.stringify(["ok"]);
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, 24134);
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import { NDKKind, NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import AdminInterface from "../index.js";
|
||||
import prisma from "../../../db.js";
|
||||
|
||||
@ -26,5 +26,5 @@ export default async function createNewToken(admin: AdminInterface, req: NDKRpcR
|
||||
if (!tokenRecord) throw new Error("Token not created");
|
||||
|
||||
const result = JSON.stringify(["ok"]);
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, 24134);
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
41
src/daemon/admin/commands/delete_key.ts
Normal file
41
src/daemon/admin/commands/delete_key.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { NDKKind, NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import AdminInterface from "../index.js";
|
||||
import prisma from "../../../db.js";
|
||||
|
||||
export default async function deleteKey(admin: AdminInterface, req: NDKRpcRequest) {
|
||||
const [keyName] = req.params as [string];
|
||||
|
||||
if (!keyName) throw new Error("Invalid params: keyName required");
|
||||
|
||||
// Check if key exists in database
|
||||
const existingKey = await prisma.key.findUnique({
|
||||
where: { keyName },
|
||||
});
|
||||
|
||||
if (!existingKey) {
|
||||
throw new Error(`Key '${keyName}' not found`);
|
||||
}
|
||||
|
||||
if (existingKey.deletedAt) {
|
||||
throw new Error(`Key '${keyName}' is already deleted`);
|
||||
}
|
||||
|
||||
// Soft delete the key
|
||||
await prisma.key.update({
|
||||
where: { keyName },
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
|
||||
// Also soft-delete all tokens for this key
|
||||
await prisma.token.updateMany({
|
||||
where: {
|
||||
keyName,
|
||||
deletedAt: null,
|
||||
},
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
|
||||
const result = JSON.stringify(["ok"]);
|
||||
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
34
src/daemon/admin/commands/delete_policy.ts
Normal file
34
src/daemon/admin/commands/delete_policy.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { NDKKind, NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import AdminInterface from "../index.js";
|
||||
import prisma from "../../../db.js";
|
||||
|
||||
export default async function deletePolicy(admin: AdminInterface, req: NDKRpcRequest) {
|
||||
const [policyIdStr] = req.params as [string];
|
||||
|
||||
if (!policyIdStr) throw new Error("Invalid params: policyId required");
|
||||
|
||||
const policyId = parseInt(policyIdStr);
|
||||
if (isNaN(policyId)) throw new Error("Invalid params: policyId must be a number");
|
||||
|
||||
const policy = await prisma.policy.findUnique({
|
||||
where: { id: policyId },
|
||||
});
|
||||
|
||||
if (!policy) {
|
||||
throw new Error(`Policy with id '${policyId}' not found`);
|
||||
}
|
||||
|
||||
if (policy.deletedAt) {
|
||||
throw new Error(`Policy with id '${policyId}' is already deleted`);
|
||||
}
|
||||
|
||||
// Soft delete the policy
|
||||
await prisma.policy.update({
|
||||
where: { id: policyId },
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
|
||||
const result = JSON.stringify(["ok"]);
|
||||
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
33
src/daemon/admin/commands/get_key.ts
Normal file
33
src/daemon/admin/commands/get_key.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { NDKKind, NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import AdminInterface from "../index.js";
|
||||
import prisma from "../../../db.js";
|
||||
|
||||
export default async function getKey(admin: AdminInterface, req: NDKRpcRequest) {
|
||||
const [keyName] = req.params as [string];
|
||||
|
||||
if (!keyName) throw new Error("Invalid params: keyName required");
|
||||
if (!admin.getKeys) throw new Error("getKeys() not implemented");
|
||||
|
||||
// Get all keys to check locked status
|
||||
const keys = await admin.getKeys();
|
||||
const keyInfo = keys.find((k) => k.name === keyName);
|
||||
|
||||
if (!keyInfo) {
|
||||
throw new Error(`Key '${keyName}' not found`);
|
||||
}
|
||||
|
||||
// Get additional metadata from database
|
||||
const dbKey = await prisma.key.findUnique({
|
||||
where: { keyName },
|
||||
});
|
||||
|
||||
const result = JSON.stringify({
|
||||
name: keyInfo.name,
|
||||
npub: keyInfo.npub || null,
|
||||
locked: !keyInfo.npub, // If no npub, the key is locked
|
||||
created_at: dbKey?.createdAt || null,
|
||||
updated_at: dbKey?.updatedAt || null,
|
||||
});
|
||||
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
63
src/daemon/admin/commands/get_permissions.ts
Normal file
63
src/daemon/admin/commands/get_permissions.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { NDKKind, NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import AdminInterface from "../index.js";
|
||||
import prisma from "../../../db.js";
|
||||
|
||||
export default async function getPermissions(admin: AdminInterface, req: NDKRpcRequest) {
|
||||
const [keyName, userPubkey] = req.params as [string, string];
|
||||
|
||||
if (!keyName || !userPubkey) {
|
||||
throw new Error("Invalid params: keyName and userPubkey required");
|
||||
}
|
||||
|
||||
// Normalize userPubkey (convert npub to hex if needed)
|
||||
let normalizedPubkey = userPubkey;
|
||||
if (userPubkey.startsWith('npub1')) {
|
||||
try {
|
||||
const decoded = nip19.decode(userPubkey);
|
||||
if (decoded.type === 'npub') {
|
||||
normalizedPubkey = decoded.data as string;
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error("Invalid npub format");
|
||||
}
|
||||
}
|
||||
|
||||
// Find the KeyUser with signing conditions
|
||||
const keyUser = await prisma.keyUser.findUnique({
|
||||
where: {
|
||||
unique_key_user: {
|
||||
keyName,
|
||||
userPubkey: normalizedPubkey,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
signingConditions: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!keyUser) {
|
||||
throw new Error(`Permission not found for user on key '${keyName}'`);
|
||||
}
|
||||
|
||||
const result = JSON.stringify({
|
||||
id: keyUser.id,
|
||||
key_name: keyUser.keyName,
|
||||
user_pubkey: keyUser.userPubkey,
|
||||
active: keyUser.revokedAt === null,
|
||||
created_at: keyUser.createdAt,
|
||||
updated_at: keyUser.updatedAt,
|
||||
revoked_at: keyUser.revokedAt,
|
||||
last_used_at: keyUser.lastUsedAt,
|
||||
description: keyUser.description,
|
||||
signing_conditions: keyUser.signingConditions.map((sc) => ({
|
||||
id: sc.id,
|
||||
method: sc.method,
|
||||
kind: sc.kind,
|
||||
content: sc.content,
|
||||
allowed: sc.allowed,
|
||||
})),
|
||||
});
|
||||
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
43
src/daemon/admin/commands/get_policy.ts
Normal file
43
src/daemon/admin/commands/get_policy.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { NDKKind, NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import AdminInterface from "../index.js";
|
||||
import prisma from "../../../db.js";
|
||||
|
||||
export default async function getPolicy(admin: AdminInterface, req: NDKRpcRequest) {
|
||||
const [policyIdStr] = req.params as [string];
|
||||
|
||||
if (!policyIdStr) throw new Error("Invalid params: policyId required");
|
||||
|
||||
const policyId = parseInt(policyIdStr);
|
||||
if (isNaN(policyId)) throw new Error("Invalid params: policyId must be a number");
|
||||
|
||||
const policy = await prisma.policy.findUnique({
|
||||
where: { id: policyId },
|
||||
include: { rules: true },
|
||||
});
|
||||
|
||||
if (!policy) {
|
||||
throw new Error(`Policy with id '${policyId}' not found`);
|
||||
}
|
||||
|
||||
if (policy.deletedAt) {
|
||||
throw new Error(`Policy with id '${policyId}' has been deleted`);
|
||||
}
|
||||
|
||||
const result = JSON.stringify({
|
||||
id: policy.id,
|
||||
name: policy.name,
|
||||
description: policy.description,
|
||||
created_at: policy.createdAt,
|
||||
updated_at: policy.updatedAt,
|
||||
expires_at: policy.expiresAt,
|
||||
rules: policy.rules.map((r) => ({
|
||||
id: r.id,
|
||||
method: r.method,
|
||||
kind: r.kind,
|
||||
max_usage_count: r.maxUsageCount,
|
||||
current_usage_count: r.currentUsageCount,
|
||||
})),
|
||||
});
|
||||
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
50
src/daemon/admin/commands/get_token.ts
Normal file
50
src/daemon/admin/commands/get_token.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { NDKKind, NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import AdminInterface from "../index.js";
|
||||
import prisma from "../../../db.js";
|
||||
|
||||
export default async function getToken(admin: AdminInterface, req: NDKRpcRequest) {
|
||||
const [tokenIdStr] = req.params as [string];
|
||||
|
||||
if (!tokenIdStr) throw new Error("Invalid params: tokenId required");
|
||||
|
||||
const tokenId = parseInt(tokenIdStr);
|
||||
if (isNaN(tokenId)) throw new Error("Invalid params: tokenId must be a number");
|
||||
|
||||
const token = await prisma.token.findUnique({
|
||||
where: { id: tokenId },
|
||||
include: {
|
||||
policy: true,
|
||||
KeyUser: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!token) {
|
||||
throw new Error(`Token with id '${tokenId}' not found`);
|
||||
}
|
||||
|
||||
// Get the key's npub for the token string
|
||||
let npub: string | null = null;
|
||||
if (admin.getKeys) {
|
||||
const keys = await admin.getKeys();
|
||||
const key = keys.find((k) => k.name === token.keyName);
|
||||
npub = key?.npub || null;
|
||||
}
|
||||
|
||||
const result = JSON.stringify({
|
||||
id: token.id,
|
||||
key_name: token.keyName,
|
||||
token: npub ? `${npub}#${token.token}` : token.token,
|
||||
client_name: token.clientName,
|
||||
created_by: token.createdBy,
|
||||
policy_id: token.policyId,
|
||||
policy_name: token.policy?.name,
|
||||
created_at: token.createdAt,
|
||||
updated_at: token.updatedAt,
|
||||
expires_at: token.expiresAt,
|
||||
deleted_at: token.deletedAt,
|
||||
redeemed_at: token.redeemedAt,
|
||||
redeemed_by: token.KeyUser?.description,
|
||||
});
|
||||
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
88
src/daemon/admin/commands/grant_permission.ts
Normal file
88
src/daemon/admin/commands/grant_permission.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { NDKKind, NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import AdminInterface from "../index.js";
|
||||
import prisma from "../../../db.js";
|
||||
|
||||
export default async function grantPermission(admin: AdminInterface, req: NDKRpcRequest) {
|
||||
const [keyName, userPubkey, policyIdStr, description] = req.params as [string, string, string, string?];
|
||||
|
||||
if (!keyName || !userPubkey || !policyIdStr) {
|
||||
throw new Error("Invalid params: keyName, userPubkey, and policyId required");
|
||||
}
|
||||
|
||||
// Normalize userPubkey (convert npub to hex if needed)
|
||||
let normalizedPubkey = userPubkey;
|
||||
if (userPubkey.startsWith('npub1')) {
|
||||
try {
|
||||
const decoded = nip19.decode(userPubkey);
|
||||
if (decoded.type === 'npub') {
|
||||
normalizedPubkey = decoded.data as string;
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error("Invalid npub format");
|
||||
}
|
||||
}
|
||||
|
||||
const policyId = parseInt(policyIdStr);
|
||||
if (isNaN(policyId)) throw new Error("Invalid params: policyId must be a number");
|
||||
|
||||
// Validate policy exists
|
||||
const policy = await prisma.policy.findUnique({
|
||||
where: { id: policyId },
|
||||
include: { rules: true },
|
||||
});
|
||||
|
||||
if (!policy) {
|
||||
throw new Error(`Policy with id '${policyId}' not found`);
|
||||
}
|
||||
|
||||
if (policy.deletedAt) {
|
||||
throw new Error(`Policy with id '${policyId}' has been deleted`);
|
||||
}
|
||||
|
||||
// Upsert KeyUser (create or update if exists)
|
||||
const keyUser = await prisma.keyUser.upsert({
|
||||
where: {
|
||||
unique_key_user: {
|
||||
keyName,
|
||||
userPubkey: normalizedPubkey,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
revokedAt: null, // Re-enable if previously revoked
|
||||
description: description || undefined,
|
||||
},
|
||||
create: {
|
||||
keyName,
|
||||
userPubkey: normalizedPubkey,
|
||||
description: description || undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// Remove existing signing conditions for this user
|
||||
await prisma.signingCondition.deleteMany({
|
||||
where: { keyUserId: keyUser.id },
|
||||
});
|
||||
|
||||
// Copy policy rules to signing conditions
|
||||
for (const rule of policy.rules) {
|
||||
await prisma.signingCondition.create({
|
||||
data: {
|
||||
keyUserId: keyUser.id,
|
||||
method: rule.method,
|
||||
kind: rule.kind,
|
||||
allowed: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const result = JSON.stringify({
|
||||
id: keyUser.id,
|
||||
key_name: keyUser.keyName,
|
||||
user_pubkey: keyUser.userPubkey,
|
||||
created_at: keyUser.createdAt,
|
||||
description: keyUser.description,
|
||||
});
|
||||
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import { NDKKind, NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import AdminInterface from "../index.js";
|
||||
|
||||
export default async function ping(admin: AdminInterface, req: NDKRpcRequest) {
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, "ok", 24134);
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, "ok", NDKKind.NostrConnect);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import { NDKKind, NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import AdminInterface from "../index.js";
|
||||
import prisma from "../../../db.js";
|
||||
|
||||
@ -25,5 +25,5 @@ export default async function renameKeyUser(admin: AdminInterface, req: NDKRpcRe
|
||||
});
|
||||
|
||||
const result = JSON.stringify(["ok"]);
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, 24134);
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
|
||||
53
src/daemon/admin/commands/revoke_permission.ts
Normal file
53
src/daemon/admin/commands/revoke_permission.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { NDKKind, NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import AdminInterface from "../index.js";
|
||||
import prisma from "../../../db.js";
|
||||
|
||||
export default async function revokePermission(admin: AdminInterface, req: NDKRpcRequest) {
|
||||
const [keyName, userPubkey] = req.params as [string, string];
|
||||
|
||||
if (!keyName || !userPubkey) {
|
||||
throw new Error("Invalid params: keyName and userPubkey required");
|
||||
}
|
||||
|
||||
// Normalize userPubkey (convert npub to hex if needed)
|
||||
let normalizedPubkey = userPubkey;
|
||||
if (userPubkey.startsWith('npub1')) {
|
||||
try {
|
||||
const decoded = nip19.decode(userPubkey);
|
||||
if (decoded.type === 'npub') {
|
||||
normalizedPubkey = decoded.data as string;
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error("Invalid npub format");
|
||||
}
|
||||
}
|
||||
|
||||
// Find the KeyUser
|
||||
const keyUser = await prisma.keyUser.findUnique({
|
||||
where: {
|
||||
unique_key_user: {
|
||||
keyName,
|
||||
userPubkey: normalizedPubkey,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!keyUser) {
|
||||
throw new Error(`Permission not found for user on key '${keyName}'`);
|
||||
}
|
||||
|
||||
if (keyUser.revokedAt) {
|
||||
throw new Error(`Permission already revoked`);
|
||||
}
|
||||
|
||||
// Revoke by setting revokedAt timestamp
|
||||
await prisma.keyUser.update({
|
||||
where: { id: keyUser.id },
|
||||
data: { revokedAt: new Date() },
|
||||
});
|
||||
|
||||
const result = JSON.stringify(["ok"]);
|
||||
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
34
src/daemon/admin/commands/revoke_token.ts
Normal file
34
src/daemon/admin/commands/revoke_token.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { NDKKind, NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import AdminInterface from "../index.js";
|
||||
import prisma from "../../../db.js";
|
||||
|
||||
export default async function revokeToken(admin: AdminInterface, req: NDKRpcRequest) {
|
||||
const [tokenIdStr] = req.params as [string];
|
||||
|
||||
if (!tokenIdStr) throw new Error("Invalid params: tokenId required");
|
||||
|
||||
const tokenId = parseInt(tokenIdStr);
|
||||
if (isNaN(tokenId)) throw new Error("Invalid params: tokenId must be a number");
|
||||
|
||||
const token = await prisma.token.findUnique({
|
||||
where: { id: tokenId },
|
||||
});
|
||||
|
||||
if (!token) {
|
||||
throw new Error(`Token with id '${tokenId}' not found`);
|
||||
}
|
||||
|
||||
if (token.deletedAt) {
|
||||
throw new Error(`Token with id '${tokenId}' is already revoked`);
|
||||
}
|
||||
|
||||
// Soft delete the token
|
||||
await prisma.token.update({
|
||||
where: { id: tokenId },
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
|
||||
const result = JSON.stringify(["ok"]);
|
||||
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import { NDKKind, NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import AdminInterface from "../index.js";
|
||||
import prisma from "../../../db.js";
|
||||
|
||||
@ -20,5 +20,5 @@ export default async function revokeUser(admin: AdminInterface, req: NDKRpcReque
|
||||
});
|
||||
|
||||
const result = JSON.stringify(["ok"]);
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, 24134);
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
|
||||
114
src/daemon/admin/commands/rotate_key.ts
Normal file
114
src/daemon/admin/commands/rotate_key.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { NDKKind, NDKPrivateKeySigner, NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import AdminInterface from "../index.js";
|
||||
import prisma from "../../../db.js";
|
||||
import { saveEncrypted } from "../../../commands/add.js";
|
||||
import { hexToBytes } from "../../../utils/hex.js";
|
||||
|
||||
export default async function rotateKey(admin: AdminInterface, req: NDKRpcRequest) {
|
||||
const [oldKeyName, newKeyName, passphrase] = req.params as [string, string, string];
|
||||
|
||||
if (!oldKeyName || !newKeyName || !passphrase) {
|
||||
throw new Error("Invalid params: oldKeyName, newKeyName, and passphrase required");
|
||||
}
|
||||
|
||||
if (!admin.loadNsec) throw new Error("No loadNsec method");
|
||||
|
||||
// Validate old key exists
|
||||
const oldKey = await prisma.key.findUnique({
|
||||
where: { keyName: oldKeyName },
|
||||
});
|
||||
|
||||
if (!oldKey) {
|
||||
throw new Error(`Key '${oldKeyName}' not found`);
|
||||
}
|
||||
|
||||
if (oldKey.deletedAt) {
|
||||
throw new Error(`Key '${oldKeyName}' is already deleted`);
|
||||
}
|
||||
|
||||
// Check new key name doesn't exist
|
||||
const existingNewKey = await prisma.key.findUnique({
|
||||
where: { keyName: newKeyName },
|
||||
});
|
||||
|
||||
if (existingNewKey) {
|
||||
throw new Error(`Key '${newKeyName}' already exists`);
|
||||
}
|
||||
|
||||
// Generate new key
|
||||
const newSigner = NDKPrivateKeySigner.generate();
|
||||
const newUser = await newSigner.user();
|
||||
const newNsec = nip19.nsecEncode(hexToBytes(newSigner.privateKey!));
|
||||
|
||||
// Save new key encrypted
|
||||
await saveEncrypted(
|
||||
admin.configFile,
|
||||
newNsec,
|
||||
passphrase,
|
||||
newKeyName
|
||||
);
|
||||
|
||||
// Create new Key record in database
|
||||
await prisma.key.create({
|
||||
data: {
|
||||
keyName: newKeyName,
|
||||
pubkey: newUser.pubkey,
|
||||
},
|
||||
});
|
||||
|
||||
// Copy KeyUser records from old key to new key
|
||||
const oldKeyUsers = await prisma.keyUser.findMany({
|
||||
where: { keyName: oldKeyName },
|
||||
include: { signingConditions: true },
|
||||
});
|
||||
|
||||
for (const oldKeyUser of oldKeyUsers) {
|
||||
// Create new KeyUser for the new key
|
||||
const newKeyUser = await prisma.keyUser.create({
|
||||
data: {
|
||||
keyName: newKeyName,
|
||||
userPubkey: oldKeyUser.userPubkey,
|
||||
description: oldKeyUser.description,
|
||||
},
|
||||
});
|
||||
|
||||
// Copy signing conditions
|
||||
for (const condition of oldKeyUser.signingConditions) {
|
||||
await prisma.signingCondition.create({
|
||||
data: {
|
||||
keyUserId: newKeyUser.id,
|
||||
method: condition.method,
|
||||
kind: condition.kind,
|
||||
content: condition.content,
|
||||
allowed: condition.allowed,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Soft-delete old key
|
||||
await prisma.key.update({
|
||||
where: { keyName: oldKeyName },
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
|
||||
// Also soft-delete tokens for old key (tokens are key-specific, not transferred)
|
||||
await prisma.token.updateMany({
|
||||
where: {
|
||||
keyName: oldKeyName,
|
||||
deletedAt: null,
|
||||
},
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
|
||||
// Load the new key into the daemon
|
||||
await admin.loadNsec(newKeyName, newNsec);
|
||||
|
||||
const result = JSON.stringify({
|
||||
npub: newUser.npub,
|
||||
name: newKeyName,
|
||||
});
|
||||
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import { NDKKind, NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import AdminInterface from "../index.js";
|
||||
|
||||
export default async function unlockKey(admin: AdminInterface, req: NDKRpcRequest) {
|
||||
@ -16,5 +16,5 @@ export default async function unlockKey(admin: AdminInterface, req: NDKRpcReques
|
||||
result = JSON.stringify({ success: false, error: e.message });
|
||||
}
|
||||
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, 24134);
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
56
src/daemon/admin/commands/validate_token.ts
Normal file
56
src/daemon/admin/commands/validate_token.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { NDKKind, NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import AdminInterface from "../index.js";
|
||||
import prisma from "../../../db.js";
|
||||
|
||||
export default async function validateToken(admin: AdminInterface, req: NDKRpcRequest) {
|
||||
const [tokenString] = req.params as [string];
|
||||
|
||||
if (!tokenString) throw new Error("Invalid params: token required");
|
||||
|
||||
// Parse token string - may include npub# prefix
|
||||
let tokenValue = tokenString;
|
||||
if (tokenString.includes('#')) {
|
||||
tokenValue = tokenString.split('#')[1];
|
||||
}
|
||||
|
||||
const token = await prisma.token.findUnique({
|
||||
where: { token: tokenValue },
|
||||
});
|
||||
|
||||
if (!token) {
|
||||
const result = JSON.stringify({
|
||||
valid: false,
|
||||
reason: "Token not found",
|
||||
});
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
|
||||
// Check if token is revoked (soft deleted)
|
||||
if (token.deletedAt) {
|
||||
const result = JSON.stringify({
|
||||
valid: false,
|
||||
reason: "Token has been revoked",
|
||||
});
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
|
||||
// Check if token is expired
|
||||
if (token.expiresAt && token.expiresAt < new Date()) {
|
||||
const result = JSON.stringify({
|
||||
valid: false,
|
||||
reason: "Token has expired",
|
||||
});
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
|
||||
// Token is valid
|
||||
const result = JSON.stringify({
|
||||
valid: true,
|
||||
key_name: token.keyName,
|
||||
client_name: token.clientName,
|
||||
expires_at: token.expiresAt,
|
||||
redeemed: token.redeemedAt !== null,
|
||||
});
|
||||
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
@ -13,6 +13,17 @@ import createNewToken from './commands/create_new_token';
|
||||
import unlockKey from './commands/unlock_key';
|
||||
import renameKeyUser from './commands/rename_key_user.js';
|
||||
import revokeUser from './commands/revoke_user';
|
||||
import getKey from './commands/get_key';
|
||||
import deleteKey from './commands/delete_key';
|
||||
import rotateKey from './commands/rotate_key';
|
||||
import getPolicy from './commands/get_policy';
|
||||
import deletePolicy from './commands/delete_policy';
|
||||
import grantPermission from './commands/grant_permission';
|
||||
import revokePermission from './commands/revoke_permission';
|
||||
import getPermissions from './commands/get_permissions';
|
||||
import getToken from './commands/get_token';
|
||||
import revokeToken from './commands/revoke_token';
|
||||
import validateToken from './commands/validate_token';
|
||||
import fs from 'fs';
|
||||
import { validateRequestFromAdmin } from './validations/request-from-admin';
|
||||
import { dmUser } from '../../utils/dm-user';
|
||||
@ -116,7 +127,7 @@ class AdminInterface {
|
||||
this.ndk.connect(2500).then(() => {
|
||||
// connect for whitelisted admins
|
||||
this.rpc.subscribe({
|
||||
"kinds": [NDKKind.NostrConnect, 24134 as number],
|
||||
"kinds": [NDKKind.NostrConnect],
|
||||
"#p": [this.signerUser!.pubkey]
|
||||
});
|
||||
|
||||
@ -135,17 +146,28 @@ class AdminInterface {
|
||||
|
||||
switch (req.method) {
|
||||
case 'get_keys': await this.reqGetKeys(req); break;
|
||||
case 'get_key': await getKey(this, req); break;
|
||||
case 'get_key_users': await this.reqGetKeyUsers(req); break;
|
||||
case 'rename_key_user': await renameKeyUser(this, req); break;
|
||||
case 'get_key_tokens': await this.reqGetKeyTokens(req); break;
|
||||
case 'revoke_user': await revokeUser(this, req); break;
|
||||
case 'create_new_key': await createNewKey(this, req); break;
|
||||
case 'delete_key': await deleteKey(this, req); break;
|
||||
case 'rotate_key': await rotateKey(this, req); break;
|
||||
case 'create_account': await createAccount(this, req); break;
|
||||
case 'ping': await ping(this, req); break;
|
||||
case 'unlock_key': await unlockKey(this, req); break;
|
||||
case 'create_new_policy': await createNewPolicy(this, req); break;
|
||||
case 'get_policies': await this.reqListPolicies(req); break;
|
||||
case 'get_policy': await getPolicy(this, req); break;
|
||||
case 'delete_policy': await deletePolicy(this, req); break;
|
||||
case 'grant_permission': await grantPermission(this, req); break;
|
||||
case 'revoke_permission': await revokePermission(this, req); break;
|
||||
case 'get_permissions': await getPermissions(this, req); break;
|
||||
case 'create_new_token': await createNewToken(this, req); break;
|
||||
case 'get_token': await getToken(this, req); break;
|
||||
case 'revoke_token': await revokeToken(this, req); break;
|
||||
case 'validate_token': await validateToken(this, req); break;
|
||||
default:
|
||||
const originalKind = req.event.kind!;
|
||||
console.log(`Unknown method ${req.method}`);
|
||||
@ -158,7 +180,7 @@ class AdminInterface {
|
||||
}
|
||||
} catch (err: any) {
|
||||
debug(`Error handling request ${req.method}: ${err?.message??err}`, req.params);
|
||||
return this.rpc.sendResponse(req.id, req.pubkey, "error", NDKKind.NostrConnectAdmin, err?.message);
|
||||
return this.rpc.sendResponse(req.id, req.pubkey, "error", NDKKind.NostrConnect, err?.message);
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,7 +218,7 @@ class AdminInterface {
|
||||
const key = keys.find((k) => k.name === keyName);
|
||||
|
||||
if (!key || !key.npub) {
|
||||
return this.rpc.sendResponse(req.id, req.pubkey, JSON.stringify([]), 24134);
|
||||
return this.rpc.sendResponse(req.id, req.pubkey, JSON.stringify([]), NDKKind.NostrConnect);
|
||||
}
|
||||
|
||||
const npub = key.npub;
|
||||
@ -218,7 +240,7 @@ class AdminInterface {
|
||||
};
|
||||
}));
|
||||
|
||||
return this.rpc.sendResponse(req.id, req.pubkey, result, 24134);
|
||||
return this.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -250,7 +272,7 @@ class AdminInterface {
|
||||
};
|
||||
}));
|
||||
|
||||
return this.rpc.sendResponse(req.id, req.pubkey, result, 24134);
|
||||
return this.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -262,7 +284,7 @@ class AdminInterface {
|
||||
const result = JSON.stringify(await this.getKeys());
|
||||
const pubkey = req.pubkey;
|
||||
|
||||
return this.rpc.sendResponse(req.id, pubkey, result, 24134); // 24134
|
||||
return this.rpc.sendResponse(req.id, pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -274,7 +296,7 @@ class AdminInterface {
|
||||
const result = JSON.stringify(await this.getKeyUsers(req));
|
||||
const pubkey = req.pubkey;
|
||||
|
||||
return this.rpc.sendResponse(req.id, pubkey, result, 24134); // 24134
|
||||
return this.rpc.sendResponse(req.id, pubkey, result, NDKKind.NostrConnect);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -340,7 +362,7 @@ class AdminInterface {
|
||||
remoteUser.pubkey,
|
||||
'acl',
|
||||
[params],
|
||||
24134,
|
||||
NDKKind.NostrConnect,
|
||||
(res: NDKRpcResponse) => {
|
||||
this.requestPermissionResponse(
|
||||
remotePubkey,
|
||||
|
||||
@ -59,7 +59,7 @@ async function createRecord(
|
||||
) {
|
||||
let params: string | undefined;
|
||||
|
||||
if (param?.rawEvent) {
|
||||
if (param && typeof param !== 'string' && 'rawEvent' in param) {
|
||||
const e = param as NDKEvent;
|
||||
params = JSON.stringify(e.rawEvent());
|
||||
} else if (param) {
|
||||
@ -113,7 +113,7 @@ export function urlAuthFlow(
|
||||
clearInterval(checkingInterval);
|
||||
|
||||
if (record.allowed === false) {
|
||||
reject(record.payload);
|
||||
reject(record.params);
|
||||
}
|
||||
console.log('resolve urlAuthFlow', !!record.params);
|
||||
resolve(record.params);
|
||||
|
||||
@ -3,7 +3,7 @@ import { IEventHandlingStrategy } from '@nostr-dev-kit/ndk';
|
||||
|
||||
export default class PublishEventHandlingStrategy implements IEventHandlingStrategy {
|
||||
async handle(backend: NDKNip46Backend, id: string, remotePubkey: string, params: string[]): Promise<string|undefined> {
|
||||
const event = await backend.signEvent(remotePubkey, params);
|
||||
const event = await (backend as any).signEvent(remotePubkey, params);
|
||||
if (!event) return undefined;
|
||||
|
||||
console.log('Publishing event', event);
|
||||
|
||||
@ -79,7 +79,8 @@ export function requestToSigningConditionQuery(method: IMethod, payload?: string
|
||||
|
||||
switch (method) {
|
||||
case 'sign_event':
|
||||
signingConditionQuery.kind = { in: [ payload?.kind?.toString(), 'all' ] };
|
||||
const kind = typeof payload === 'object' && payload !== null ? (payload as NostrEvent).kind : undefined;
|
||||
signingConditionQuery.kind = { in: [ kind?.toString(), 'all' ] };
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@ -39,7 +39,8 @@ function getKeys(config: DaemonConfig) {
|
||||
const keys: Key[] = [];
|
||||
|
||||
for (const [name, nsec] of Object.entries(config.keys)) {
|
||||
const hexpk = bytesToHex(nip19.decode(nsec).data as Uint8Array);
|
||||
const decoded = nip19.decode(nsec) as unknown as { type: 'nsec', data: Uint8Array };
|
||||
const hexpk = bytesToHex(decoded.data);
|
||||
const user = await new NDKPrivateKeySigner(hexpk).user();
|
||||
const key = {
|
||||
name,
|
||||
@ -165,7 +166,7 @@ class Daemon {
|
||||
explicitRelayUrls: config.nostr.relays,
|
||||
});
|
||||
this.ndk.pool.on('relay:connect', (r) => console.log(`✅ Connected to ${r.url}`) );
|
||||
this.ndk.pool.on('relay:notice', (n, r) => { console.log(`👀 Notice from ${r.url}`, n); });
|
||||
(this.ndk.pool as any).on('relay:notice', (n: string, r: any) => { console.log(`👀 Notice from ${r.url}`, n); });
|
||||
|
||||
this.ndk.pool.on('relay:disconnect', (r) => {
|
||||
console.log(`🚫 Disconnected from ${r.url}`);
|
||||
|
||||
@ -17,7 +17,7 @@ async function validateAuthCookie(request) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { pubkey: jwt }
|
||||
});
|
||||
|
||||
@ -230,7 +230,7 @@ export async function processRegistrationWebHandler(request, reply) {
|
||||
|
||||
await allowAllRequestsFromKey(
|
||||
record.remotePubkey,
|
||||
record.keyName,
|
||||
record.keyName!,
|
||||
record.method,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
15
vitest.config.ts
Normal file
15
vitest.config.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['src/**/*.test.ts'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
include: ['src/daemon/admin/commands/**/*.ts'],
|
||||
exclude: ['**/*.test.ts'],
|
||||
},
|
||||
},
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user