mirror of
https://github.com/kind-0/nsecbunkerd.git
synced 2026-05-03 07:00:11 +00:00
Merge 9fc020126953a7598ee331c1445ba88f81774339 into f4fd7403ccf1b479c9d717210a8e4768081d75a7
This commit is contained in:
commit
d91bfc392c
2
.gitignore
vendored
2
.gitignore
vendored
@ -9,3 +9,5 @@ config
|
|||||||
.env
|
.env
|
||||||
.turbo
|
.turbo
|
||||||
prisma
|
prisma
|
||||||
|
/.claude/
|
||||||
|
/.idea/
|
||||||
|
|||||||
46
Dockerfile
46
Dockerfile
@ -1,34 +1,62 @@
|
|||||||
|
# Build stage - needs the ndk/core directory for local file dependency
|
||||||
FROM node:20.11-bullseye AS build
|
FROM node:20.11-bullseye AS build
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy package files and install dependencies
|
# Copy pre-built ndk/core for the local file dependency
|
||||||
COPY package*.json ./
|
# ndk/core must already be built locally (has dist/ directory)
|
||||||
|
# Also copy ndk/node_modules which contains ndk/core's hoisted dependencies (tseep, etc.)
|
||||||
|
COPY ndk/core /ndk/core
|
||||||
|
COPY ndk/node_modules /ndk/node_modules
|
||||||
|
|
||||||
|
# Remove the prepare script from ndk/core to prevent it from trying to rebuild
|
||||||
|
RUN sed -i 's/"prepare": "bun run build",//' /ndk/core/package.json
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY nsecbunkerd/package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
# Copy application files
|
# Copy application files
|
||||||
COPY . .
|
COPY nsecbunkerd/ .
|
||||||
|
|
||||||
# Generate prisma client and build the application
|
# Generate prisma client and build the application
|
||||||
RUN npx prisma generate
|
RUN npx prisma generate
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
# Prune dev dependencies to reduce image size
|
||||||
|
RUN npm prune --production
|
||||||
|
|
||||||
# Runtime stage
|
# Runtime stage
|
||||||
FROM node:20.11-alpine as runtime
|
FROM node:20.11-alpine AS runtime
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add --no-cache openssl && \
|
apk add --no-cache openssl curl && \
|
||||||
rm -rf /var/cache/apk/*
|
rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
# Copy built files from the build stage
|
# Copy the ndk/core dependency and its hoisted node_modules
|
||||||
COPY --from=build /app .
|
COPY --from=build /ndk/core /ndk/core
|
||||||
|
COPY --from=build /ndk/node_modules /ndk/node_modules
|
||||||
|
|
||||||
# Install only runtime dependencies
|
# Copy built application with production node_modules from build stage
|
||||||
RUN npm install --only=production
|
COPY --from=build /app/dist ./dist
|
||||||
|
COPY --from=build /app/node_modules ./node_modules
|
||||||
|
COPY --from=build /app/package.json ./package.json
|
||||||
|
COPY --from=build /app/prisma ./prisma
|
||||||
|
|
||||||
|
# Copy scripts directory if it exists
|
||||||
|
COPY --from=build /app/scripts ./scripts
|
||||||
|
|
||||||
|
# Create config directory for runtime configuration
|
||||||
|
RUN mkdir -p /app/config
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# No HTTP healthcheck - nsecbunkerd doesn't expose a health endpoint
|
||||||
|
# Docker Swarm will rely on process status
|
||||||
|
|
||||||
ENTRYPOINT [ "node", "./dist/index.js" ]
|
ENTRYPOINT [ "node", "./dist/index.js" ]
|
||||||
CMD ["start"]
|
CMD ["start"]
|
||||||
|
|||||||
44
Dockerfile.local
Normal file
44
Dockerfile.local
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
FROM node:20.11-bullseye AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install pnpm
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json pnpm-lock.yaml ./
|
||||||
|
|
||||||
|
# Replace workspace:* with actual npm version for @nostr-dev-kit/ndk
|
||||||
|
RUN sed -i 's/"@nostr-dev-kit\/ndk": "workspace:\*"/"@nostr-dev-kit\/ndk": "^2.10.0"/' package.json
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN pnpm install --no-frozen-lockfile
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Generate prisma client and build the application
|
||||||
|
RUN npx prisma generate
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
# Runtime stage
|
||||||
|
FROM node:20.11-alpine as runtime
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apk update && \
|
||||||
|
apk add --no-cache openssl && \
|
||||||
|
rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
# Copy built files from the build stage
|
||||||
|
COPY --from=build /app .
|
||||||
|
|
||||||
|
# Install only runtime dependencies
|
||||||
|
RUN npm install -g pnpm && \
|
||||||
|
sed -i 's/"@nostr-dev-kit\/ndk": "workspace:\*"/"@nostr-dev-kit\/ndk": "^2.10.0"/' package.json && \
|
||||||
|
pnpm install --prod --no-frozen-lockfile
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENTRYPOINT [ "node", "./dist/index.js" ]
|
||||||
|
CMD ["start"]
|
||||||
120
README.md
120
README.md
@ -177,6 +177,126 @@ 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 | Since |
|
||||||
|
|--------|------------|-------------|-------|
|
||||||
|
| `get_keys` | none | List all keys with their status (locked/unlocked) | |
|
||||||
|
| `get_key` | `keyName` | Get details about a specific key | v0.11.0 |
|
||||||
|
| `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 | v0.11.0 |
|
||||||
|
| `rotate_key` | `oldKeyName`, `newKeyName`, `passphrase` | Create new key and migrate permissions | v0.11.0 |
|
||||||
|
|
||||||
|
### Policy Management
|
||||||
|
|
||||||
|
| Method | Parameters | Description | Since |
|
||||||
|
|--------|------------|-------------|-------|
|
||||||
|
| `get_policies` | none | List all policies with their rules | |
|
||||||
|
| `get_policy` | `policyId` | Get details about a specific policy | v0.11.0 |
|
||||||
|
| `create_new_policy` | `policyJson` | Create a new policy with rules | |
|
||||||
|
| `delete_policy` | `policyId` | Soft-delete a policy | v0.11.0 |
|
||||||
|
|
||||||
|
**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 | Since |
|
||||||
|
|--------|------------|-------------|-------|
|
||||||
|
| `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 | v0.11.0 |
|
||||||
|
| `revoke_permission` | `keyName`, `userPubkey` | Revoke user's access to a key | v0.11.0 |
|
||||||
|
| `get_permissions` | `keyName`, `userPubkey` | Get user's permissions on a key | v0.11.0 |
|
||||||
|
| `rename_key_user` | `userPubkey`, `description` | Update a user's description | |
|
||||||
|
| `revoke_user` | `keyUserId` | Revoke user by ID | |
|
||||||
|
|
||||||
|
### Token Management
|
||||||
|
|
||||||
|
| Method | Parameters | Description | Since |
|
||||||
|
|--------|------------|-------------|-------|
|
||||||
|
| `get_key_tokens` | `keyName` | List all tokens for a key | |
|
||||||
|
| `get_token` | `tokenId` | Get details about a specific token | v0.11.0 |
|
||||||
|
| `create_new_token` | `keyName`, `clientName`, `policyId`, `[durationInHours]` | Create an access token | |
|
||||||
|
| `revoke_token` | `tokenId` | Revoke (soft-delete) a token | v0.11.0 |
|
||||||
|
| `validate_token` | `tokenString` | Check if a token is valid | v0.11.0 |
|
||||||
|
|
||||||
|
### Utility
|
||||||
|
|
||||||
|
| Method | Parameters | Description | Since |
|
||||||
|
|--------|------------|-------------|-------|
|
||||||
|
| `ping` | none | Health check - returns "pong" | |
|
||||||
|
| `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
|
||||||
|
|
||||||
|
Unit tests for all admin commands were added in v0.11.0 using Vitest.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Run tests in watch mode
|
||||||
|
npm run test:watch
|
||||||
|
|
||||||
|
# Run tests with coverage
|
||||||
|
npm run test:coverage
|
||||||
|
```
|
||||||
|
|
||||||
# Authors
|
# Authors
|
||||||
|
|
||||||
* [pablof7z](nostr:npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft)
|
* [pablof7z](nostr:npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft)
|
||||||
|
|||||||
51
nostr-tools-v2-patch.md
Normal file
51
nostr-tools-v2-patch.md
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Patch to upgrade nsecbunkerd to nostr-tools v2
|
||||||
|
|
||||||
|
## Step 1: Update package.json
|
||||||
|
Change: "nostr-tools": "^1.17.0" → "nostr-tools": "^2.17.0"
|
||||||
|
|
||||||
|
## Step 2: Add hex conversion utility
|
||||||
|
Create file: src/utils/hex.ts
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function bytesToHex(bytes: Uint8Array): string {
|
||||||
|
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hexToBytes(hex: string): Uint8Array {
|
||||||
|
const bytes = new Uint8Array(hex.length / 2);
|
||||||
|
for (let i = 0; i < hex.length; i += 2) {
|
||||||
|
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: Patch files that decode nsec
|
||||||
|
|
||||||
|
### src/daemon/run.ts (line ~41)
|
||||||
|
```diff
|
||||||
|
+import { bytesToHex } from '../utils/hex.js';
|
||||||
|
...
|
||||||
|
-const hexpk = nip19.decode(nsec).data as string;
|
||||||
|
+const hexpk = bytesToHex(nip19.decode(nsec).data as Uint8Array);
|
||||||
|
```
|
||||||
|
|
||||||
|
### src/commands/add.ts (line ~37)
|
||||||
|
```diff
|
||||||
|
+import { bytesToHex } from '../utils/hex.js';
|
||||||
|
...
|
||||||
|
-decoded = nip19.decode(nsec);
|
||||||
|
+const decoded = nip19.decode(nsec);
|
||||||
|
+const hexpk = decoded.type === 'nsec' ? bytesToHex(decoded.data as Uint8Array) : decoded.data;
|
||||||
|
```
|
||||||
|
|
||||||
|
### src/daemon/admin/commands/create_new_key.ts (line ~16)
|
||||||
|
```diff
|
||||||
|
+import { bytesToHex } from '../../../utils/hex.js';
|
||||||
|
...
|
||||||
|
-key = new NDKPrivateKeySigner(nip19.decode(_nsec).data as string);
|
||||||
|
+key = new NDKPrivateKeySigner(bytesToHex(nip19.decode(_nsec).data as Uint8Array));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Note
|
||||||
|
npubEncode/decode still uses hex strings in v2, so request-from-admin.ts works unchanged.
|
||||||
8935
package-lock.json
generated
Normal file
8935
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nsecbunkerd",
|
"name": "nsecbunkerd",
|
||||||
"version": "0.10.5",
|
"version": "0.11.2",
|
||||||
"description": "nsecbunker daemon",
|
"description": "nsecbunker daemon",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -27,7 +27,10 @@
|
|||||||
"start": "node ./scripts/start.js",
|
"start": "node ./scripts/start.js",
|
||||||
"lfg": "node ./scripts/start.js start",
|
"lfg": "node ./scripts/start.js start",
|
||||||
"nsecbunkerd": "node dist/index.js",
|
"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": [
|
"keywords": [
|
||||||
"nostr"
|
"nostr"
|
||||||
@ -39,7 +42,7 @@
|
|||||||
"@fastify/view": "^8.2.0",
|
"@fastify/view": "^8.2.0",
|
||||||
"@inquirer/password": "^1.1.2",
|
"@inquirer/password": "^1.1.2",
|
||||||
"@inquirer/prompts": "^1.2.3",
|
"@inquirer/prompts": "^1.2.3",
|
||||||
"@nostr-dev-kit/ndk": "workspace:*",
|
"@nostr-dev-kit/ndk": "file:../ndk/core",
|
||||||
"@prisma/client": "^5.4.1",
|
"@prisma/client": "^5.4.1",
|
||||||
"@scure/base": "^1.1.1",
|
"@scure/base": "^1.1.1",
|
||||||
"@types/yargs": "^17.0.24",
|
"@types/yargs": "^17.0.24",
|
||||||
@ -57,7 +60,7 @@
|
|||||||
"isomorphic-ws": "^5.0.0",
|
"isomorphic-ws": "^5.0.0",
|
||||||
"lnbits": "^1.1.5",
|
"lnbits": "^1.1.5",
|
||||||
"lnbits-ts": "^0.0.2",
|
"lnbits-ts": "^0.0.2",
|
||||||
"nostr-tools": "^1.17.0",
|
"nostr-tools": "^2.17.0",
|
||||||
"websocket-polyfill": "^0.0.3",
|
"websocket-polyfill": "^0.0.3",
|
||||||
"ws": "^8.13.0",
|
"ws": "^8.13.0",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
@ -68,6 +71,7 @@
|
|||||||
"prisma": "^5.4.1",
|
"prisma": "^5.4.1",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tsup": "^7.2.0",
|
"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
@ -100,7 +100,7 @@ function loadPrivateKey(): string | undefined {
|
|||||||
} else {
|
} else {
|
||||||
// check if we have a @ so we try to get the npub from nip05
|
// check if we have a @ so we try to get the npub from nip05
|
||||||
if (remotePubkey.includes('@')) {
|
if (remotePubkey.includes('@')) {
|
||||||
const u = await NDKUser.fromNip05(remotePubkey);
|
const u = await NDKUser.fromNip05(remotePubkey, ndk);
|
||||||
if (!u) {
|
if (!u) {
|
||||||
console.log(`Invalid nip05 ${remotePubkey}`);
|
console.log(`Invalid nip05 ${remotePubkey}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
97
src/daemon/admin/commands/__tests__/create_new_key.test.ts
Normal file
97
src/daemon/admin/commands/__tests__/create_new_key.test.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { createMockAdmin, createMockRequest, getResponseResult, resetMocks, mockPrisma } 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(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the prisma module
|
||||||
|
vi.mock('../../../../db.js', () => ({
|
||||||
|
default: mockPrisma,
|
||||||
|
}));
|
||||||
|
|
||||||
|
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 key was saved to database
|
||||||
|
expect(mockPrisma.key.upsert).toHaveBeenCalledWith({
|
||||||
|
where: { keyName: 'my-key' },
|
||||||
|
update: {
|
||||||
|
pubkey: expect.any(String),
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
keyName: 'my-key',
|
||||||
|
pubkey: expect.any(String),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 pong', 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,
|
||||||
|
'pong',
|
||||||
|
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/);
|
||||||
|
});
|
||||||
|
});
|
||||||
109
src/daemon/admin/commands/__tests__/test-utils.ts
Normal file
109
src/daemon/admin/commands/__tests__/test-utils.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
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(),
|
||||||
|
upsert: 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { Hexpubkey, NDKKind, NDKPrivateKeySigner, NDKRpcRequest, NDKUserProfile } from "@nostr-dev-kit/ndk";
|
import { Hexpubkey, NDKKind, NDKPrivateKeySigner, NDKRpcRequest, NDKUserProfile } from "@nostr-dev-kit/ndk";
|
||||||
import AdminInterface from "..";
|
import AdminInterface from "..";
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
|
import { hexToBytes } from "../../../utils/hex.js";
|
||||||
import { setupSkeletonProfile } from "../../lib/profile";
|
import { setupSkeletonProfile } from "../../lib/profile";
|
||||||
import { IConfig, getCurrentConfig, saveCurrentConfig } from "../../../config";
|
import { IConfig, getCurrentConfig, saveCurrentConfig } from "../../../config";
|
||||||
import { readFileSync, writeFileSync } from "fs";
|
import { readFileSync, writeFileSync } from "fs";
|
||||||
@ -131,7 +132,7 @@ export default async function createAccount(admin: AdminInterface, req: NDKRpcRe
|
|||||||
username = payload[0];
|
username = payload[0];
|
||||||
domain = payload[1];
|
domain = payload[1];
|
||||||
email = payload[2];
|
email = payload[2];
|
||||||
return createAccountReal(admin, req, username, domain, email);
|
return createAccountReal(admin, req, username!, domain!, email);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,7 +196,7 @@ export async function createAccountReal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const keyName = nip05;
|
const keyName = nip05;
|
||||||
const nsec = nip19.nsecEncode(key.privateKey!);
|
const nsec = nip19.nsecEncode(hexToBytes(key.privateKey!));
|
||||||
currentConfig.keys[keyName] = { key: key.privateKey };
|
currentConfig.keys[keyName] = { key: key.privateKey };
|
||||||
|
|
||||||
saveCurrentConfig(admin.configFile, currentConfig);
|
saveCurrentConfig(admin.configFile, currentConfig);
|
||||||
@ -209,10 +210,10 @@ export async function createAccountReal(
|
|||||||
// access it without having to go through an approval flow
|
// access it without having to go through an approval flow
|
||||||
await grantPermissions(req, keyName);
|
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) {
|
} catch (e: any) {
|
||||||
console.trace('error', e);
|
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);
|
e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
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 AdminInterface from "../index.js";
|
||||||
import { saveEncrypted } from "../../../commands/add.js";
|
import { saveEncrypted } from "../../../commands/add.js";
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { setupSkeletonProfile } from "../../lib/profile.js";
|
import { setupSkeletonProfile } from "../../lib/profile.js";
|
||||||
|
import { bytesToHex, hexToBytes } from "../../../utils/hex.js";
|
||||||
|
import prisma from "../../../db.js";
|
||||||
|
|
||||||
export default async function createNewKey(admin: AdminInterface, req: NDKRpcRequest) {
|
export default async function createNewKey(admin: AdminInterface, req: NDKRpcRequest) {
|
||||||
const [ keyName, passphrase, _nsec ] = req.params as [ string, string, string? ];
|
const [ keyName, passphrase, _nsec ] = req.params as [ string, string, string? ];
|
||||||
@ -13,7 +15,7 @@ export default async function createNewKey(admin: AdminInterface, req: NDKRpcReq
|
|||||||
let key;
|
let key;
|
||||||
|
|
||||||
if (_nsec) {
|
if (_nsec) {
|
||||||
key = new NDKPrivateKeySigner(nip19.decode(_nsec).data as string);
|
key = new NDKPrivateKeySigner(bytesToHex(nip19.decode(_nsec).data as Uint8Array));
|
||||||
} else {
|
} else {
|
||||||
key = NDKPrivateKeySigner.generate();
|
key = NDKPrivateKeySigner.generate();
|
||||||
|
|
||||||
@ -23,7 +25,7 @@ export default async function createNewKey(admin: AdminInterface, req: NDKRpcReq
|
|||||||
}
|
}
|
||||||
|
|
||||||
const user = await key.user();
|
const user = await key.user();
|
||||||
const nsec = nip19.nsecEncode(key.privateKey!);
|
const nsec = nip19.nsecEncode(hexToBytes(key.privateKey!));
|
||||||
|
|
||||||
await saveEncrypted(
|
await saveEncrypted(
|
||||||
admin.configFile,
|
admin.configFile,
|
||||||
@ -34,9 +36,22 @@ export default async function createNewKey(admin: AdminInterface, req: NDKRpcReq
|
|||||||
|
|
||||||
await admin.loadNsec(keyName, nsec);
|
await admin.loadNsec(keyName, nsec);
|
||||||
|
|
||||||
|
// Also save to database so delete_key can find it
|
||||||
|
await prisma.key.upsert({
|
||||||
|
where: { keyName },
|
||||||
|
update: {
|
||||||
|
pubkey: user.pubkey,
|
||||||
|
deletedAt: null, // Ensure it's not marked as deleted
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
keyName,
|
||||||
|
pubkey: user.pubkey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const result = JSON.stringify({
|
const result = JSON.stringify({
|
||||||
npub: user.npub,
|
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 AdminInterface from "../index.js";
|
||||||
import prisma from "../../../db.js";
|
import prisma from "../../../db.js";
|
||||||
|
|
||||||
@ -19,9 +19,9 @@ export default async function createNewPolicy(admin: AdminInterface, req: NDKRpc
|
|||||||
for (const rule of policy.rules) {
|
for (const rule of policy.rules) {
|
||||||
await prisma.policyRule.create({
|
await prisma.policyRule.create({
|
||||||
data: {
|
data: {
|
||||||
policyId: policyRecord.id,
|
Policy: { connect: { id: policyRecord.id } },
|
||||||
kind: rule.kind.toString(),
|
kind: rule.kind != null ? rule.kind.toString() : null,
|
||||||
method: rule.method,
|
method: rule.method ?? "sign_event",
|
||||||
maxUsageCount: rule.use_count,
|
maxUsageCount: rule.use_count,
|
||||||
currentUsageCount: 0,
|
currentUsageCount: 0,
|
||||||
}
|
}
|
||||||
@ -29,5 +29,5 @@ export default async function createNewPolicy(admin: AdminInterface, req: NDKRpc
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = JSON.stringify(["ok"]);
|
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 AdminInterface from "../index.js";
|
||||||
import prisma from "../../../db.js";
|
import prisma from "../../../db.js";
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ export default async function createNewToken(admin: AdminInterface, req: NDKRpcR
|
|||||||
|
|
||||||
const token = [...Array(64)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
|
const token = [...Array(64)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
|
||||||
const data: any = {
|
const data: any = {
|
||||||
keyName, clientName, policyId,
|
keyName, clientName, policyId: parseInt(policyId),
|
||||||
createdBy: req.pubkey,
|
createdBy: req.pubkey,
|
||||||
token
|
token
|
||||||
};
|
};
|
||||||
@ -26,5 +26,5 @@ export default async function createNewToken(admin: AdminInterface, req: NDKRpcR
|
|||||||
if (!tokenRecord) throw new Error("Token not created");
|
if (!tokenRecord) throw new Error("Token not created");
|
||||||
|
|
||||||
const result = JSON.stringify(["ok"]);
|
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);
|
||||||
|
}
|
||||||
51
src/daemon/admin/commands/get_token.ts
Normal file
51
src/daemon/admin/commands/get_token.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
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,
|
||||||
|
revoked: !!token.deletedAt, // Revoked if deletedAt is set
|
||||||
|
});
|
||||||
|
|
||||||
|
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";
|
import AdminInterface from "../index.js";
|
||||||
|
|
||||||
export default async function ping(admin: AdminInterface, req: NDKRpcRequest) {
|
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, "pong", 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 AdminInterface from "../index.js";
|
||||||
import prisma from "../../../db.js";
|
import prisma from "../../../db.js";
|
||||||
|
|
||||||
@ -25,5 +25,5 @@ export default async function renameKeyUser(admin: AdminInterface, req: NDKRpcRe
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = JSON.stringify(["ok"]);
|
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 AdminInterface from "../index.js";
|
||||||
import prisma from "../../../db.js";
|
import prisma from "../../../db.js";
|
||||||
|
|
||||||
@ -20,5 +20,5 @@ export default async function revokeUser(admin: AdminInterface, req: NDKRpcReque
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = JSON.stringify(["ok"]);
|
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";
|
import AdminInterface from "../index.js";
|
||||||
|
|
||||||
export default async function unlockKey(admin: AdminInterface, req: NDKRpcRequest) {
|
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 });
|
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 unlockKey from './commands/unlock_key';
|
||||||
import renameKeyUser from './commands/rename_key_user.js';
|
import renameKeyUser from './commands/rename_key_user.js';
|
||||||
import revokeUser from './commands/revoke_user';
|
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 fs from 'fs';
|
||||||
import { validateRequestFromAdmin } from './validations/request-from-admin';
|
import { validateRequestFromAdmin } from './validations/request-from-admin';
|
||||||
import { dmUser } from '../../utils/dm-user';
|
import { dmUser } from '../../utils/dm-user';
|
||||||
@ -116,7 +127,7 @@ class AdminInterface {
|
|||||||
this.ndk.connect(2500).then(() => {
|
this.ndk.connect(2500).then(() => {
|
||||||
// connect for whitelisted admins
|
// connect for whitelisted admins
|
||||||
this.rpc.subscribe({
|
this.rpc.subscribe({
|
||||||
"kinds": [NDKKind.NostrConnect, 24134 as number],
|
"kinds": [NDKKind.NostrConnect],
|
||||||
"#p": [this.signerUser!.pubkey]
|
"#p": [this.signerUser!.pubkey]
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -135,17 +146,28 @@ class AdminInterface {
|
|||||||
|
|
||||||
switch (req.method) {
|
switch (req.method) {
|
||||||
case 'get_keys': await this.reqGetKeys(req); break;
|
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 'get_key_users': await this.reqGetKeyUsers(req); break;
|
||||||
case 'rename_key_user': await renameKeyUser(this, req); break;
|
case 'rename_key_user': await renameKeyUser(this, req); break;
|
||||||
case 'get_key_tokens': await this.reqGetKeyTokens(req); break;
|
case 'get_key_tokens': await this.reqGetKeyTokens(req); break;
|
||||||
case 'revoke_user': await revokeUser(this, req); break;
|
case 'revoke_user': await revokeUser(this, req); break;
|
||||||
case 'create_new_key': await createNewKey(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 'create_account': await createAccount(this, req); break;
|
||||||
case 'ping': await ping(this, req); break;
|
case 'ping': await ping(this, req); break;
|
||||||
case 'unlock_key': await unlockKey(this, req); break;
|
case 'unlock_key': await unlockKey(this, req); break;
|
||||||
case 'create_new_policy': await createNewPolicy(this, req); break;
|
case 'create_new_policy': await createNewPolicy(this, req); break;
|
||||||
case 'get_policies': await this.reqListPolicies(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 '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:
|
default:
|
||||||
const originalKind = req.event.kind!;
|
const originalKind = req.event.kind!;
|
||||||
console.log(`Unknown method ${req.method}`);
|
console.log(`Unknown method ${req.method}`);
|
||||||
@ -158,7 +180,7 @@ class AdminInterface {
|
|||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
debug(`Error handling request ${req.method}: ${err?.message??err}`, req.params);
|
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);
|
const key = keys.find((k) => k.name === keyName);
|
||||||
|
|
||||||
if (!key || !key.npub) {
|
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;
|
const npub = key.npub;
|
||||||
@ -205,6 +227,7 @@ class AdminInterface {
|
|||||||
return {
|
return {
|
||||||
id: t.id,
|
id: t.id,
|
||||||
key_name: t.keyName,
|
key_name: t.keyName,
|
||||||
|
key_npub: npub,
|
||||||
client_name: t.clientName,
|
client_name: t.clientName,
|
||||||
token: [ npub, t.token ].join('#'),
|
token: [ npub, t.token ].join('#'),
|
||||||
policy_id: t.policyId,
|
policy_id: t.policyId,
|
||||||
@ -215,10 +238,11 @@ class AdminInterface {
|
|||||||
redeemed_at: t.redeemedAt,
|
redeemed_at: t.redeemedAt,
|
||||||
redeemed_by: t.KeyUser?.description,
|
redeemed_by: t.KeyUser?.description,
|
||||||
time_until_expiration: t.expiresAt ? (t.expiresAt.getTime() - Date.now()) / 1000 : null,
|
time_until_expiration: t.expiresAt ? (t.expiresAt.getTime() - Date.now()) / 1000 : null,
|
||||||
|
revoked: !!t.deletedAt, // Revoked if deletedAt is set
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return this.rpc.sendResponse(req.id, req.pubkey, result, 24134);
|
return this.rpc.sendResponse(req.id, req.pubkey, result, NDKKind.NostrConnect);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -250,7 +274,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 +286,7 @@ class AdminInterface {
|
|||||||
const result = JSON.stringify(await this.getKeys());
|
const result = JSON.stringify(await this.getKeys());
|
||||||
const pubkey = req.pubkey;
|
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 +298,7 @@ class AdminInterface {
|
|||||||
const result = JSON.stringify(await this.getKeyUsers(req));
|
const result = JSON.stringify(await this.getKeyUsers(req));
|
||||||
const pubkey = req.pubkey;
|
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 +364,7 @@ class AdminInterface {
|
|||||||
remoteUser.pubkey,
|
remoteUser.pubkey,
|
||||||
'acl',
|
'acl',
|
||||||
[params],
|
[params],
|
||||||
24134,
|
NDKKind.NostrConnect,
|
||||||
(res: NDKRpcResponse) => {
|
(res: NDKRpcResponse) => {
|
||||||
this.requestPermissionResponse(
|
this.requestPermissionResponse(
|
||||||
remotePubkey,
|
remotePubkey,
|
||||||
|
|||||||
@ -59,7 +59,7 @@ async function createRecord(
|
|||||||
) {
|
) {
|
||||||
let params: string | undefined;
|
let params: string | undefined;
|
||||||
|
|
||||||
if (param?.rawEvent) {
|
if (param && typeof param !== 'string' && 'rawEvent' in param) {
|
||||||
const e = param as NDKEvent;
|
const e = param as NDKEvent;
|
||||||
params = JSON.stringify(e.rawEvent());
|
params = JSON.stringify(e.rawEvent());
|
||||||
} else if (param) {
|
} else if (param) {
|
||||||
@ -113,7 +113,7 @@ export function urlAuthFlow(
|
|||||||
clearInterval(checkingInterval);
|
clearInterval(checkingInterval);
|
||||||
|
|
||||||
if (record.allowed === false) {
|
if (record.allowed === false) {
|
||||||
reject(record.payload);
|
reject(record.params);
|
||||||
}
|
}
|
||||||
console.log('resolve urlAuthFlow', !!record.params);
|
console.log('resolve urlAuthFlow', !!record.params);
|
||||||
resolve(record.params);
|
resolve(record.params);
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { IEventHandlingStrategy } from '@nostr-dev-kit/ndk';
|
|||||||
|
|
||||||
export default class PublishEventHandlingStrategy implements IEventHandlingStrategy {
|
export default class PublishEventHandlingStrategy implements IEventHandlingStrategy {
|
||||||
async handle(backend: NDKNip46Backend, id: string, remotePubkey: string, params: string[]): Promise<string|undefined> {
|
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;
|
if (!event) return undefined;
|
||||||
|
|
||||||
console.log('Publishing event', event);
|
console.log('Publishing event', event);
|
||||||
|
|||||||
@ -79,7 +79,8 @@ export function requestToSigningConditionQuery(method: IMethod, payload?: string
|
|||||||
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case 'sign_event':
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import NDK, { NDKPrivateKeySigner, Nip46PermitCallback, Nip46PermitCallbackParams } from '@nostr-dev-kit/ndk';
|
import NDK, { NDKPrivateKeySigner, Nip46PermitCallback, Nip46PermitCallbackParams } from '@nostr-dev-kit/ndk';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
|
import { bytesToHex, hexToBytes } from '../utils/hex.js';
|
||||||
import { Backend } from './backend/index.js';
|
import { Backend } from './backend/index.js';
|
||||||
import {
|
import {
|
||||||
IMethod,
|
IMethod,
|
||||||
@ -34,11 +35,22 @@ export type KeyUser = {
|
|||||||
|
|
||||||
function getKeys(config: DaemonConfig) {
|
function getKeys(config: DaemonConfig) {
|
||||||
return async (): Promise<Key[]> => {
|
return async (): Promise<Key[]> => {
|
||||||
let lockedKeyNames = Object.keys(config.allKeys);
|
// Get soft-deleted key names from database to exclude them
|
||||||
|
const deletedKeys = await prisma.key.findMany({
|
||||||
|
where: { deletedAt: { not: null } },
|
||||||
|
select: { keyName: true },
|
||||||
|
});
|
||||||
|
const deletedKeyNames = new Set(deletedKeys.map(k => k.keyName));
|
||||||
|
|
||||||
|
let lockedKeyNames = Object.keys(config.allKeys).filter(name => !deletedKeyNames.has(name));
|
||||||
const keys: Key[] = [];
|
const keys: Key[] = [];
|
||||||
|
|
||||||
for (const [name, nsec] of Object.entries(config.keys)) {
|
for (const [name, nsec] of Object.entries(config.keys)) {
|
||||||
const hexpk = nip19.decode(nsec).data as string;
|
// Skip soft-deleted keys
|
||||||
|
if (deletedKeyNames.has(name)) continue;
|
||||||
|
|
||||||
|
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 user = await new NDKPrivateKeySigner(hexpk).user();
|
||||||
const key = {
|
const key = {
|
||||||
name,
|
name,
|
||||||
@ -82,6 +94,7 @@ function getKeyUsers(config: IConfig) {
|
|||||||
createdAt: user.createdAt,
|
createdAt: user.createdAt,
|
||||||
lastUsedAt: user.lastUsedAt || undefined,
|
lastUsedAt: user.lastUsedAt || undefined,
|
||||||
revokedAt: user.revokedAt || undefined,
|
revokedAt: user.revokedAt || undefined,
|
||||||
|
active: !user.revokedAt, // Active if not revoked
|
||||||
signingConditions: user.signingConditions, // Include signing conditions
|
signingConditions: user.signingConditions, // Include signing conditions
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -164,7 +177,7 @@ class Daemon {
|
|||||||
explicitRelayUrls: config.nostr.relays,
|
explicitRelayUrls: config.nostr.relays,
|
||||||
});
|
});
|
||||||
this.ndk.pool.on('relay:connect', (r) => console.log(`✅ Connected to ${r.url}`) );
|
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) => {
|
this.ndk.pool.on('relay:disconnect', (r) => {
|
||||||
console.log(`🚫 Disconnected from ${r.url}`);
|
console.log(`🚫 Disconnected from ${r.url}`);
|
||||||
@ -206,7 +219,7 @@ class Daemon {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nsec = nip19.nsecEncode(settings.key);
|
const nsec = nip19.nsecEncode(hexToBytes(settings.key));
|
||||||
this.loadNsec(keyName, nsec);
|
this.loadNsec(keyName, nsec);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ async function validateAuthCookie(request) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findFirst({
|
||||||
where: { pubkey: jwt }
|
where: { pubkey: jwt }
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -230,7 +230,7 @@ export async function processRegistrationWebHandler(request, reply) {
|
|||||||
|
|
||||||
await allowAllRequestsFromKey(
|
await allowAllRequestsFromKey(
|
||||||
record.remotePubkey,
|
record.remotePubkey,
|
||||||
record.keyName,
|
record.keyName!,
|
||||||
record.method,
|
record.method,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
|
|||||||
17
src/utils/hex.ts
Normal file
17
src/utils/hex.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Convert Uint8Array to hex string
|
||||||
|
*/
|
||||||
|
export function bytesToHex(bytes: Uint8Array): string {
|
||||||
|
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert hex string to Uint8Array
|
||||||
|
*/
|
||||||
|
export function hexToBytes(hex: string): Uint8Array {
|
||||||
|
const bytes = new Uint8Array(hex.length / 2);
|
||||||
|
for (let i = 0; i < hex.length; i += 2) {
|
||||||
|
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
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