feat(Phase1): Implement Scout API, Stripe Webhooks, and Builder Whitelisting
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s

This commit is contained in:
OG T
2026-06-07 13:58:13 +08:00
parent aacf45d87d
commit 8003aceab0
32 changed files with 8280 additions and 305 deletions

View File

@@ -16,6 +16,7 @@
"next": "16.2.7",
"react": "19.2.4",
"react-dom": "19.2.4",
"stripe": "^22.2.0",
"zod": "^3.23.0"
},
"devDependencies": {

File diff suppressed because one or more lines are too long

View File

@@ -7,10 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
const {
Decimal,
DbNull,
JsonNull,
AnyNull,
NullTypes,
objectEnumValues,
makeStrictEnum,
Public,
getRuntime,
@@ -24,12 +21,12 @@ exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 7.8.0
* Query Engine version: 3c6e192761c0362d496ed980de936e2f3cebcd3a
* Prisma Client JS version: 6.19.3
* Query Engine version: c2990dca591cba766e3b7ef5d9e8a84796e47ab7
*/
Prisma.prismaVersion = {
client: "7.8.0",
engine: "3c6e192761c0362d496ed980de936e2f3cebcd3a"
client: "6.19.3",
engine: "c2990dca591cba766e3b7ef5d9e8a84796e47ab7"
}
Prisma.PrismaClientKnownRequestError = () => {
@@ -101,11 +98,15 @@ In case this error is unexpected for you, please report it in https://pris.ly/pr
/**
* Shorthand utilities for JSON filtering
*/
Prisma.DbNull = DbNull
Prisma.JsonNull = JsonNull
Prisma.AnyNull = AnyNull
Prisma.DbNull = objectEnumValues.instances.DbNull
Prisma.JsonNull = objectEnumValues.instances.JsonNull
Prisma.AnyNull = objectEnumValues.instances.AnyNull
Prisma.NullTypes = NullTypes
Prisma.NullTypes = {
DbNull: objectEnumValues.classes.DbNull,
JsonNull: objectEnumValues.classes.JsonNull,
AnyNull: objectEnumValues.classes.AnyNull
}
@@ -134,14 +135,18 @@ exports.Prisma.TaskScalarFieldEnum = {
required_stack: 'required_stack',
retry_count: 'retry_count',
stripe_payment_intent_id: 'stripe_payment_intent_id',
stripe_checkout_session_id: 'stripe_checkout_session_id',
expires_at: 'expires_at',
created_at: 'created_at',
updated_at: 'updated_at'
updated_at: 'updated_at',
scout_id: 'scout_id',
builder_id: 'builder_id'
};
exports.Prisma.ClaimScalarFieldEnum = {
id: 'id',
task_id: 'task_id',
agent_id: 'agent_id',
developer_wallet: 'developer_wallet',
status: 'status',
claim_token: 'claim_token',
@@ -202,6 +207,16 @@ exports.Prisma.LedgerEntryScalarFieldEnum = {
updated_at: 'updated_at'
};
exports.Prisma.AgentProfileScalarFieldEnum = {
id: 'id',
agent_id: 'agent_id',
type: 'type',
wallet_address: 'wallet_address',
status: 'status',
created_at: 'created_at',
updated_at: 'updated_at'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
@@ -239,7 +254,8 @@ exports.Prisma.ModelName = {
Submission: 'Submission',
JudgeResult: 'JudgeResult',
AuditEvent: 'AuditEvent',
LedgerEntry: 'LedgerEntry'
LedgerEntry: 'LedgerEntry',
AgentProfile: 'AgentProfile'
};
/**

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
{
"name": "prisma-client-e5a0d29448aef14ee47356f474ec26ee939c82359a834a5929b1331dfd872836",
"name": "prisma-client-3cb8d001424c53952725f92eb8ceb9df5c6eed9994572f107c72f10a31e6bf6d",
"main": "index.js",
"types": "index.d.ts",
"browser": "default.js",
@@ -7,17 +7,17 @@
"./client": {
"require": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
@@ -27,22 +27,34 @@
".": {
"require": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
},
"./edge": {
"types": "./edge.d.ts",
"require": "./edge.js",
"import": "./edge.js",
"default": "./edge.js"
},
"./react-native": {
"types": "./react-native.d.ts",
"require": "./react-native.js",
"import": "./react-native.js",
"default": "./react-native.js"
},
"./extension": {
"types": "./extension.d.ts",
"require": "./extension.js",
@@ -61,11 +73,11 @@
"import": "./index.js",
"default": "./index.js"
},
"./edge": {
"types": "./edge.d.ts",
"require": "./edge.js",
"import": "./edge.js",
"default": "./edge.js"
"./wasm": {
"types": "./wasm.d.ts",
"require": "./wasm.js",
"import": "./wasm.mjs",
"default": "./wasm.mjs"
},
"./runtime/client": {
"types": "./runtime/client.d.ts",
@@ -77,12 +89,42 @@
"import": "./runtime/client.mjs",
"default": "./runtime/client.mjs"
},
"./runtime/library": {
"types": "./runtime/library.d.ts",
"require": "./runtime/library.js",
"import": "./runtime/library.mjs",
"default": "./runtime/library.mjs"
},
"./runtime/binary": {
"types": "./runtime/binary.d.ts",
"require": "./runtime/binary.js",
"import": "./runtime/binary.mjs",
"default": "./runtime/binary.mjs"
},
"./runtime/wasm-engine-edge": {
"types": "./runtime/wasm-engine-edge.d.ts",
"require": "./runtime/wasm-engine-edge.js",
"import": "./runtime/wasm-engine-edge.mjs",
"default": "./runtime/wasm-engine-edge.mjs"
},
"./runtime/wasm-compiler-edge": {
"types": "./runtime/wasm-compiler-edge.d.ts",
"require": "./runtime/wasm-compiler-edge.js",
"import": "./runtime/wasm-compiler-edge.mjs",
"default": "./runtime/wasm-compiler-edge.mjs"
},
"./runtime/edge": {
"types": "./runtime/edge.d.ts",
"require": "./runtime/edge.js",
"import": "./runtime/edge-esm.js",
"default": "./runtime/edge-esm.js"
},
"./runtime/react-native": {
"types": "./runtime/react-native.d.ts",
"require": "./runtime/react-native.js",
"import": "./runtime/react-native.js",
"default": "./runtime/react-native.js"
},
"./runtime/index-browser": {
"types": "./runtime/index-browser.d.ts",
"require": "./runtime/index-browser.js",
@@ -109,13 +151,10 @@
},
"./*": "./*"
},
"version": "7.8.0",
"version": "6.19.3",
"sideEffects": false,
"dependencies": {
"@prisma/client-runtime-utils": "7.8.0"
},
"imports": {
"#wasm-compiler-loader": {
"#wasm-engine-loader": {
"edge-light": "./wasm-edge-light-loader.mjs",
"workerd": "./wasm-worker-loader.mjs",
"worker": "./wasm-worker-loader.mjs",
@@ -124,17 +163,17 @@
"#main-entry-point": {
"require": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,14 +1,6 @@
import { AnyNull } from '@prisma/client-runtime-utils';
import { DbNull } from '@prisma/client-runtime-utils';
import { Decimal } from '@prisma/client-runtime-utils';
import { isAnyNull } from '@prisma/client-runtime-utils';
import { isDbNull } from '@prisma/client-runtime-utils';
import { isJsonNull } from '@prisma/client-runtime-utils';
import { isObjectEnumValue } from '@prisma/client-runtime-utils';
import { JsonNull } from '@prisma/client-runtime-utils';
import { NullTypes } from '@prisma/client-runtime-utils';
export { AnyNull }
declare class AnyNull extends NullTypesEnumValue {
#private;
}
declare type Args<T, F extends Operation> = T extends {
[K: symbol]: {
@@ -22,9 +14,278 @@ declare type Args<T, F extends Operation> = T extends {
};
} ? T[symbol]['types']['operations'][F]['args'] : any;
export { DbNull }
declare class DbNull extends NullTypesEnumValue {
#private;
}
export { Decimal }
export declare function Decimal(n: Decimal.Value): Decimal;
export declare namespace Decimal {
export type Constructor = typeof Decimal;
export type Instance = Decimal;
export type Rounding = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
export type Modulo = Rounding | 9;
export type Value = string | number | Decimal;
// http://mikemcl.github.io/decimal.js/#constructor-properties
export interface Config {
precision?: number;
rounding?: Rounding;
toExpNeg?: number;
toExpPos?: number;
minE?: number;
maxE?: number;
crypto?: boolean;
modulo?: Modulo;
defaults?: boolean;
}
}
export declare class Decimal {
readonly d: number[];
readonly e: number;
readonly s: number;
constructor(n: Decimal.Value);
absoluteValue(): Decimal;
abs(): Decimal;
ceil(): Decimal;
clampedTo(min: Decimal.Value, max: Decimal.Value): Decimal;
clamp(min: Decimal.Value, max: Decimal.Value): Decimal;
comparedTo(n: Decimal.Value): number;
cmp(n: Decimal.Value): number;
cosine(): Decimal;
cos(): Decimal;
cubeRoot(): Decimal;
cbrt(): Decimal;
decimalPlaces(): number;
dp(): number;
dividedBy(n: Decimal.Value): Decimal;
div(n: Decimal.Value): Decimal;
dividedToIntegerBy(n: Decimal.Value): Decimal;
divToInt(n: Decimal.Value): Decimal;
equals(n: Decimal.Value): boolean;
eq(n: Decimal.Value): boolean;
floor(): Decimal;
greaterThan(n: Decimal.Value): boolean;
gt(n: Decimal.Value): boolean;
greaterThanOrEqualTo(n: Decimal.Value): boolean;
gte(n: Decimal.Value): boolean;
hyperbolicCosine(): Decimal;
cosh(): Decimal;
hyperbolicSine(): Decimal;
sinh(): Decimal;
hyperbolicTangent(): Decimal;
tanh(): Decimal;
inverseCosine(): Decimal;
acos(): Decimal;
inverseHyperbolicCosine(): Decimal;
acosh(): Decimal;
inverseHyperbolicSine(): Decimal;
asinh(): Decimal;
inverseHyperbolicTangent(): Decimal;
atanh(): Decimal;
inverseSine(): Decimal;
asin(): Decimal;
inverseTangent(): Decimal;
atan(): Decimal;
isFinite(): boolean;
isInteger(): boolean;
isInt(): boolean;
isNaN(): boolean;
isNegative(): boolean;
isNeg(): boolean;
isPositive(): boolean;
isPos(): boolean;
isZero(): boolean;
lessThan(n: Decimal.Value): boolean;
lt(n: Decimal.Value): boolean;
lessThanOrEqualTo(n: Decimal.Value): boolean;
lte(n: Decimal.Value): boolean;
logarithm(n?: Decimal.Value): Decimal;
log(n?: Decimal.Value): Decimal;
minus(n: Decimal.Value): Decimal;
sub(n: Decimal.Value): Decimal;
modulo(n: Decimal.Value): Decimal;
mod(n: Decimal.Value): Decimal;
naturalExponential(): Decimal;
exp(): Decimal;
naturalLogarithm(): Decimal;
ln(): Decimal;
negated(): Decimal;
neg(): Decimal;
plus(n: Decimal.Value): Decimal;
add(n: Decimal.Value): Decimal;
precision(includeZeros?: boolean): number;
sd(includeZeros?: boolean): number;
round(): Decimal;
sine() : Decimal;
sin() : Decimal;
squareRoot(): Decimal;
sqrt(): Decimal;
tangent() : Decimal;
tan() : Decimal;
times(n: Decimal.Value): Decimal;
mul(n: Decimal.Value) : Decimal;
toBinary(significantDigits?: number): string;
toBinary(significantDigits: number, rounding: Decimal.Rounding): string;
toDecimalPlaces(decimalPlaces?: number): Decimal;
toDecimalPlaces(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
toDP(decimalPlaces?: number): Decimal;
toDP(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
toExponential(decimalPlaces?: number): string;
toExponential(decimalPlaces: number, rounding: Decimal.Rounding): string;
toFixed(decimalPlaces?: number): string;
toFixed(decimalPlaces: number, rounding: Decimal.Rounding): string;
toFraction(max_denominator?: Decimal.Value): Decimal[];
toHexadecimal(significantDigits?: number): string;
toHexadecimal(significantDigits: number, rounding: Decimal.Rounding): string;
toHex(significantDigits?: number): string;
toHex(significantDigits: number, rounding?: Decimal.Rounding): string;
toJSON(): string;
toNearest(n: Decimal.Value, rounding?: Decimal.Rounding): Decimal;
toNumber(): number;
toOctal(significantDigits?: number): string;
toOctal(significantDigits: number, rounding: Decimal.Rounding): string;
toPower(n: Decimal.Value): Decimal;
pow(n: Decimal.Value): Decimal;
toPrecision(significantDigits?: number): string;
toPrecision(significantDigits: number, rounding: Decimal.Rounding): string;
toSignificantDigits(significantDigits?: number): Decimal;
toSignificantDigits(significantDigits: number, rounding: Decimal.Rounding): Decimal;
toSD(significantDigits?: number): Decimal;
toSD(significantDigits: number, rounding: Decimal.Rounding): Decimal;
toString(): string;
truncated(): Decimal;
trunc(): Decimal;
valueOf(): string;
static abs(n: Decimal.Value): Decimal;
static acos(n: Decimal.Value): Decimal;
static acosh(n: Decimal.Value): Decimal;
static add(x: Decimal.Value, y: Decimal.Value): Decimal;
static asin(n: Decimal.Value): Decimal;
static asinh(n: Decimal.Value): Decimal;
static atan(n: Decimal.Value): Decimal;
static atanh(n: Decimal.Value): Decimal;
static atan2(y: Decimal.Value, x: Decimal.Value): Decimal;
static cbrt(n: Decimal.Value): Decimal;
static ceil(n: Decimal.Value): Decimal;
static clamp(n: Decimal.Value, min: Decimal.Value, max: Decimal.Value): Decimal;
static clone(object?: Decimal.Config): Decimal.Constructor;
static config(object: Decimal.Config): Decimal.Constructor;
static cos(n: Decimal.Value): Decimal;
static cosh(n: Decimal.Value): Decimal;
static div(x: Decimal.Value, y: Decimal.Value): Decimal;
static exp(n: Decimal.Value): Decimal;
static floor(n: Decimal.Value): Decimal;
static hypot(...n: Decimal.Value[]): Decimal;
static isDecimal(object: any): object is Decimal;
static ln(n: Decimal.Value): Decimal;
static log(n: Decimal.Value, base?: Decimal.Value): Decimal;
static log2(n: Decimal.Value): Decimal;
static log10(n: Decimal.Value): Decimal;
static max(...n: Decimal.Value[]): Decimal;
static min(...n: Decimal.Value[]): Decimal;
static mod(x: Decimal.Value, y: Decimal.Value): Decimal;
static mul(x: Decimal.Value, y: Decimal.Value): Decimal;
static noConflict(): Decimal.Constructor; // Browser only
static pow(base: Decimal.Value, exponent: Decimal.Value): Decimal;
static random(significantDigits?: number): Decimal;
static round(n: Decimal.Value): Decimal;
static set(object: Decimal.Config): Decimal.Constructor;
static sign(n: Decimal.Value): number;
static sin(n: Decimal.Value): Decimal;
static sinh(n: Decimal.Value): Decimal;
static sqrt(n: Decimal.Value): Decimal;
static sub(x: Decimal.Value, y: Decimal.Value): Decimal;
static sum(...n: Decimal.Value[]): Decimal;
static tan(n: Decimal.Value): Decimal;
static tanh(n: Decimal.Value): Decimal;
static trunc(n: Decimal.Value): Decimal;
static readonly default?: Decimal.Constructor;
static readonly Decimal?: Decimal.Constructor;
static readonly precision: number;
static readonly rounding: Decimal.Rounding;
static readonly toExpNeg: number;
static readonly toExpPos: number;
static readonly minE: number;
static readonly maxE: number;
static readonly crypto: boolean;
static readonly modulo: Decimal.Modulo;
static readonly ROUND_UP: 0;
static readonly ROUND_DOWN: 1;
static readonly ROUND_CEIL: 2;
static readonly ROUND_FLOOR: 3;
static readonly ROUND_HALF_UP: 4;
static readonly ROUND_HALF_DOWN: 5;
static readonly ROUND_HALF_EVEN: 6;
static readonly ROUND_HALF_CEIL: 7;
static readonly ROUND_HALF_FLOOR: 8;
static readonly EUCLID: 9;
}
declare type Exact<A, W> = (A extends unknown ? (W extends A ? {
[K in keyof A]: Exact<A[K], W[K]>;
@@ -38,15 +299,9 @@ declare type GetRuntimeOutput = {
isEdge: boolean;
};
export { isAnyNull }
export { isDbNull }
export { isJsonNull }
export { isObjectEnumValue }
export { JsonNull }
declare class JsonNull extends NullTypesEnumValue {
#private;
}
/**
* Generates more strict variant of an enum which, unlike regular enum,
@@ -68,7 +323,32 @@ export declare function makeStrictEnum<T extends Record<PropertyKey, string | nu
declare type Narrowable = string | number | bigint | boolean | [];
export { NullTypes }
declare class NullTypesEnumValue extends ObjectEnumValue {
_getNamespace(): string;
}
/**
* Base class for unique values of object-valued enums.
*/
declare abstract class ObjectEnumValue {
constructor(arg?: symbol);
abstract _getNamespace(): string;
_getName(): string;
toString(): string;
}
export declare const objectEnumValues: {
classes: {
DbNull: typeof DbNull;
JsonNull: typeof JsonNull;
AnyNull: typeof AnyNull;
};
instances: {
DbNull: DbNull;
JsonNull: JsonNull;
AnyNull: AnyNull;
};
};
declare type Operation = 'findFirst' | 'findFirstOrThrow' | 'findUnique' | 'findUniqueOrThrow' | 'findMany' | 'create' | 'createMany' | 'createManyAndReturn' | 'update' | 'updateMany' | 'updateManyAndReturn' | 'upsert' | 'delete' | 'deleteMany' | 'aggregate' | 'count' | 'groupBy' | '$queryRaw' | '$executeRaw' | '$queryRawUnsafe' | '$executeRawUnsafe' | 'findRaw' | 'aggregateRaw' | '$runCommandRaw';

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,42 +5,51 @@ generator client {
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Task {
id String @id @default(uuid())
title String
description String
status String // Enum: TaskStatus (OPEN, EXECUTING, VERIFYING, COMPLETED, FAILED, etc)
difficulty String // Enum: TaskDifficulty (HELLO_WORLD, COMPONENT, VIEW, EPIC)
scope_clarity_score Float
error_classification String? // Enum: TaskErrorClassification
reward_amount Int // Stored in cents
reward_currency String // USD, TWD, USDC
acceptance_criteria Json // Contains validation_mode, test_file_content, rules
required_stack String[]
retry_count Int @default(0)
stripe_payment_intent_id String?
expires_at DateTime?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
id String @id @default(uuid())
title String
description String
status String // Enum: TaskStatus (OPEN, EXECUTING, VERIFYING, COMPLETED, FAILED, etc)
difficulty String // Enum: TaskDifficulty (HELLO_WORLD, COMPONENT, VIEW, EPIC)
scope_clarity_score Float
error_classification String? // Enum: TaskErrorClassification
reward_amount Int // Stored in cents
reward_currency String // USD, TWD, USDC
acceptance_criteria Json // Contains validation_mode, test_file_content, rules
required_stack String[]
retry_count Int @default(0)
stripe_payment_intent_id String?
stripe_checkout_session_id String? // Used for Scout flow
expires_at DateTime?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
scout_id String?
scout_agent AgentProfile? @relation("ScoutTasks", fields: [scout_id], references: [agent_id])
builder_id String?
builder_agent AgentProfile? @relation("BuilderTasks", fields: [builder_id], references: [agent_id])
claims Claim[]
submissions Submission[]
}
model Claim {
id String @id @default(uuid())
id String @id @default(uuid())
task_id String
task Task @relation(fields: [task_id], references: [id])
task Task @relation(fields: [task_id], references: [id])
agent_id String
agent AgentProfile @relation(fields: [agent_id], references: [agent_id])
developer_wallet String
status String // EXECUTING, CANCELLED, VERIFYING, COMPLETED
claim_token String @unique // Idempotency token for this claim
claim_token String @unique // Idempotency token for this claim
held_amount Int
held_currency String
expires_at DateTime
created_at DateTime @default(now())
updated_at DateTime @updatedAt
created_at DateTime @default(now())
updated_at DateTime @updatedAt
submissions Submission[]
}
@@ -99,3 +108,17 @@ model LedgerEntry {
created_at DateTime @default(now())
updated_at DateTime @updatedAt
}
model AgentProfile {
id String @id @default(uuid())
agent_id String @unique
type String // BUILDER or SCOUT
wallet_address String?
status String // WHITELISTED, BANNED, PENDING
created_at DateTime @default(now())
updated_at DateTime @updatedAt
tasks_as_scout Task[] @relation("ScoutTasks")
tasks_as_builder Task[] @relation("BuilderTasks")
claims Claim[]
}

View File

@@ -2,4 +2,4 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
export default import('./query_compiler_fast_bg.wasm?module')
export default import('./query_engine_bg.wasm?module')

View File

@@ -2,4 +2,4 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
export default import('./query_compiler_fast_bg.wasm')
export default import('./query_engine_bg.wasm')

View File

@@ -0,0 +1 @@
export * from "./default"

File diff suppressed because one or more lines are too long

View File

@@ -22,10 +22,16 @@ model Task {
required_stack String[]
retry_count Int @default(0)
stripe_payment_intent_id String?
stripe_checkout_session_id String? // Used for Scout flow
expires_at DateTime?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
scout_id String?
scout_agent AgentProfile? @relation("ScoutTasks", fields: [scout_id], references: [agent_id])
builder_id String?
builder_agent AgentProfile? @relation("BuilderTasks", fields: [builder_id], references: [agent_id])
claims Claim[]
submissions Submission[]
}
@@ -34,6 +40,8 @@ model Claim {
id String @id @default(uuid())
task_id String
task Task @relation(fields: [task_id], references: [id])
agent_id String
agent AgentProfile @relation(fields: [agent_id], references: [agent_id])
developer_wallet String
status String // EXECUTING, CANCELLED, VERIFYING, COMPLETED
claim_token String @unique // Idempotency token for this claim
@@ -100,3 +108,17 @@ model LedgerEntry {
created_at DateTime @default(now())
updated_at DateTime @updatedAt
}
model AgentProfile {
id String @id @default(uuid())
agent_id String @unique
type String // BUILDER or SCOUT
wallet_address String?
status String // WHITELISTED, BANNED, PENDING
created_at DateTime @default(now())
updated_at DateTime @updatedAt
tasks_as_scout Task[] @relation("ScoutTasks")
tasks_as_builder Task[] @relation("BuilderTasks")
claims Claim[]
}

View File

@@ -65,11 +65,19 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
case "claim_task": {
const parsed = ClaimTaskRequestSchema.parse(body);
// Verify Agent Whitelist
const agent = await prisma.agentProfile.findUnique({
where: { agent_id: parsed.agent_id }
});
if (!agent || agent.status !== "WHITELISTED") {
return NextResponse.json({ error: "Forbidden: Agent is not whitelisted" }, { status: 403 });
}
const claim = await prisma.$transaction(async (tx) => {
const updated = await tx.task.updateMany({
where: { id: parsed.task_id, status: TaskStatus.OPEN },
data: { status: TaskStatus.EXECUTING }
data: { status: TaskStatus.EXECUTING, builder_id: agent.agent_id }
});
if (updated.count === 0) {
@@ -81,6 +89,7 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
const newClaim = await tx.claim.create({
data: {
task_id: task.id,
agent_id: agent.agent_id,
developer_wallet: parsed.developer_wallet,
status: TaskStatus.EXECUTING,
claim_token: crypto.randomUUID(),

View File

@@ -0,0 +1,106 @@
import { NextRequest, NextResponse } from "next/server";
import { ScoutDraftRequestSchema, ScoutDraftResponseSchema, TaskStatus } from "@agent-bounty/contracts";
import { prisma } from "@/lib/prisma";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
apiVersion: "2026-05-27.dahlia",
});
export async function POST(request: NextRequest) {
const authHeader = request.headers.get("Authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return NextResponse.json({ error: "Unauthorized: Missing Bearer token" }, { status: 401 });
}
const token = authHeader.split(" ")[1];
if (process.env.API_KEY && token !== process.env.API_KEY) {
return NextResponse.json({ error: "Forbidden: Invalid API Key" }, { status: 403 });
}
try {
const body = await request.json();
const parsed = ScoutDraftRequestSchema.parse(body);
// Validate scout_id exists and is whitelisted
const scout = await prisma.agentProfile.findUnique({
where: { agent_id: parsed.scout_id }
});
if (!scout || scout.status !== "WHITELISTED") {
return NextResponse.json({ error: "Forbidden: Scout Agent is not whitelisted" }, { status: 403 });
}
// Create DRAFT task
const task = await prisma.task.create({
data: {
title: parsed.title,
description: parsed.description,
status: TaskStatus.DRAFT,
difficulty: "COMPONENT", // Defaulting for Phase 1
scope_clarity_score: 1.0,
reward_amount: parsed.reward_amount,
reward_currency: parsed.reward_currency,
required_stack: parsed.required_stack,
scout_id: scout.agent_id,
acceptance_criteria: {
validation_mode: "VITEST_UNIT",
test_file_content: parsed.test_file_content,
}
}
});
// Create Stripe Checkout Session
// We do a manual capture session so the funds are only captured when Judge passes
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
mode: "payment",
line_items: [
{
price_data: {
currency: parsed.reward_currency.toLowerCase(),
product_data: {
name: `VibeWork Task: ${parsed.title}`,
description: "Auth-Hold. Funds will only be captured when task is judged PASS.",
},
unit_amount: parsed.reward_amount,
},
quantity: 1,
},
],
payment_intent_data: {
capture_method: "manual",
metadata: {
task_id: task.id,
scout_id: scout.agent_id,
}
},
// You should set these to actual frontend URLs
success_url: `${process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'}/tasks/${task.id}?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'}/tasks/create`,
});
// Save session ID so webhook can find it
await prisma.task.update({
where: { id: task.id },
data: { stripe_checkout_session_id: session.id }
});
const responseData = {
task_id: task.id,
checkout_url: session.url!,
status: TaskStatus.DRAFT,
};
ScoutDraftResponseSchema.parse(responseData); // strict output validation
return NextResponse.json(responseData);
} catch (error: any) {
console.error("[Scout API Error]", error);
if (error.name === "ZodError") {
return NextResponse.json({ error_type: "InvalidParams", message: error.errors }, { status: 400 });
}
return NextResponse.json({ error_type: "InternalError", message: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,62 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import Stripe from "stripe";
import { TaskStatus } from "@agent-bounty/contracts";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
apiVersion: "2026-05-27.dahlia",
});
export async function POST(request: NextRequest) {
const payload = await request.text();
const signature = request.headers.get("stripe-signature");
if (!signature || !process.env.STRIPE_WEBHOOK_SECRET) {
return NextResponse.json({ error: "Missing signature or webhook secret" }, { status: 400 });
}
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
payload,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err: any) {
console.error(`[Webhook Error]`, err.message);
return NextResponse.json({ error: `Webhook Error: ${err.message}` }, { status: 400 });
}
try {
if (event.type === "checkout.session.completed") {
const session = event.data.object as Stripe.Checkout.Session;
const task = await prisma.task.findFirst({
where: { stripe_checkout_session_id: session.id }
});
if (!task) {
console.error(`[Webhook] Task not found for session: ${session.id}`);
return NextResponse.json({ received: true });
}
// Payment is authorized (Auth Hold)
// Save the payment_intent_id and set status to OPEN
await prisma.task.update({
where: { id: task.id },
data: {
stripe_payment_intent_id: session.payment_intent as string,
status: TaskStatus.OPEN
}
});
console.log(`[Webhook] Task ${task.id} is now OPEN. Payment Intent: ${session.payment_intent}`);
}
return NextResponse.json({ received: true });
} catch (error: any) {
console.error("[Webhook Processing Error]", error);
return NextResponse.json({ error: "Internal Error" }, { status: 500 });
}
}

View File

@@ -1,7 +1,10 @@
import { Prisma } from "../../prisma/generated/client";
import Stripe from "stripe";
// In Phase 2, we use a Mock implementation for Stripe to ensure our DB state machine
// and idempotency rules are solid before integrating the real Stripe SDK.
// Initialize Stripe with the secret key from env
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
apiVersion: "2026-05-27.dahlia", // Use latest or your specific API version
});
export async function authHold(
tx: Prisma.TransactionClient,
@@ -20,17 +23,23 @@ export async function authHold(
throw new Error(`Previous authHold failed for idempotencyKey: ${idempotencyKey}`);
}
// --- MOCK STRIPE CALL ---
// In real life: const intent = await stripe.paymentIntents.create({ amount, currency, payment_method_types: ['card'], capture_method: 'manual', metadata: { taskId, wallet } });
const mockStripeObjectId = `pi_mock_hold_${crypto.randomUUID()}`;
// ------------------------
// Check if we already have a payment intent for this task
const task = await tx.task.findUnique({ where: { id: taskId } });
if (!task || !task.stripe_payment_intent_id) {
throw new Error("Task does not have a stripe_payment_intent_id. It must be created via Scout checkout first.");
}
// In Phase 1 real flow, the Auth Hold is done by the user before the task becomes OPEN.
// When an AI claims the task, we don't actually create a new PaymentIntent.
// We just verify the PaymentIntent has enough funds or is capturable.
// To keep it simple, we just record that the claim successfully locked the existing intent
return await tx.ledgerEntry.create({
data: {
task_id: taskId,
phase: "AUTH_HOLD",
idempotency_key: idempotencyKey,
stripe_object_id: mockStripeObjectId,
stripe_object_id: task.stripe_payment_intent_id,
response_status: "SUCCESS",
http_status: 200,
},
@@ -50,27 +59,47 @@ export async function capturePayment(
throw new Error(`Previous capturePayment failed for idempotencyKey: ${idempotencyKey}`);
}
// We should find the AUTH_HOLD record to get the intent ID in real life
const holdEntry = await tx.ledgerEntry.findFirst({
where: { task_id: taskId, phase: "AUTH_HOLD", response_status: "SUCCESS" },
const task = await tx.task.findUnique({ where: { id: taskId } });
if (!task || !task.stripe_payment_intent_id) {
throw new Error("Cannot capture without a valid stripe_payment_intent_id on the task");
}
const claim = await tx.claim.findFirst({
where: { task_id: taskId, status: "COMPLETED" },
orderBy: { created_at: "desc" }
});
if (!holdEntry || !holdEntry.stripe_object_id) {
throw new Error("Cannot capture without a successful AUTH_HOLD");
if (!claim) {
throw new Error("Cannot capture without a COMPLETED claim");
}
// --- MOCK STRIPE CALL ---
// In real life: const capture = await stripe.paymentIntents.capture(holdEntry.stripe_object_id);
const mockStripeObjectId = `ch_mock_capture_${crypto.randomUUID()}`;
// ------------------------
let capturedIntent;
try {
// Perform real Stripe capture
capturedIntent = await stripe.paymentIntents.capture(task.stripe_payment_intent_id, undefined, {
idempotencyKey
});
} catch (error: any) {
// Record failed capture
await tx.ledgerEntry.create({
data: {
task_id: taskId,
phase: "CAPTURE",
idempotency_key: idempotencyKey,
stripe_object_id: task.stripe_payment_intent_id,
response_status: "FAILED",
http_status: error.statusCode || 500,
},
});
throw error;
}
return await tx.ledgerEntry.create({
data: {
task_id: taskId,
phase: "CAPTURE",
idempotency_key: idempotencyKey,
stripe_object_id: mockStripeObjectId,
stripe_object_id: capturedIntent.id,
response_status: "SUCCESS",
http_status: 200,
},
@@ -90,13 +119,8 @@ export async function releasePayment(
throw new Error(`Previous releasePayment failed for idempotencyKey: ${idempotencyKey}`);
}
const holdEntry = await tx.ledgerEntry.findFirst({
where: { task_id: taskId, phase: "AUTH_HOLD", response_status: "SUCCESS" },
orderBy: { created_at: "desc" }
});
if (!holdEntry || !holdEntry.stripe_object_id) {
// If there was no hold to begin with, releasing is a no-op but we log it as SUCCESS
const task = await tx.task.findUnique({ where: { id: taskId } });
if (!task || !task.stripe_payment_intent_id) {
return await tx.ledgerEntry.create({
data: {
task_id: taskId,
@@ -109,17 +133,31 @@ export async function releasePayment(
});
}
// --- MOCK STRIPE CALL ---
// In real life: const cancel = await stripe.paymentIntents.cancel(holdEntry.stripe_object_id);
const mockStripeObjectId = `re_mock_release_${crypto.randomUUID()}`;
// ------------------------
let canceledIntent;
try {
canceledIntent = await stripe.paymentIntents.cancel(task.stripe_payment_intent_id, undefined, {
idempotencyKey
});
} catch (error: any) {
await tx.ledgerEntry.create({
data: {
task_id: taskId,
phase: "RELEASE",
idempotency_key: idempotencyKey,
stripe_object_id: task.stripe_payment_intent_id,
response_status: "FAILED",
http_status: error.statusCode || 500,
},
});
throw error;
}
return await tx.ledgerEntry.create({
data: {
task_id: taskId,
phase: "RELEASE",
idempotency_key: idempotencyKey,
stripe_object_id: mockStripeObjectId,
stripe_object_id: canceledIntent.id,
response_status: "SUCCESS",
http_status: 200,
},

View File

@@ -55,6 +55,7 @@
* ARCHIVED → (終態)
*/
export const TaskStatus = {
DRAFT: "DRAFT",
OPEN: "OPEN",
EXECUTING: "EXECUTING",
VERIFYING: "VERIFYING",

View File

@@ -82,6 +82,7 @@ export const TaskBountySchema = z.object({
title: z.string().min(5).max(120),
description: z.string().min(20).max(2000),
status: z.enum([
TaskStatus.DRAFT,
TaskStatus.OPEN,
TaskStatus.EXECUTING,
TaskStatus.VERIFYING,
@@ -137,6 +138,7 @@ export const TaskBountySchema = z.object({
export const ClaimTaskRequestSchema = z.object({
task_id: UUIDSchema,
agent_id: z.string().min(1, "必須提供 agent_id 進行白名單驗證"),
/** Agent 收款錢包Stripe Connect account 或 EVM 地址) */
developer_wallet: z
.string()
@@ -286,9 +288,25 @@ export const SettlementLedgerEntrySchema = z.object({
});
// ─────────────────────────────────────────────
// Lead SchemaScout 導流任務草案)
// Scout Draft / Lead Schemas
// ─────────────────────────────────────────────
export const ScoutDraftRequestSchema = z.object({
scout_id: z.string().min(1, "必須提供 scout_id 進行歸因"),
title: z.string().min(5).max(120),
description: z.string().min(20).max(2000),
reward_amount: MoneyAmountSchema,
reward_currency: z.enum([SupportedCurrency.USD, SupportedCurrency.TWD]),
required_stack: z.array(z.string()).default(["React", "Tailwind CSS"]),
test_file_content: z.string().min(20, "必須提供測試檔以便自動驗收"),
});
export const ScoutDraftResponseSchema = z.object({
task_id: UUIDSchema,
checkout_url: z.string().url(),
status: z.literal(TaskStatus.DRAFT),
});
export const LeadSchema = z.object({
lead_id: UUIDSchema,
scout_agent_id: z.string().optional(),

32
pnpm-lock.yaml generated
View File

@@ -35,6 +35,9 @@ importers:
react-dom:
specifier: 19.2.4
version: 19.2.4(react@19.2.4)
stripe:
specifier: ^22.2.0
version: 22.2.0(@types/node@20.19.42)
zod:
specifier: ^3.23.0
version: 3.25.76
@@ -2583,6 +2586,15 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
stripe@22.2.0:
resolution: {integrity: sha512-WFGpMOom9QZqso1kcnSwJsCdC1QHDlMoCOxBZRf3JraMzhkfw7dgSdD2a1CFZrqC+mzAfqeEtYILrZhWKIDruA==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
styled-jsx@5.1.6:
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
engines: {node: '>= 12.0.0'}
@@ -4182,8 +4194,8 @@ snapshots:
'@next/eslint-plugin-next': 16.2.7
eslint: 9.39.4(jiti@2.7.0)
eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.7.0))
@@ -4205,7 +4217,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)):
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
@@ -4216,22 +4228,22 @@ snapshots:
tinyglobby: 0.2.17
unrs-resolver: 1.12.2
optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0))
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.13.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)):
eslint-module-utils@2.13.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
eslint: 9.39.4(jiti@2.7.0)
eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0))
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)):
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -4242,7 +4254,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.4(jiti@2.7.0)
eslint-import-resolver-node: 0.3.10
eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))
eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0))
hasown: 2.0.4
is-core-module: 2.16.2
is-glob: 4.0.3
@@ -5499,6 +5511,10 @@ snapshots:
strip-json-comments@3.1.1: {}
stripe@22.2.0(@types/node@20.19.42):
optionalDependencies:
'@types/node': 20.19.42
styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.4):
dependencies:
client-only: 0.0.1

40
setup_188.sh Normal file
View File

@@ -0,0 +1,40 @@
#!/bin/bash
certbot certonly --webroot -w /var/www/html -d agent.wooo.work --non-interactive --agree-tos --account 957a0a8ba3c1393f153d98b8aa7e6c07
cat << 'NGINX_CONF_SSL' > /etc/nginx/sites-available/agent.wooo.work.conf
server {
listen 80;
server_name agent.wooo.work;
location /.well-known/acme-challenge/ {
root /var/www/html;
}
location / {
return 301 https://$server_name$request_uri;
}
}
server {
listen 443 ssl http2;
server_name agent.wooo.work;
ssl_certificate /etc/letsencrypt/live/agent.wooo.work/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/agent.wooo.work/privkey.pem;
location / {
proxy_pass http://192.168.0.110;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
NGINX_CONF_SSL
nginx -t && systemctl reload nginx