Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ NODE_ENV=development
PORT=3000
API_PREFIX=/api/v1

# OpenAPI / Swagger
# Swagger UI (/api/docs) and JSON export are always active in development.
# In production they are disabled by default — set to "true" to opt in.
SWAGGER_ENABLED=false

# Database
DATABASE_URL=postgresql://user:password@localhost:5432/alianStructure.db

Expand Down
58 changes: 57 additions & 1 deletion .github/workflows/build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,60 @@ jobs:
run: npx tsc --noEmit

- name: Build project
run: npm run build
run: npm run build

openapi:
name: Generate OpenAPI Spec & TypeScript Client
runs-on: ubuntu-latest
# Only run on pushes to main/master (not PRs) to avoid redundant artifact uploads.
if: github.event_name == 'push'
needs: build
env:
NODE_ENV: development
# Provide minimal env vars so the app can bootstrap without a real DB.
# The export script boots with abortOnError:false so it tolerates DB absence.
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/openapi_gen
JWT_SECRET: ci-openapi-export-secret
PORT: 3001
steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Export OpenAPI JSON
run: npm run openapi:export

- name: Set up Java (required by OpenAPI Generator CLI)
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'

- name: Generate TypeScript fetch client
run: |
npx @openapitools/openapi-generator-cli@2.13.4 generate \
-i docs/openapi.json \
-g typescript-fetch \
-o client \
--additional-properties=supportsES6=true,typescriptThreePlus=true

- name: Upload OpenAPI JSON artifact
uses: actions/upload-artifact@v4
with:
name: openapi-spec
path: docs/openapi.json
retention-days: 30

- name: Upload TypeScript client artifact
uses: actions/upload-artifact@v4
with:
name: typescript-client
path: client/
retention-days: 30
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,13 @@ dist
# Vite logs files
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

# Generated OpenAPI artifacts — produced by `npm run openapi:export`
docs/openapi.json

# Generated TypeScript client — produced by `npm run openapi:client`
client/

