{"path":"oracle/src/index.ts","size":11531,"locked":false,"binary":false,"truncated":false,"content":"import * as Sentry from \"@sentry/node\";\nimport express, { Request, Response, NextFunction } from \"express\";\nimport helmet from \"helmet\";\nimport cors from \"cors\";\nimport rateLimit from \"express-rate-limit\";\nimport { config } from \"./config\";\nimport { store } from \"./store\";\nimport { startListener } from \"./listener\";\nimport { runConsensus } from \"./verification\";\nimport { releasePayment, cancelJob } from \"./chain\";\nimport {\n  SubmitRequest,\n  SubmitResponse,\n  SubmissionArtifact,\n  ConsensusOutcome,\n} from \"./types\";\n\n// ─── SENTRY INIT ─────────────────────────────────────────────────────────────\n\nif (config.sentry.enabled) {\n  Sentry.init({\n    dsn: config.sentry.dsn,\n    environment: config.isDev ? \"development\" : \"production\",\n    tracesSampleRate: 0.2,\n  });\n  console.log(\"[sentry] Error tracking initialized\");\n}\n\n// ─── EXPRESS APP ─────────────────────────────────────────────────────────────\n\nconst app = express();\n\n// ─── SECURITY MIDDLEWARE ──────────────────────────────────────────────────────\n\napp.use(helmet());\n\nconst corsOptions: cors.CorsOptions = {\n  origin: config.server.corsOrigins\n    ? config.server.corsOrigins.split(\",\").map((o) => o.trim())\n    : false,\n  methods: [\"GET\", \"POST\"],\n  allowedHeaders: [\"Content-Type\", \"Authorization\", \"X-API-Key\"],\n};\napp.use(cors(corsOptions));\n\napp.use(express.json({ limit: \"100kb\" }));\n\nconst limiter = rateLimit({\n  windowMs: config.server.rateLimitWindowMs,\n  max: config.server.rateLimitMax,\n  standardHeaders: true,\n  legacyHeaders: false,\n  message: { error: \"Too many requests. Please try again later.\" },\n});\napp.use(limiter);\n\n// ─── API KEY AUTH MIDDLEWARE ──────────────────────────────────────────────────\n\nfunction requireApiKey(req: Request, res: Response, next: NextFunction): void {\n  if (!config.server.apiKey) {\n    next();\n    return;\n  }\n\n  const provided =\n    req.headers[\"x-api-key\"] ??\n    req.headers[\"authorization\"]?.replace(/^Bearer\\s+/i, \"\");\n  if (!provided || provided !== config.server.apiKey) {\n    res.status(401).json({ error: \"Unauthorized: invalid or missing API key\" });\n    return;\n  }\n  next();\n}\n\n// ─── HEALTH CHECK ─────────────────────────────────────────────────────────────\n\napp.get(\"/health\", (_req: Request, res: Response) => {\n  res.json({\n    status: \"ok\",\n    pendingJobs: store.size(),\n    timestamp: new Date().toISOString(),\n  });\n});\n\n// ─── GET JOB STATUS ───────────────────────────────────────────────────────────\n\napp.get(\"/jobs/:jobId\", (req: Request, res: Response) => {\n  const rawJobId = req.params.jobId;\n  const jobId = Array.isArray(rawJobId) ? rawJobId[0] : rawJobId;\n  const job = store.get(jobId);\n\n  if (!job) {\n    res.status(404).json({ error: \"Job not found in oracle store\" });\n    return;\n  }\n\n  res.json({\n    jobId,\n    escrowPubkey: job.escrowPubkey.toBase58(),\n    client: job.escrow.client.toBase58(),\n    freelancer: job.escrow.freelancer.toBase58(),\n    amount: job.escrow.amount.toString(),\n    status: job.escrow.status,\n    detectedAt: job.detectedAt,\n  });\n});\n\n// ─── SUBMIT DELIVERABLE ───────────────────────────────────────────────────────\n\napp.post(\"/submit\", requireApiKey, async (req: Request, res: Response) => {\n  const body = req.body as SubmitRequest;\n\n  // Basic input validation\n  if (\n    !body.jobId ||\n    !body.description ||\n    !Array.isArray(body.acceptanceCriteria) ||\n    body.acceptanceCriteria.length === 0 ||\n    !body.deliverable ||\n    !body.deliverableType\n  ) {\n    res.status(400).json({\n      success: false,\n      jobId: body.jobId ?? \"\",\n      error:\n        \"Missing required fields: jobId, description, acceptanceCriteria, deliverable, deliverableType\",\n    } satisfies SubmitResponse);\n    return;\n  }\n\n  const job = store.get(body.jobId);\n\n  if (!job) {\n    console.warn(`[submit] Job ${body.jobId} not found in oracle store`);\n\n    res.status(404).json({\n      success: false,\n      jobId: body.jobId,\n      error:\n        \"Job not found. Either the job does not exist, has already been resolved, or the oracle has not yet detected the on-chain event. If just initialized, wait a few seconds and retry.\",\n    } satisfies SubmitResponse);\n    return;\n  }\n\n  if (!(\"pending\" in job.escrow.status)) {\n    res.status(409).json({\n      success: false,\n      jobId: body.jobId,\n      error: \"Job is no longer in Pending state. It has already been resolved.\",\n    } satisfies SubmitResponse);\n    return;\n  }\n\n  // Input length validation to prevent prompt injection and memory abuse\n  const MAX_DELIVERABLE_LENGTH = 50_000;\n  const MAX_DESCRIPTION_LENGTH = 5_000;\n  const MAX_CRITERIA_LENGTH = 2_000;\n\n  if (body.deliverable.length > MAX_DELIVERABLE_LENGTH) {\n    res.status(400).json({\n      success: false,\n      jobId: body.jobId,\n      error: `Deliverable exceeds maximum length of ${MAX_DELIVERABLE_LENGTH} characters`,\n    } satisfies SubmitResponse);\n    return;\n  }\n\n  if (body.description.length > MAX_DESCRIPTION_LENGTH) {\n    res.status(400).json({\n      success: false,\n      jobId: body.jobId,\n      error: `Description exceeds maximum length of ${MAX_DESCRIPTION_LENGTH} characters`,\n    } satisfies SubmitResponse);\n    return;\n  }\n\n  if (body.acceptanceCriteria.some((c) => c.length > MAX_CRITERIA_LENGTH)) {\n    res.status(400).json({\n      success: false,\n      jobId: body.jobId,\n      error: `Each acceptance criterion must be under ${MAX_CRITERIA_LENGTH} characters`,\n    } satisfies SubmitResponse);\n    return;\n  }\n\n  const VALID_DELIVERABLE_TYPES = [\"url\", \"ipfs\", \"text\", \"json\"] as const;\n  if (\n    !VALID_DELIVERABLE_TYPES.includes(\n      body.deliverableType as (typeof VALID_DELIVERABLE_TYPES)[number],\n    )\n  ) {\n    res.status(400).json({\n      success: false,\n      jobId: body.jobId,\n      error: `Invalid deliverableType. Must be one of: ${VALID_DELIVERABLE_TYPES.join(\", \")}`,\n    } satisfies SubmitResponse);\n    return;\n  }\n\n  const artifact: SubmissionArtifact = {\n    jobId: body.jobId,\n    description: body.description,\n    acceptanceCriteria: body.acceptanceCriteria,\n    deliverable: body.deliverable,\n    deliverableType: body.deliverableType,\n    submittedAt: Date.now(),\n  };\n\n  console.log(`[submit] Starting verification for job: ${body.jobId}`);\n\n  let outcome: ConsensusOutcome;\n  let txSig: string | undefined;\n\n  try {\n    const result = await runConsensus(job, artifact);\n    outcome = result.outcome;\n\n    console.log(`[submit] Consensus: ${outcome} | ${result.reasoning}`);\n\n    if (outcome === \"RELEASE\") {\n      txSig = await releasePayment(job);\n      store.remove(body.jobId);\n      console.log(`[submit] Payment released. tx: ${txSig}`);\n    } else if (outcome === \"REFUND\") {\n      txSig = await cancelJob(job);\n      store.remove(body.jobId);\n      console.log(`[submit] Job cancelled, refund issued. tx: ${txSig}`);\n    } else {\n      // ESCALATE — log for human review, do not execute on-chain\n      console.warn(\n        `[submit] ESCALATE: Job ${body.jobId} requires human arbitration.`,\n      );\n      console.warn(\n        `         Gemini: ${result.geminiVerdict.verdict} (${result.geminiVerdict.confidence.toFixed(2)})`,\n      );\n      console.warn(\n        `         Claude: ${result.claudeVerdict.verdict} (${result.claudeVerdict.confidence.toFixed(2)})`,\n      );\n\n      if (config.sentry.enabled) {\n        Sentry.captureMessage(`Oracle escalation: job ${body.jobId}`, {\n          level: \"warning\",\n          extra: {\n            jobId: body.jobId,\n            gemini: result.geminiVerdict,\n            claude: result.claudeVerdict,\n          },\n        });\n      }\n    }\n\n    res.json({\n      success: true,\n      jobId: body.jobId,\n      outcome,\n      txSig,\n    } satisfies SubmitResponse);\n  } catch (err: unknown) {\n    const message = err instanceof Error ? err.message : String(err);\n    console.error(\n      `[submit] Verification/execution error for job ${body.jobId}:`,\n      message,\n    );\n\n    if (config.sentry.enabled) {\n      Sentry.captureException(err);\n    }\n\n    res.status(500).json({\n      success: false,\n      jobId: body.jobId,\n      error: config.isDev\n        ? `Internal oracle error: ${message}`\n        : \"Internal oracle error. Please try again later.\",\n    } satisfies SubmitResponse);\n  }\n});\n\n// ─── FALLBACK ERROR HANDLER ───────────────────────────────────────────────────\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\napp.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {\n  if (config.sentry.enabled) Sentry.captureException(err);\n  console.error(\"[express] Unhandled error:\", err.message);\n  res.status(500).json({ error: \"Internal server error\" });\n});\n\n// ─── BOOTSTRAP ───────────────────────────────────────────────────────────────\n\nfunction bootstrap(): void {\n  // 1. Start gRPC listener — detects on-chain job events\n  startListener();\n\n  // 2. Start HTTP server — receives freelancer submission triggers\n  const server = app.listen(config.server.port, () => {\n    const rpcHost = new URL(config.solana.rpcUrl).hostname;\n    console.log(`\\n🟢 Mappers Oracle running`);\n    console.log(`   HTTP server: http://localhost:${config.server.port}`);\n    console.log(`   Program ID:  ${config.solana.programId}`);\n    console.log(`   Network:     ${rpcHost}\\n`);\n  });\n\n  server.on(\"error\", (err: NodeJS.ErrnoException) => {\n    if (err.code === \"EADDRINUSE\") {\n      console.error(\n        `[oracle] Port ${config.server.port} is already in use. Exiting.`,\n      );\n    } else {\n      console.error(`[oracle] HTTP server error: ${err.message}`);\n    }\n    process.exit(1);\n  });\n\n  // 3. Graceful shutdown\n  process.on(\"SIGTERM\", () => {\n    console.log(\"[oracle] SIGTERM received. Shutting down gracefully.\");\n    process.exit(0);\n  });\n\n  process.on(\"SIGINT\", () => {\n    console.log(\"[oracle] SIGINT received. Shutting down.\");\n    process.exit(0);\n  });\n\n  process.on(\"unhandledRejection\", (reason) => {\n    console.error(\"[oracle] Unhandled rejection:\", reason);\n    if (config.sentry.enabled) {\n      Sentry.captureException(reason);\n      // Flush Sentry events before exiting\n      void Sentry.flush(2000).finally(() => process.exit(1));\n    } else {\n      process.exit(1);\n    }\n  });\n\n  process.on(\"uncaughtException\", (err) => {\n    console.error(\"[oracle] Uncaught exception:\", err);\n    if (config.sentry.enabled) {\n      Sentry.captureException(err);\n      void Sentry.flush(2000).finally(() => process.exit(1));\n    } else {\n      process.exit(1);\n    }\n  });\n}\n\nbootstrap();\n"}