Add OpenPayd marketplace admin integration
Some checks failed
API CI / API Lint (push) Successful in 38s
API CI / API Type Check (push) Failing after 43s
API CI / API Test (push) Successful in 56s
API CI / API Build (push) Failing after 45s
API CI / Build Docker Image (push) Has been skipped
CD Pipeline / Deploy to Staging (push) Failing after 23s
CI Pipeline / Lint and Type Check (push) Failing after 33s
CI Pipeline / Build (push) Has been skipped
CI Pipeline / Test Backend (push) Failing after 1m21s
CI Pipeline / Test Frontend (push) Failing after 32s
CI Pipeline / Security Scan (push) Failing after 1m19s
Deploy to Staging / Deploy to Staging (push) Failing after 27s
Portal CI / Portal Lint (push) Failing after 23s
Portal CI / Portal Type Check (push) Failing after 20s
Portal CI / Portal Test (push) Failing after 23s
Portal CI / Portal Build (push) Failing after 23s
Test Suite / frontend-tests (push) Failing after 31s
Test Suite / api-tests (push) Failing after 45s
Test Suite / blockchain-tests (push) Failing after 31s
Type Check / type-check (map[directory:. name:root]) (push) Failing after 19s
Type Check / type-check (map[directory:api name:api]) (push) Failing after 19s
Type Check / type-check (map[directory:portal name:portal]) (push) Failing after 21s
CD Pipeline / Deploy to Production (push) Has been skipped
Some checks failed
API CI / API Lint (push) Successful in 38s
API CI / API Type Check (push) Failing after 43s
API CI / API Test (push) Successful in 56s
API CI / API Build (push) Failing after 45s
API CI / Build Docker Image (push) Has been skipped
CD Pipeline / Deploy to Staging (push) Failing after 23s
CI Pipeline / Lint and Type Check (push) Failing after 33s
CI Pipeline / Build (push) Has been skipped
CI Pipeline / Test Backend (push) Failing after 1m21s
CI Pipeline / Test Frontend (push) Failing after 32s
CI Pipeline / Security Scan (push) Failing after 1m19s
Deploy to Staging / Deploy to Staging (push) Failing after 27s
Portal CI / Portal Lint (push) Failing after 23s
Portal CI / Portal Type Check (push) Failing after 20s
Portal CI / Portal Test (push) Failing after 23s
Portal CI / Portal Build (push) Failing after 23s
Test Suite / frontend-tests (push) Failing after 31s
Test Suite / api-tests (push) Failing after 45s
Test Suite / blockchain-tests (push) Failing after 31s
Type Check / type-check (map[directory:. name:root]) (push) Failing after 19s
Type Check / type-check (map[directory:api name:api]) (push) Failing after 19s
Type Check / type-check (map[directory:portal name:portal]) (push) Failing after 21s
CD Pipeline / Deploy to Production (push) Has been skipped
This commit is contained in:
114
api/src/routes/openpayd-connector.ts
Normal file
114
api/src/routes/openpayd-connector.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
|
||||
import { requireTenant } from '../middleware/tenant-auth.js'
|
||||
import { marketplaceSubscriptionService } from '../services/marketplace-subscription.js'
|
||||
|
||||
const PRODUCT_SLUG = 'phoenix-openpayd-connector'
|
||||
const ENTITLEMENT_KEY = 'OPENPAYD_CONNECTOR_ENTITLED'
|
||||
const CONNECTOR_BASE =
|
||||
process.env.OPENPAYD_CONNECTOR_INTERNAL_URL || 'https://openpayd-connector.sankofa.nexus'
|
||||
|
||||
const OPERATOR_COMMANDS = new Set([
|
||||
'probeEnvironment',
|
||||
'rotateCredential',
|
||||
'setProductionMutatingGate',
|
||||
'suspendTenant',
|
||||
'offboardTenant',
|
||||
'exportComplianceEvidencePack'
|
||||
])
|
||||
|
||||
const TENANT_COMMANDS = new Set([
|
||||
'getStatus',
|
||||
'syncPaymentAccounts',
|
||||
'getBeneficiaryRequiredDetails',
|
||||
'validateBeneficiary',
|
||||
'createLinkedClient',
|
||||
'createAccount',
|
||||
'createBankBeneficiary',
|
||||
'createPayout',
|
||||
'initiateOpenBankingPayment'
|
||||
])
|
||||
|
||||
function tenantFromRequest(request: FastifyRequest, reply: FastifyReply) {
|
||||
const headerTenant = (request.headers['x-tenant-id'] as string | undefined)?.trim()
|
||||
if (headerTenant && request.tenantContext) {
|
||||
request.tenantContext = { ...request.tenantContext, tenantId: headerTenant }
|
||||
}
|
||||
return requireTenant(request, reply)
|
||||
}
|
||||
|
||||
function isOperator(context: ReturnType<typeof tenantFromRequest>): boolean {
|
||||
const role = `${context.role || ''}`.toLowerCase()
|
||||
const tenantRole = `${context.tenantRole || ''}`.toLowerCase()
|
||||
return context.isSystemAdmin || role === 'admin' || tenantRole === 'tenant-admin' || tenantRole === 'admin'
|
||||
}
|
||||
|
||||
async function entitlementState(tenantId: string) {
|
||||
const entitlements = await marketplaceSubscriptionService.getTenantEntitlements(tenantId)
|
||||
const row = entitlements.find((entitlement) => entitlement.entitlementKey === ENTITLEMENT_KEY)
|
||||
return {
|
||||
entitlement: row,
|
||||
entitled: Boolean(row && ['ACTIVE', 'PENDING', 'REQUEST_ONLY'].includes(row.status)),
|
||||
active: row?.status === 'ACTIVE'
|
||||
}
|
||||
}
|
||||
|
||||
async function proxyConnector(command: string, body: Record<string, unknown>) {
|
||||
const res = await fetch(`${CONNECTOR_BASE.replace(/\/$/, '')}/v1/commands/${command}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
const data = await res.json().catch(() => ({}))
|
||||
return { status: res.status, data }
|
||||
}
|
||||
|
||||
export async function registerOpenPaydConnectorRoutes(fastify: FastifyInstance) {
|
||||
fastify.get('/api/v1/openpayd/status', async (request, reply) => {
|
||||
const context = tenantFromRequest(request, reply)
|
||||
const tenantId = context.tenantId!
|
||||
const entitlements = await entitlementState(tenantId)
|
||||
if (!entitlements.entitled && !isOperator(context)) {
|
||||
return reply.code(403).send({ error: 'OpenPayd connector entitlement required', productSlug: PRODUCT_SLUG })
|
||||
}
|
||||
|
||||
const environment = ((request.query as { environment?: string }).environment || 'sandbox') as string
|
||||
const proxied = await proxyConnector('getStatus', { tenantId, environment })
|
||||
return reply.code(proxied.status).send({
|
||||
productSlug: PRODUCT_SLUG,
|
||||
entitlementKey: ENTITLEMENT_KEY,
|
||||
entitlementStatus: entitlements.entitlement?.status || 'NONE',
|
||||
connector: proxied.data
|
||||
})
|
||||
})
|
||||
|
||||
fastify.post('/api/v1/openpayd/commands/:command', async (request, reply) => {
|
||||
const context = tenantFromRequest(request, reply)
|
||||
const tenantId = context.tenantId!
|
||||
const command = (request.params as { command: string }).command
|
||||
const body = (request.body || {}) as Record<string, unknown>
|
||||
const entitlements = await entitlementState(tenantId)
|
||||
const operator = isOperator(context)
|
||||
|
||||
if (!entitlements.entitled && !operator) {
|
||||
return reply.code(403).send({ error: 'OpenPayd connector entitlement required', productSlug: PRODUCT_SLUG })
|
||||
}
|
||||
if (OPERATOR_COMMANDS.has(command) && !operator) {
|
||||
return reply.code(403).send({ error: 'Operator role required for OpenPayd command', command })
|
||||
}
|
||||
if (!OPERATOR_COMMANDS.has(command) && !TENANT_COMMANDS.has(command)) {
|
||||
return reply.code(404).send({ error: 'Unsupported OpenPayd command', command })
|
||||
}
|
||||
if (!operator && !entitlements.active && command !== 'getStatus') {
|
||||
return reply.code(403).send({ error: 'Active OpenPayd entitlement required', command })
|
||||
}
|
||||
|
||||
const proxied = await proxyConnector(command, {
|
||||
tenantId,
|
||||
environment: body.environment || 'sandbox',
|
||||
payload: body.payload || {},
|
||||
idempotencyKey: body.idempotencyKey,
|
||||
approvalRef: body.approvalRef
|
||||
})
|
||||
return reply.code(proxied.status).send(proxied.data)
|
||||
})
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { validateAllSecrets } from './lib/secret-validation'
|
||||
import { initializeFIPS } from './lib/crypto'
|
||||
import { getFastifyTLSOptions } from './lib/tls-config'
|
||||
import { registerPhoenixRailingRoutes } from './routes/phoenix-railing.js'
|
||||
import { registerOpenPaydConnectorRoutes } from './routes/openpayd-connector.js'
|
||||
import { printSchema } from 'graphql'
|
||||
|
||||
// Get TLS configuration (empty if certificates not available)
|
||||
@@ -98,11 +99,12 @@ async function startServer() {
|
||||
await apolloServer.start()
|
||||
|
||||
// Register GraphQL route
|
||||
fastify.post('/graphql', async (request, reply) => {
|
||||
return fastifyApolloHandler(apolloServer, {
|
||||
context: async () => createContext(request),
|
||||
})(request, reply)
|
||||
})
|
||||
fastify.post(
|
||||
'/graphql',
|
||||
fastifyApolloHandler(apolloServer, {
|
||||
context: async (request) => createContext(request),
|
||||
})
|
||||
)
|
||||
|
||||
// Health check endpoint
|
||||
fastify.get('/health', async () => {
|
||||
@@ -141,6 +143,7 @@ async function startServer() {
|
||||
|
||||
// Phoenix API Railing: /api/v1/infra/*, /api/v1/ve/*, /api/v1/health/* proxy + /api/v1/tenants/me/*
|
||||
await registerPhoenixRailingRoutes(fastify)
|
||||
await registerOpenPaydConnectorRoutes(fastify)
|
||||
|
||||
// Start Fastify server
|
||||
const port = parseInt(process.env.PORT || '4000', 10)
|
||||
@@ -161,4 +164,3 @@ async function startServer() {
|
||||
}
|
||||
|
||||
startServer()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user