# OpenAPI Generator CLI cache
.openapi-generator/
openapitools.json
20 changes: 20 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
"seed:audit": "ts-node src/seeds/seed-audit-data.ts",
"docs:generate": "nest start --watch",
"docs:serve": "nest start",
"docs:build": "nest build && node dist/main.js"
"docs:build": "nest build && node dist/main.js",
"openapi:export": "cross-env NODE_ENV=development ts-node -r tsconfig-paths/register scripts/export-openapi.ts",
"openapi:client": "npm run openapi:export && npx @openapitools/openapi-generator-cli generate -i docs/openapi.json -g typescript-fetch -o client --additional-properties=supportsES6=true,typescriptThreePlus=true"
},
"dependencies": {
"@nestjs/axios": "^4.0.1",
Expand Down Expand Up @@ -127,6 +129,7 @@
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.1.2",
"jest": "^29.7.0",
"cross-env": "^7.0.3",
"nodemon": "^3.1.11",
"pg": "^8.22.0",
"pino-pretty": "^13.1.3",
Expand Down
70 changes: 70 additions & 0 deletions scripts/export-openapi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Standalone script that boots the NestJS app just long enough to generate
* and write the OpenAPI JSON document to docs/openapi.json, then exits.
*
* Usage:
* npx ts-node -r tsconfig-paths/register scripts/export-openapi.ts
* npm run openapi:export
*/

import { NestFactory } from "@nestjs/core";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import { writeFileSync, mkdirSync } from "fs";
import { join } from "path";
import { AppModule } from "../src/app.module";

async function exportOpenApi() {
// Silence NestJS bootstrap logs — we only want the artefact output
const app = await NestFactory.create(AppModule, { logger: false, abortOnError: false });

const config = new DocumentBuilder()
.setTitle("alian-structure Backend API")
.setDescription(
"Comprehensive API documentation for alian-structure backend services including " +
"agent management, oracle submissions, compute operations, and audit trails.",
)
.setVersion("1.0.0")
.setContact("alian-structure Team", "https://alian-structure.com", "api@alian-structure.com")
.setLicense("Apache 2.0", "https://www.apache.org/licenses/LICENSE-2.0")
.addServer("http://localhost:3001", "Development Server")
.addServer("https://api.alian-structure.com", "Production Server")
.addBearerAuth(
{ type: "http", scheme: "bearer", bearerFormat: "JWT", name: "JWT", description: "Enter JWT token", in: "header" },
"JWT-auth",
)
.addApiKey(
{ type: "apiKey", name: "X-API-Key", in: "header", description: "API key for service-to-service communication" },
"api-key",
)
.addTag("Health", "Liveness, readiness, and startup probes for Kubernetes orchestration")
.addTag("Authentication", "User authentication and authorization")
.addTag("Enhanced Authentication & KYC", "Enhanced auth with 2FA and KYC flows")
.addTag("Users", "User management operations")
.addTag("Oracle", "Oracle data submissions and payload management")
.addTag("Price Feed", "Aggregated on-chain price data")
.addTag("Audit", "Audit trail and logging")
.addTag("Profile", "User profile management")
.addTag("Info", "API health and meta-information")
.build();

const document = SwaggerModule.createDocument(app, config, {
deepScanRoutes: true,
operationIdFactory: (_controllerKey: string, methodKey: string) => methodKey,
});

// Write JSON
const outDir = join(__dirname, "..", "docs");
mkdirSync(outDir, { recursive: true });

const jsonPath = join(outDir, "openapi.json");
writeFileSync(jsonPath, JSON.stringify(document, null, 2), "utf8");
console.log(`✅ OpenAPI JSON written to ${jsonPath}`);

await app.close();
process.exit(0);
}

exportOpenApi().catch((err) => {
console.error("Failed to export OpenAPI spec:", err);
process.exit(1);
});
38 changes: 38 additions & 0 deletions src/blockchain/oracle/dto/payload-response.dto.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,65 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { PayloadStatus, PayloadType } from "../entities/signed-payload.entity";

/**
* Response DTO for payload operations
*/
export class PayloadResponseDto {
@ApiProperty({ description: "Unique payload UUID", example: "a1b2c3d4-1234-5678-90ef-ghijklmnopqr" })
id: string;

@ApiProperty({ description: "Type of payload", enum: PayloadType, example: PayloadType.PRICE_FEED })
payloadType: PayloadType;

@ApiProperty({ description: "Ethereum address that signed this payload", example: "0xAbCd1234567890abcdef1234567890abcdef1234" })
signerAddress: string;

@ApiProperty({ description: "Submission nonce", example: "42" })
nonce: string;

@ApiProperty({ description: "Raw payload data", type: "object", example: { token: "ETH", price: 3200.5 } })
payload: Record<string, any>;

@ApiProperty({ description: "Keccak256 hash of the payload", example: "0xabc123..." })
payloadHash: string;

@ApiProperty({ description: "EIP-712 structured data hash", example: "0xdef456..." })
structuredDataHash: string;

@ApiPropertyOptional({ description: "ECDSA signature (0x-prefixed, 132 chars)", nullable: true, example: "0x..." })
signature: string | null;

@ApiProperty({ description: "Payload expiry timestamp" })
expiresAt: Date;

@ApiProperty({ description: "Current submission status", enum: PayloadStatus, example: PayloadStatus.PENDING })
status: PayloadStatus;

@ApiPropertyOptional({ description: "On-chain transaction hash after submission", nullable: true, example: "0x..." })
transactionHash: string | null;

@ApiPropertyOptional({ description: "Block number when confirmed on-chain", nullable: true, example: "18500000" })
blockNumber: string | null;

@ApiProperty({ description: "Total number of submission attempts", example: 1 })
submissionAttempts: number;

@ApiPropertyOptional({ description: "Error message if submission failed", nullable: true })
errorMessage: string | null;

@ApiPropertyOptional({ description: "Optional metadata", nullable: true, type: "object" })
metadata: Record<string, any> | null;

@ApiProperty({ description: "Record creation timestamp" })
createdAt: Date;

@ApiProperty({ description: "Record last-updated timestamp" })
updatedAt: Date;

@ApiPropertyOptional({ description: "When submitted to blockchain", nullable: true })
submittedAt: Date | null;

@ApiPropertyOptional({ description: "When confirmed on-chain", nullable: true })
confirmedAt: Date | null;
}

Expand Down
10 changes: 10 additions & 0 deletions src/blockchain/oracle/dto/sign-payload.dto.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import { IsString, IsNotEmpty, Matches } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";

/**
* DTO for signing a payload with a private key
*/
export class SignPayloadDto {
@ApiProperty({
description: "UUID of the payload to sign",
example: "a1b2c3d4-1234-5678-90ef-ghijklmnopqr",
})
@IsString()
@IsNotEmpty()
payloadId: string;

@ApiProperty({
description: "Ethereum private key (0x-prefixed, 64 hex chars). NOTE: use client-side signing in production.",
example: "0x4c0883a69102937d6231471b5dbb6e538eba2ef68e5fd63f36fe1ef7e9bb4d7f",
pattern: "^0x[a-fA-F0-9]{64}$",
})
@IsString()
@IsNotEmpty()
@Matches(/^0x[a-fA-F0-9]{64}$/, {
Expand Down
16 changes: 16 additions & 0 deletions src/blockchain/oracle/dto/verify-signature.dto.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
import { IsString, IsNotEmpty, IsObject, Matches } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";

/**
* DTO for verifying a signature off-chain
*/
export class VerifySignatureDto {
@ApiProperty({
description: "Payload data that was originally signed",
type: "object",
example: { token: "ETH", price: 3200.5, timestamp: 1620000000000 },
})
@IsObject()
@IsNotEmpty()
payload: Record<string, any>;

@ApiProperty({
description: "ECDSA signature (0x-prefixed, 132 chars)",
example: "0x1234567890abcdef....",
pattern: "^0x[a-fA-F0-9]{130}$",
})
@IsString()
@IsNotEmpty()
@Matches(/^0x[a-fA-F0-9]{130}$/, {
message: "Signature must be a valid hex string with 0x prefix (132 chars)",
})
signature: string;

@ApiProperty({
description: "Expected signer Ethereum address (0x-prefixed, 40 hex chars)",
example: "0xAbCd1234567890abcdef1234567890abcdef1234",
pattern: "^0x[a-fA-F0-9]{40}$",
})
@IsString()
@IsNotEmpty()
@Matches(/^0x[a-fA-F0-9]{40}$/, {
Expand Down
Loading
Loading