diff --git a/artifacts/api-server/src/index.ts b/artifacts/api-server/src/index.ts index 8c6667b..cfbb570 100644 --- a/artifacts/api-server/src/index.ts +++ b/artifacts/api-server/src/index.ts @@ -32,13 +32,17 @@ server.listen(port, () => { } startEngagementEngine(); - // Seed Timmy's own pubkey with elite relay access on every startup. - // This is idempotent — upsert is safe to run multiple times. + // Seed Timmy's own pubkey with elite identity + relay write access. + // Resolves pubkey from TIMMY_NOSTR_PUBKEY env var if set (explicit override), + // otherwise falls back to the hex pubkey derived from TIMMY_NOSTR_NSEC. + // Idempotent — safe to run on every startup. + const timmyPubkey = process.env["TIMMY_NOSTR_PUBKEY"] ?? timmyIdentityService.pubkeyHex; relayAccountService - .grant(timmyIdentityService.pubkeyHex, "write", "Timmy's own pubkey — elite access", "manual") + .seedElite(timmyPubkey, "Timmy's own pubkey — elite access seeded at startup") .then(() => - rootLogger.info("relay: Timmy's pubkey seeded with write access", { - pubkey: timmyIdentityService.pubkeyHex.slice(0, 8), + rootLogger.info("relay: Timmy's pubkey seeded with elite access", { + pubkey: timmyPubkey.slice(0, 8), + source: process.env["TIMMY_NOSTR_PUBKEY"] ? "TIMMY_NOSTR_PUBKEY env" : "timmyIdentityService", }), ) .catch((err) => diff --git a/artifacts/api-server/src/lib/relay-accounts.ts b/artifacts/api-server/src/lib/relay-accounts.ts index 9680722..aac1c7a 100644 --- a/artifacts/api-server/src/lib/relay-accounts.ts +++ b/artifacts/api-server/src/lib/relay-accounts.ts @@ -10,6 +10,11 @@ * Only "write" access generates an "accept" from the relay policy. * "read" is reserved for future read-gated relays. * "none" = default deny. + * + * Revocation: + * revoke() sets grantedBy = "manual-revoked". syncFromTrustTier() respects + * this marker and will never auto-reinstate a manually revoked account. + * Only an explicit admin grant() call can restore access after revocation. */ import { db, nostrIdentities, relayAccounts } from "@workspace/db"; @@ -34,6 +39,8 @@ const TIER_ACCESS: Record = { elite: envAccess("RELAY_ACCESS_ELITE", "write"), }; +const ACCESS_RANK: Record = { none: 0, read: 1, write: 2 }; + // ── RelayAccountService ─────────────────────────────────────────────────────── export class RelayAccountService { @@ -101,14 +108,17 @@ export class RelayAccountService { /** * Revoke relay access for a pubkey. - * Sets revokedAt and resets access_level to "none". - * No-op if pubkey has no row. + * Sets revokedAt, access_level → "none", grantedBy → "manual-revoked". + * The "manual-revoked" marker prevents syncFromTrustTier() from auto-reinstating + * this account on the next trust update. Only an explicit grant() by an admin + * can restore access after revocation. */ async revoke(pubkey: string, reason?: string): Promise { await db .update(relayAccounts) .set({ accessLevel: "none", + grantedBy: "manual-revoked", revokedAt: new Date(), notes: reason ?? null, }) @@ -121,12 +131,61 @@ export class RelayAccountService { } /** - * Sync relay access from the current trust tier. - * Only updates rows that were auto-granted (grantedBy = "auto-tier"). - * Manually-granted rows are never downgraded by this method. - * Upgrades always apply regardless of grantedBy. + * Seed a pubkey with elite identity state and relay write access. + * Called at startup for Timmy's own pubkey. Idempotent. + * + * Sets nostr_identities.tier = "elite" to reflect the elite trust state, + * then grants relay write access as a manual (permanent) grant. */ - async syncFromTrustTier(pubkey: string, tier: TrustTier): Promise { + async seedElite(pubkey: string, notes: string): Promise { + const ELITE_SCORE = 200; // matches TIER_ELITE threshold default + + // Upsert nostr_identities with elite tier + score + await db + .insert(nostrIdentities) + .values({ + pubkey, + tier: "elite", + trustScore: ELITE_SCORE, + lastSeen: new Date(), + }) + .onConflictDoUpdate({ + target: nostrIdentities.pubkey, + set: { + tier: "elite", + trustScore: ELITE_SCORE, + updatedAt: new Date(), + }, + }); + + // Grant relay write access (manual — never overridden by auto-tier sync) + await this.grant(pubkey, "write", notes, "manual"); + + logger.info("relay: elite seed applied", { + pubkey: pubkey.slice(0, 8), + notes, + }); + } + + /** + * Sync relay access from the current trust tier. + * Fetches the pubkey's tier from nostr_identities internally. + * + * Rules: + * - "manual-revoked" rows: never auto-reinstated + * - "manual" rows (active): only upgraded, never downgraded + * - "auto-tier" rows: always synced to current tier's target access level + * - No existing row: create one if the tier earns access (non-"none") + */ + async syncFromTrustTier(pubkey: string): Promise { + // Fetch current tier from DB (written by TrustService before this is called) + const identityRows = await db + .select({ tier: nostrIdentities.tier }) + .from(nostrIdentities) + .where(eq(nostrIdentities.pubkey, pubkey)) + .limit(1); + + const tier: TrustTier = (identityRows[0]?.tier as TrustTier) ?? "new"; const targetLevel = TIER_ACCESS[tier]; const rows = await db @@ -138,32 +197,28 @@ export class RelayAccountService { const existing = rows[0]; if (!existing) { - // No row yet — only create one if the tier earns access if (targetLevel !== "none") { await this.grant(pubkey, targetLevel, `auto: tier promotion to ${tier}`, "auto-tier"); } return; } - // Manual grants are never downgraded by auto-tier sync + // Manually revoked — admin must explicitly re-grant; never auto-reinstate + if (existing.grantedBy === "manual-revoked") { + return; + } + + // Active manual grant — only upgrade, never downgrade if (existing.grantedBy === "manual" && existing.revokedAt === null) { - const ACCESS_RANK: Record = { none: 0, read: 1, write: 2 }; - if (ACCESS_RANK[targetLevel] <= ACCESS_RANK[existing.accessLevel]) { - return; // manual grant already at equal or better level + if (ACCESS_RANK[targetLevel] > ACCESS_RANK[existing.accessLevel]) { + await this.grant(pubkey, targetLevel, `auto-upgrade: tier ${tier}`, "manual"); } - // Upgrade the manual row (manual stays manual — it's a promotion, not a demotion) - await this.grant(pubkey, targetLevel, `auto-upgrade from tier ${tier}`, "manual"); return; } // Auto-tier row — sync to current tier's target level if (targetLevel !== existing.accessLevel || existing.revokedAt !== null) { - await this.grant( - pubkey, - targetLevel, - `auto: tier ${tier} → ${targetLevel}`, - "auto-tier", - ); + await this.grant(pubkey, targetLevel, `auto: tier ${tier} → ${targetLevel}`, "auto-tier"); } } diff --git a/artifacts/api-server/src/lib/trust.ts b/artifacts/api-server/src/lib/trust.ts index 3bd5617..bd672a3 100644 --- a/artifacts/api-server/src/lib/trust.ts +++ b/artifacts/api-server/src/lib/trust.ts @@ -158,7 +158,7 @@ export class TrustService { }); // Sync relay access whenever the tier may have changed - relayAccountService.syncFromTrustTier(pubkey, newTier).catch((err) => + relayAccountService.syncFromTrustTier(pubkey).catch((err) => logger.warn("relay sync failed after success", { pubkey: pubkey.slice(0, 8), err }), ); } @@ -191,7 +191,7 @@ export class TrustService { }); // Sync relay access on tier change (may revoke write on repeated failures) - relayAccountService.syncFromTrustTier(pubkey, newTier).catch((err) => + relayAccountService.syncFromTrustTier(pubkey).catch((err) => logger.warn("relay sync failed after failure", { pubkey: pubkey.slice(0, 8), err }), ); }