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
39 changes: 39 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Release

on:
push:
branches: [main]

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: write
issues: write
pull-requests: write

env:
NODE_VERSION: '20'

jobs:
release:
name: Release
runs-on: ubuntu-latest
if: github.repository == 'Smartdevs17/agenticpay'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false

- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}

- run: npm ci

- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx semantic-release
38 changes: 38 additions & 0 deletions .gitpod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
image:
file: .devcontainer/Dockerfile

tasks:
- name: Setup
init: |
npm ci --prefer-offline || npm install
if command -v rustup >/dev/null 2>&1; then
rustup target add wasm32-unknown-unknown
fi
(cd backend && npm run db:generate)
command: |
docker compose -f docker-compose.yml up -d postgres redis
npm run dev

ports:
- name: Frontend
port: 3000
onOpen: open-preview
- name: Backend API
port: 3001
onOpen: ignore
- name: PostgreSQL
port: 5432
onOpen: ignore
visibility: private
- name: Redis
port: 6379
onOpen: ignore
visibility: private

vscode:
extensions:
- dbaeumer.vscode-eslint
- esbenp.prettier-vscode
- bradlc.vscode-tailwindcss
- rust-lang.rust-analyzer
- Prisma.prisma
21 changes: 21 additions & 0 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md"
}
],
"@semantic-release/github",
[
"@semantic-release/git",
{
"assets": ["CHANGELOG.md"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
]
]
}
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Changelog

All notable changes to this project are documented in this file.

Entries are generated automatically from
[Conventional Commits](https://www.conventionalcommits.org/) by
[semantic-release](https://semantic-release.gitbook.io/) on every push to `main`.
See `.releaserc.json` and `.github/workflows/release.yml`.
4 changes: 4 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ RATE_LIMIT_ENTERPRISE=1000
RATE_LIMIT_WINDOW_MS=900000
COMPRESSION_THRESHOLD=1024

# Security headers
HSTS_MAX_AGE_SECONDS=31536000
PERMISSIONS_POLICY=camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), interest-cohort=()

# IP Allowlist (comma-separated CIDR ranges)
# IP_ALLOWLIST=10.0.0.0/8,192.168.1.0/24
# IP_ALLOWLIST_ENABLED=false
Expand Down
12 changes: 12 additions & 0 deletions backend/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ const envSchema = z.object({
DB_POOL_ACQUIRE_TIMEOUT_MS: z.string().default('30000'),
DB_POOL_MAX_USES: z.string().default('7500'),
DB_STATEMENT_TIMEOUT_MS: z.string().default('30000'),
HSTS_MAX_AGE_SECONDS: z.string().default('31536000'),
PERMISSIONS_POLICY: z
.string()
.default('camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), interest-cohort=()'),
});

const parsed = envSchema.safeParse(process.env);
Expand Down Expand Up @@ -77,6 +81,14 @@ export const config = {
statementTimeoutMs: Number(env.DB_STATEMENT_TIMEOUT_MS),
},
},
security: {
hsts: {
maxAge: Number(env.HSTS_MAX_AGE_SECONDS),
includeSubDomains: true,
preload: true,
},
permissionsPolicy: env.PERMISSIONS_POLICY,
},
} as const;

export type Config = typeof config;
7 changes: 6 additions & 1 deletion backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { stellarRouter } from './routes/stellar.js';
import { catalogRouter } from './routes/catalog.js';
import { jobsRouter } from './routes/jobs.js';
import { healthRouter } from './routes/health.js';
import { docsRouter } from './routes/docs.js';
import { queueRouter } from './routes/queue.js';
import { slaRouter } from './routes/sla.js';
import { startJobs, getJobScheduler } from './jobs/index.js';
Expand All @@ -31,7 +32,7 @@ import { pushRouter } from './routes/push.js';
import { ipAllowlistRouter } from './routes/ip-allowlist.js';
import { stripeRouter } from './routes/stripe.js';
import { ipAllowlistMiddleware, initIpAllowlist } from './middleware/ip-allowlist.js';
import { SecurityMiddleware, SecurityMonitor } from './middleware/security.js';
import { SecurityMiddleware, SecurityMonitor, securityHeadersMiddleware } from './middleware/security.js';
import { sanitizeInput, contentSecurityPolicy } from './middleware/sanitize.js';
import { notificationsRouter } from './routes/notifications.js';
import { auditRouter } from './routes/audit.js';
Expand Down Expand Up @@ -164,6 +165,7 @@ const invoiceLimiter = rateLimit({
legacyHeaders: false,
});

