fix(#26): FK constraints, trust scoring completeness, trust_tier always returned

- sessions.ts / jobs.ts schema: add .references(() => nostrIdentities.pubkey) FK constraints
  on nostrPubkey columns; import without .js extension for drizzle-kit CJS compat
- Schema pushed to DB (FK constraints now enforced at DB level)
- sessions route: call getOrCreate before insert to guarantee FK target exists;
  recordFailure now covers both 'rejected' AND 'failed' final states
- jobs route: call getOrCreate before insert; recordFailure added in
  runEvalInBackground for rejected and failed states; recordFailure added in
  runWorkInBackground catch block for failed state
- All GET/POST endpoints now always return trust_tier (anonymous fallback)
- Full typecheck clean; schema pushed; smoke tested — all routes green
This commit is contained in:
Replit Agent
2026-03-19 16:07:46 +00:00
parent 74831bba7c
commit 1237f10539
4 changed files with 35 additions and 13 deletions

View File

@@ -90,6 +90,12 @@ async function runEvalInBackground(jobId: string, request: string): Promise<void
.set({ state: "rejected", rejectionReason: evalResult.reason, updatedAt: new Date() })
.where(eq(jobs.id, jobId));
eventBus.publish({ type: "job:state", jobId, state: "rejected" });
// Trust scoring — penalise on rejection
const rejectedJob = await getJobById(jobId);
if (rejectedJob?.nostrPubkey) {
void trustService.recordFailure(rejectedJob.nostrPubkey, evalResult.reason ?? "rejected");
}
}
} catch (err) {
const message = err instanceof Error ? err.message : "Evaluation error";
@@ -98,6 +104,12 @@ async function runEvalInBackground(jobId: string, request: string): Promise<void
.set({ state: "failed", errorMessage: message, updatedAt: new Date() })
.where(eq(jobs.id, jobId));
eventBus.publish({ type: "job:failed", jobId, reason: message });
// Trust scoring — penalise on eval failure
const failedJob = await getJobById(jobId);
if (failedJob?.nostrPubkey) {
void trustService.recordFailure(failedJob.nostrPubkey, message);
}
}
}
@@ -166,6 +178,12 @@ async function runWorkInBackground(jobId: string, request: string, workAmountSat
.set({ state: "failed", errorMessage: message, updatedAt: new Date() })
.where(eq(jobs.id, jobId));
eventBus.publish({ type: "job:failed", jobId, reason: message });
// Trust scoring — penalise on work failure
const failedJob = await getJobById(jobId);
if (failedJob?.nostrPubkey) {
void trustService.recordFailure(failedJob.nostrPubkey, message);
}
}
}
@@ -269,8 +287,9 @@ router.post("/jobs", jobsLimiter, async (req: Request, res: Response) => {
}
const { request } = parseResult.data;
// Optionally bind a Nostr identity
// Optionally bind a Nostr identity — ensure row exists before FK insert
const nostrPubkey = resolveNostrPubkey(req);
if (nostrPubkey) await trustService.getOrCreate(nostrPubkey);
try {
const evalFee = pricingService.calculateEvalFeeSats();
@@ -316,7 +335,7 @@ router.post("/jobs", jobsLimiter, async (req: Request, res: Response) => {
jobId,
createdAt: createdAt.toISOString(),
...(nostrPubkey ? { nostrPubkey } : {}),
...(trust ? { trust_tier: trust.tier } : {}),
trust_tier: trust ? trust.tier : "anonymous",
evalInvoice: {
paymentRequest: lnbitsInvoice.paymentRequest,
amountSats: evalFee,
@@ -346,7 +365,7 @@ router.get("/jobs/:id", async (req: Request, res: Response) => {
const trustTier = job.nostrPubkey
? await trustService.getTier(job.nostrPubkey)
: undefined;
: "anonymous";
const base = {
jobId: job.id,
@@ -354,7 +373,7 @@ router.get("/jobs/:id", async (req: Request, res: Response) => {
createdAt: job.createdAt.toISOString(),
completedAt: job.state === "complete" ? job.updatedAt.toISOString() : null,
...(job.nostrPubkey ? { nostrPubkey: job.nostrPubkey } : {}),
...(trustTier ? { trust_tier: trustTier } : {}),
trust_tier: trustTier,
};
switch (job.state) {

View File

@@ -50,7 +50,7 @@ function sessionView(session: Session, includeInvoice = false, trustTier?: strin
expiresAt: session.expiresAt?.toISOString() ?? null,
minimumBalanceSats: MIN_BALANCE_SATS,
...(session.nostrPubkey ? { nostrPubkey: session.nostrPubkey } : {}),
...(trustTier ? { trust_tier: trustTier } : {}),
trust_tier: trustTier ?? "anonymous",
...(session.macaroon && (session.state === "active" || session.state === "paused")
? { macaroon: session.macaroon }
: {}),
@@ -160,8 +160,9 @@ router.post("/sessions", sessionsLimiter, async (req: Request, res: Response) =>
return;
}
// Optionally bind a Nostr identity
// Optionally bind a Nostr identity — ensure row exists before FK insert
const nostrPubkey = resolveNostrPubkey(req);
if (nostrPubkey) await trustService.getOrCreate(nostrPubkey);
try {
const sessionId = randomUUID();
@@ -187,7 +188,7 @@ router.post("/sessions", sessionsLimiter, async (req: Request, res: Response) =>
sessionId,
state: "awaiting_payment",
...(nostrPubkey ? { nostrPubkey } : {}),
...(trust ? { trust_tier: trust.tier } : {}),
trust_tier: trust ? trust.tier : "anonymous",
invoice: {
paymentRequest: invoice.paymentRequest,
amountSats,
@@ -377,8 +378,8 @@ router.post("/sessions/:id/request", async (req: Request, res: Response) => {
if (session.nostrPubkey) {
if (finalState === "complete") {
void trustService.recordSuccess(session.nostrPubkey, debitedSats);
} else if (finalState === "rejected") {
void trustService.recordFailure(session.nostrPubkey, reason ?? "rejected");
} else if (finalState === "rejected" || finalState === "failed") {
void trustService.recordFailure(session.nostrPubkey, reason ?? errorMessage ?? finalState);
}
}

View File

@@ -1,6 +1,7 @@
import { pgTable, text, timestamp, integer, real } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod/v4";
import { nostrIdentities } from "./nostr-identities";
export const JOB_STATES = [
"awaiting_eval_payment",
@@ -36,8 +37,8 @@ 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"),
// Optional Nostr identity bound at job creation (FK → nostr_identities.pubkey)
nostrPubkey: text("nostr_pubkey").references(() => nostrIdentities.pubkey),
// ── Post-work honest accounting & refund ─────────────────────────────────
actualAmountSats: integer("actual_amount_sats"),

View File

@@ -1,6 +1,7 @@
import { pgTable, text, timestamp, integer, boolean, real } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod/v4";
import { nostrIdentities } from "./nostr-identities";
// ── Session state machine ─────────────────────────────────────────────────────
@@ -43,8 +44,8 @@ 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"),
// Optional Nostr identity bound at session creation (FK → nostr_identities.pubkey)
nostrPubkey: text("nostr_pubkey").references(() => nostrIdentities.pubkey),
// TTL — refreshed on each successful request
expiresAt: timestamp("expires_at", { withTimezone: true }),