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

This commit is contained in:
defiQUG
2026-07-20 19:05:43 -07:00
parent 7e915e8c10
commit d2a5d7684e
9 changed files with 590 additions and 14 deletions

View 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)
})
}

View File

@@ -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()