feat(#26): Nostr identity + trust engine

- New nostr_identities DB table (pubkey, trust_score, tier, interaction_count, sats_absorbed_today, last_seen)
- nullable nostr_pubkey FK on sessions + jobs tables; schema pushed
- TrustService: getTier, getOrCreate, recordSuccess/Failure, HMAC token (issue/verify)
- Soft score decay (lazy, on read) when identity absent > N days
- POST /api/identity/challenge + POST /api/identity/verify (NIP-01 sig verification)
- GET /api/identity/me — look up trust profile by X-Nostr-Token
- POST /api/sessions + POST /api/jobs accept optional nostr_token; bind pubkey to row
- GET /sessions/:id + GET /jobs/:id include trust_tier in response
- recordSuccess/Failure called after session request + job work completes
- X-Nostr-Token added to CORS allowedHeaders + exposedHeaders
- TIMMY_TOKEN_SECRET set as persistent shared env var
This commit is contained in:
Replit Agent
2026-03-19 15:59:14 +00:00
parent fa0ebc6b5c
commit 9b778351e4
12 changed files with 581 additions and 6 deletions

View File

@@ -5,3 +5,4 @@ export * from "./messages";
export * from "./bootstrap-jobs";
export * from "./world-events";
export * from "./sessions";
export * from "./nostr-identities";

View File

@@ -36,6 +36,9 @@ export const jobs = pgTable("jobs", {
actualOutputTokens: integer("actual_output_tokens"),
actualCostUsd: real("actual_cost_usd"),
// Optional Nostr identity bound at job creation
nostrPubkey: text("nostr_pubkey"),
// ── Post-work honest accounting & refund ─────────────────────────────────
actualAmountSats: integer("actual_amount_sats"),
refundAmountSats: integer("refund_amount_sats"),

View File

@@ -0,0 +1,39 @@
import { pgTable, text, timestamp, integer } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod/v4";
// ── Trust tier labels ─────────────────────────────────────────────────────────
// Boundaries are env-var overridable via TrustService.
export const TRUST_TIERS = ["new", "established", "trusted", "elite"] as const;
export type TrustTier = (typeof TRUST_TIERS)[number];
// ── nostr_identities ──────────────────────────────────────────────────────────
// One row per Nostr pubkey (64-char lowercase hex). Trust score drives pricing
// decisions in the cost-routing layer (Task #27).
export const nostrIdentities = pgTable("nostr_identities", {
pubkey: text("pubkey").primaryKey(),
trustScore: integer("trust_score").notNull().default(0),
tier: text("tier").$type<TrustTier>().notNull().default("new"),
interactionCount: integer("interaction_count").notNull().default(0),
// Rolling daily absorption budget (reset by TrustService on read)
satsAbsorbedToday: integer("sats_absorbed_today").notNull().default(0),
absorbedResetAt: timestamp("absorbed_reset_at", { withTimezone: true })
.defaultNow()
.notNull(),
lastSeen: timestamp("last_seen", { withTimezone: true }).defaultNow().notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
});
export const insertNostrIdentitySchema = createInsertSchema(nostrIdentities).omit({
createdAt: true,
updatedAt: true,
});
export type NostrIdentity = typeof nostrIdentities.$inferSelect;
export type InsertNostrIdentity = z.infer<typeof insertNostrIdentitySchema>;

View File

@@ -43,6 +43,9 @@ export const sessions = pgTable("sessions", {
// Auth token — issued once when session activates; required for requests
macaroon: text("macaroon"),
// Optional Nostr identity bound at session creation
nostrPubkey: text("nostr_pubkey"),
// TTL — refreshed on each successful request
expiresAt: timestamp("expires_at", { withTimezone: true }),