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:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
Reference in New Issue
Block a user