app.use(securityHeadersMiddleware());
app.use(
cors({
origin: config.cors.allowedOrigins,
Expand Down Expand Up @@ -234,6 +236,9 @@ app.use((req: Request, res: Response, next: NextFunction) => {
// Health & Readiness checks
app.use(healthRouter);

// Interactive API documentation & playground — Issue #758
app.use('/docs', docsRouter);

import { versionMiddleware } from './middleware/versioning.js';

import { portfolioRouter } from './routes/portfolio.js';
Expand Down
21 changes: 21 additions & 0 deletions backend/src/middleware/security.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,33 @@ import { Request, Response, NextFunction } from 'express';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import { InputSanitizer, sanitizeInput, contentSecurityPolicy, createSecurityRateLimit } from './sanitize';
import { config } from '../config';

/**
* Comprehensive Security Middleware Stack
* Implements defense-in-depth security measures
*/

/**
* Security headers middleware — HSTS and Permissions-Policy.
*
* Kept independent of the CSP-bearing helmet() configuration in
* `applySecurity()` so it can be wired into the app without pulling in
* CSP directives, which are managed separately.
*/
export function securityHeadersMiddleware() {
const hsts = helmet.hsts({
maxAge: config.security.hsts.maxAge,
includeSubDomains: config.security.hsts.includeSubDomains,
preload: config.security.hsts.preload,
});

return (req: Request, res: Response, next: NextFunction): void => {
res.setHeader('Permissions-Policy', config.security.permissionsPolicy);
hsts(req, res, next);
};
}

export class SecurityMiddleware {
private static instance: SecurityMiddleware;
private sanitizer: InputSanitizer;
Expand Down
156 changes: 156 additions & 0 deletions frontend/app/developers/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { BookOpen, Code2, ExternalLink, FileJson, Terminal } from 'lucide-react';

export const metadata: Metadata = {
title: 'Developer Portal | AgenticPay',
description:
'Explore the AgenticPay API playground, OpenAPI specification, and official SDKs for TypeScript, Python, and Go.',
};

const GITHUB_REPO = 'https://github.com/Smartdevs17/agenticpay';

const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api/v1';
// The interactive docs/playground are served from the API origin, outside of /api/v1.
const API_ORIGIN = API_BASE.replace(/\/api\/v1\/?$/, '');
const PLAYGROUND_URL = `${API_ORIGIN}/docs`;
const OPENAPI_SPEC_URL = `${API_ORIGIN}/docs/openapi.json`;

interface Sdk {
language: string;
install: string;
packagePath: string;
}

const SDKS: Sdk[] = [
{ language: 'TypeScript', install: 'npm install @agenticpay/sdk', packagePath: 'packages/sdk' },
{ language: 'Python', install: 'pip install agenticpay', packagePath: 'sdks/python' },
{ language: 'Go', install: 'go get github.com/Smartdevs17/agenticpay-sdk-go', packagePath: 'sdks/go' },
];

interface Guide {
title: string;
description: string;
file: string;
}

const GUIDES: Guide[] = [
{ title: 'SDK overview', description: 'Available SDKs, quick start, and authentication.', file: 'docs/sdk/README.md' },
{ title: 'Migrating from REST', description: 'Move from raw REST calls to the typed SDKs.', file: 'docs/sdk/MIGRATION-FROM-REST.md' },
{ title: 'Error handling', description: 'The SDK error hierarchy and how to handle it.', file: 'docs/sdk/ERROR-HANDLING.md' },
{ title: 'Testing', description: 'Mocking and testing code that uses the SDKs.', file: 'docs/sdk/TESTING.md' },
{ title: 'Versioning', description: 'SDK release and API versioning policy.', file: 'docs/sdk/VERSIONING.md' },
];

export default function DevelopersPage() {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50">
<main className="pt-20 pb-20">
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="mx-auto max-w-5xl">
<div className="overflow-hidden rounded-[2rem] border border-white/70 bg-white/90 shadow-xl shadow-blue-100/60 backdrop-blur-sm">
<div className="border-b border-slate-100 bg-gradient-to-r from-blue-600 via-cyan-500 to-indigo-600 px-6 py-12 text-white sm:px-10">
<div className="inline-flex items-center gap-2 rounded-full bg-white/15 px-4 py-2 text-sm font-medium">
<Code2 className="h-4 w-4" />
Developer Portal
</div>
<h1 className="mt-6 text-4xl font-bold tracking-tight sm:text-5xl">
Build on AgenticPay
</h1>
<p className="mt-4 max-w-3xl text-base leading-7 text-blue-50 sm:text-lg">
Explore the API in an interactive playground, browse the OpenAPI specification,
and get started with an official SDK.
</p>
<div className="mt-8 flex flex-wrap gap-3">
<a
href={PLAYGROUND_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-full bg-white px-5 py-3 text-sm font-semibold text-blue-700 shadow-sm transition-colors hover:bg-blue-50"
>
<Terminal className="h-4 w-4" />
Open API playground
</a>
<a
href={OPENAPI_SPEC_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-full bg-white/15 px-5 py-3 text-sm font-semibold text-white transition-colors hover:bg-white/25"
>
<FileJson className="h-4 w-4" />
OpenAPI spec (JSON)
</a>
</div>
</div>

<div className="space-y-12 px-6 py-10 sm:px-10 sm:py-12">
<section aria-labelledby="sdks-heading">
<h2 id="sdks-heading" className="text-2xl font-semibold tracking-tight text-slate-900">
Official SDKs
</h2>
<p className="mt-2 text-sm leading-6 text-slate-600">
Typed clients for the AgenticPay API, published from this repository.
</p>
<div className="mt-6 grid gap-4 sm:grid-cols-3">
{SDKS.map((sdk) => (
<div
key={sdk.language}
className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"
>
<h3 className="text-base font-semibold text-slate-900">{sdk.language}</h3>
<pre className="mt-3 overflow-x-auto rounded-lg bg-slate-900 px-3 py-2 text-xs text-slate-100">
<code>{sdk.install}</code>
</pre>
<Link
href={`${GITHUB_REPO}/tree/main/${sdk.packagePath}`}
target="_blank"
rel="noopener noreferrer"
className="mt-3 inline-flex items-center gap-1 text-sm font-medium text-blue-600 hover:text-blue-700"
>
View source
<ExternalLink className="h-3.5 w-3.5" />
</Link>
</div>
))}
</div>
</section>

<section aria-labelledby="guides-heading">
<h2 id="guides-heading" className="text-2xl font-semibold tracking-tight text-slate-900">
Guides
</h2>
<p className="mt-2 text-sm leading-6 text-slate-600">
Reference documentation for integrating with the AgenticPay SDKs and API.
</p>
<ul className="mt-6 divide-y divide-slate-100 rounded-2xl border border-slate-200 bg-white">
{GUIDES.map((guide) => (
<li key={guide.file}>
<Link
href={`${GITHUB_REPO}/blob/main/${guide.file}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between gap-4 px-5 py-4 transition-colors hover:bg-slate-50"
>
<span className="flex items-start gap-3">
<BookOpen className="mt-0.5 h-4 w-4 shrink-0 text-blue-600" />
<span>
<span className="block text-sm font-medium text-slate-900">
{guide.title}
</span>
<span className="block text-sm text-slate-500">{guide.description}</span>
</span>
</span>
<ExternalLink className="h-4 w-4 shrink-0 text-slate-400" />
</Link>
</li>
))}
</ul>
</section>
</div>
</div>
</div>
</div>
</main>
</div>
);
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"generate:api-hooks:watch": "node packages/api-hooks-generator/dist/cli.js --watch",
"analyze:bundle": "bash scripts/analyze-bundle.sh",
"lighthouse:ci": "lhci autorun --config=./lighthouse/lighthouse.config.json",
"log-viewer": "node packages/log-viewer/dist/cli.js"
"log-viewer": "node packages/log-viewer/dist/cli.js",
"release": "semantic-release"
},
"devDependencies": {
"turbo": "^1.10.16",
Expand Down
Loading