From ee776452bd41484fd9e99c56c4c0a4e1ac771cb0 Mon Sep 17 00:00:00 2001 From: Josh Date: Tue, 10 Mar 2026 12:11:51 -0400 Subject: [PATCH 1/2] feat: auto-approve connect requests with valid connection tokens When a connect request includes a valid one-time connection token (or admin secret), auto-approve with "reasonable" trust level and return 'ack' immediately instead of requiring manual dashboard approval. The connection token is already a one-time secret with a TTL, so validating it is sufficient authorization. This enables automated signup flows where the server creates a key + connection token and the client connects without human intervention. Requests without a secret still go through manual approval (existing behavior). Co-Authored-By: Claude Opus 4.6 --- apps/signet/src/daemon/nip46-backend.ts | 34 +++++++++++++++++-------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/apps/signet/src/daemon/nip46-backend.ts b/apps/signet/src/daemon/nip46-backend.ts index 4ceacce..b0010f6 100644 --- a/apps/signet/src/daemon/nip46-backend.ts +++ b/apps/signet/src/daemon/nip46-backend.ts @@ -7,6 +7,7 @@ import createDebug from 'debug'; import { bytesToHex } from './lib/hex.js'; import { toErrorMessage } from './lib/errors.js'; import { logger } from './lib/logger.js'; +import { grantPermissionsByTrustLevel } from './lib/acl.js'; import { TTLCache } from './lib/ttl-cache.js'; import prisma from '../db.js'; import type { RelayPool } from './lib/relay-pool.js'; @@ -350,8 +351,9 @@ export class Nip46Backend { /** * Handle connect request with secret validation. * Validates against one-time connection tokens first, then falls back to admin secret. - * Secret validates the connection attempt but does NOT auto-approve. - * User must still approve and select trust level via the UI. + * When a valid secret is provided, auto-approve with "reasonable" trust level — + * the secret itself is proof of authorization (one-time token with TTL, or admin secret). + * Requests without a secret go through manual approval via the UI. */ private async handleConnect( id: string, @@ -367,8 +369,16 @@ export class Nip46Backend { const tokenValid = await tokenService.validateAndRedeemToken(providedSecret, this.keyName); if (tokenValid) { + // Auto-approve: connection token is a one-time secret with TTL, + // so validating it is sufficient authorization. debug('[%s] connect with valid one-time token from %s', this.keyName, humanPubkey); - } else if (this.adminSecret) { + logger.info('Auto-approving connect via connection token', { key: this.keyName, from: humanPubkey }); + const keyUserId = await grantPermissionsByTrustLevel(remotePubkey, this.keyName, 'reasonable', 'auto-approved via connection token'); + this.addAppSubscription(keyUserId, []); + return 'ack'; + } + + if (this.adminSecret) { // Fallback: check against persistent admin secret const secretsMatch = providedSecret.length === this.adminSecret.length && crypto.timingSafeEqual(Buffer.from(providedSecret), Buffer.from(this.adminSecret)); @@ -377,17 +387,21 @@ export class Nip46Backend { debug('[%s] connect with invalid secret from %s', this.keyName, humanPubkey); return undefined; // Silent rejection - no response sent } + + // Auto-approve admin secret connections too debug('[%s] connect with valid admin secret from %s', this.keyName, humanPubkey); - } else { - // No admin secret configured and token was invalid - debug('[%s] connect with invalid token from %s (no admin secret fallback)', this.keyName, humanPubkey); - return undefined; // Silent rejection + logger.info('Auto-approving connect via admin secret', { key: this.keyName, from: humanPubkey }); + const keyUserId = await grantPermissionsByTrustLevel(remotePubkey, this.keyName, 'reasonable', 'auto-approved via admin secret'); + this.addAppSubscription(keyUserId, []); + return 'ack'; } + + // No admin secret configured and token was invalid + debug('[%s] connect with invalid secret from %s (no admin secret fallback)', this.keyName, humanPubkey); + return undefined; // Silent rejection } - // If no secret provided, proceed to approval flow (existing behavior) - // All connect requests go through the normal approval flow - // This allows the user to see the request and select a trust level + // No secret provided — go through manual approval flow (existing behavior) logger.info('Connect request, awaiting approval', { key: this.keyName, from: humanPubkey }); const permitted = await this.permitCallback({ id, From 30e5ec3aad5f2fee215935be3496f2c0daa9130c Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 11 Mar 2026 08:06:56 -0400 Subject: [PATCH 2/2] chore: grant full trust on auto-approve via connection token Connection tokens are only issued by the admin API, so the connecting app is trusted. Full trust avoids blocking on sensitive event kinds (profile metadata, relay lists, etc.) that are needed during signup. Co-Authored-By: Claude Opus 4.6 --- apps/signet/src/daemon/nip46-backend.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/signet/src/daemon/nip46-backend.ts b/apps/signet/src/daemon/nip46-backend.ts index b0010f6..6f1afe4 100644 --- a/apps/signet/src/daemon/nip46-backend.ts +++ b/apps/signet/src/daemon/nip46-backend.ts @@ -373,7 +373,7 @@ export class Nip46Backend { // so validating it is sufficient authorization. debug('[%s] connect with valid one-time token from %s', this.keyName, humanPubkey); logger.info('Auto-approving connect via connection token', { key: this.keyName, from: humanPubkey }); - const keyUserId = await grantPermissionsByTrustLevel(remotePubkey, this.keyName, 'reasonable', 'auto-approved via connection token'); + const keyUserId = await grantPermissionsByTrustLevel(remotePubkey, this.keyName, 'full', 'auto-approved via connection token'); this.addAppSubscription(keyUserId, []); return 'ack'; } @@ -391,7 +391,7 @@ export class Nip46Backend { // Auto-approve admin secret connections too debug('[%s] connect with valid admin secret from %s', this.keyName, humanPubkey); logger.info('Auto-approving connect via admin secret', { key: this.keyName, from: humanPubkey }); - const keyUserId = await grantPermissionsByTrustLevel(remotePubkey, this.keyName, 'reasonable', 'auto-approved via admin secret'); + const keyUserId = await grantPermissionsByTrustLevel(remotePubkey, this.keyName, 'full', 'auto-approved via admin secret'); this.addAppSubscription(keyUserId, []); return 'ack'; }