chore: consolidate local WIP (repo cleanup 20260707)
Some checks failed
API CI / API Lint (push) Successful in 47s
API CI / API Type Check (push) Failing after 47s
API CI / API Test (push) Successful in 1m0s
API CI / API Build (push) Failing after 50s
API CI / Build Docker Image (push) Has been skipped
Build Crossplane Provider / build (push) Failing after 5m51s
CD Pipeline / Deploy to Staging (push) Failing after 29s
CI Pipeline / Lint and Type Check (push) Failing after 36s
CI Pipeline / Build (push) Has been skipped
CI Pipeline / Test Backend (push) Failing after 1m33s
CI Pipeline / Test Frontend (push) Failing after 30s
CI Pipeline / Security Scan (push) Failing after 1m16s
Crossplane Provider CI / Go Test (push) Failing after 3m23s
Crossplane Provider CI / Go Lint (push) Failing after 7m27s
Crossplane Provider CI / Go Build (push) Failing after 3m27s
Deploy to Staging / Deploy to Staging (push) Failing after 30s
Portal CI / Portal Lint (push) Failing after 21s
Portal CI / Portal Type Check (push) Failing after 21s
Portal CI / Portal Test (push) Failing after 21s
Portal CI / Portal Build (push) Failing after 22s
Test Suite / frontend-tests (push) Failing after 30s
Test Suite / api-tests (push) Failing after 49s
Test Suite / blockchain-tests (push) Failing after 30s
Type Check / type-check (map[directory:. name:root]) (push) Failing after 23s
Type Check / type-check (map[directory:api name:api]) (push) Failing after 21s
Type Check / type-check (map[directory:portal name:portal]) (push) Failing after 19s
Validate Configuration Files / validate (push) Failing after 1m52s
CD Pipeline / Deploy to Production (push) Has been skipped

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-07-07 09:41:34 -07:00
parent 477cf73005
commit 33d50fb91e
277 changed files with 44421 additions and 75 deletions

View File

@@ -0,0 +1,201 @@
{
"schemaVersion": "1.0.0",
"updated": "2026-07-02",
"description": "Canonical marketplace entitlement registry — maps product slugs to Keycloak/feature flags, SKUs, and onboarding runbooks",
"entitlementModel": "contract_po_first",
"billingNote": "Automated billing is roadmap (PUBLIC_SECTOR baseline G2). Grant entitlements via PO/contract then Keycloak flags.",
"products": [
{
"productSlug": "phoenix-ledger-service",
"entitlementKeys": ["PHOENIX_LEDGER_ENTITLED"],
"publisher": "phoenix-cloud-services",
"category": "LEDGER_SERVICES",
"manifestRef": null,
"onboardingRunbook": null,
"fulfillmentMode": "self_service"
},
{
"productSlug": "phoenix-identity-service",
"entitlementKeys": ["PHOENIX_IDENTITY_ENTITLED"],
"publisher": "phoenix-cloud-services",
"category": "IDENTITY_SERVICES",
"manifestRef": null,
"onboardingRunbook": null,
"fulfillmentMode": "self_service"
},
{
"productSlug": "phoenix-wallet-registry",
"entitlementKeys": ["PHOENIX_WALLET_ENTITLED"],
"publisher": "phoenix-cloud-services",
"category": "WALLET_SERVICES",
"manifestRef": null,
"onboardingRunbook": null,
"fulfillmentMode": "self_service"
},
{
"productSlug": "phoenix-tx-orchestrator",
"entitlementKeys": ["PHOENIX_TX_ORCH_ENTITLED"],
"publisher": "phoenix-cloud-services",
"category": "ORCHESTRATION_SERVICES",
"manifestRef": null,
"onboardingRunbook": null,
"fulfillmentMode": "self_service"
},
{
"productSlug": "phoenix-messaging-orchestrator",
"entitlementKeys": ["PHOENIX_MSG_ORCH_ENTITLED"],
"publisher": "phoenix-cloud-services",
"category": "ORCHESTRATION_SERVICES",
"manifestRef": null,
"onboardingRunbook": null,
"fulfillmentMode": "self_service"
},
{
"productSlug": "phoenix-voice-orchestrator",
"entitlementKeys": ["PHOENIX_VOICE_ORCH_ENTITLED"],
"publisher": "phoenix-cloud-services",
"category": "ORCHESTRATION_SERVICES",
"manifestRef": null,
"onboardingRunbook": null,
"fulfillmentMode": "self_service"
},
{
"productSlug": "phoenix-event-bus",
"entitlementKeys": ["PHOENIX_EVENT_BUS_ENTITLED"],
"publisher": "phoenix-cloud-services",
"category": "PLATFORM_SERVICES",
"manifestRef": null,
"onboardingRunbook": null,
"fulfillmentMode": "self_service"
},
{
"productSlug": "phoenix-audit-service",
"entitlementKeys": ["PHOENIX_AUDIT_ENTITLED"],
"publisher": "phoenix-cloud-services",
"category": "PLATFORM_SERVICES",
"manifestRef": null,
"onboardingRunbook": null,
"fulfillmentMode": "self_service"
},
{
"productSlug": "phoenix-observability",
"entitlementKeys": ["PHOENIX_OBSERVABILITY_ENTITLED"],
"publisher": "phoenix-cloud-services",
"category": "PLATFORM_SERVICES",
"manifestRef": null,
"onboardingRunbook": null,
"fulfillmentMode": "self_service"
},
{
"productSlug": "phoenix-b2b-integration-hub",
"displayName": "DBIS B2B Integration Hub",
"entitlementKeys": ["B2B_HUB_ENTITLED"],
"publisher": "phoenix-cloud-services",
"category": "PLATFORM_SERVICES",
"manifestRef": "config/b2b-integration-hub-marketplace.v1.json",
"onboardingRunbook": "docs/03-deployment/B2B_MARKETPLACE_TENANT_ONBOARDING_RUNBOOK.md",
"deployScript": "scripts/deployment/deploy-b2b-integration-stack-from-marketplace.sh",
"verifyCommand": "pnpm b2b:validate",
"catalogSkus": ["b2b-hub-lab", "b2b-hub-dedicated", "b2b-stack-bundle"],
"fulfillmentMode": "operator_provisioned",
"surfaces": {
"productPath": "/marketplace/products/phoenix-b2b-integration-hub",
"managePath": "/marketplace/entitlements/phoenix-b2b-integration-hub"
}
},
{
"productSlug": "phoenix-peppol-hosted-connector",
"displayName": "PEPPOL Hosted Connector",
"entitlementKeys": ["PEPPOL_HOSTED_ENTITLED"],
"publisher": "phoenix-cloud-services",
"category": "PLATFORM_SERVICES",
"manifestRef": "config/peppol-hosted-marketplace.v1.json",
"onboardingRunbook": "docs/03-deployment/B2B_MARKETPLACE_TENANT_ONBOARDING_RUNBOOK.md",
"catalogSkus": ["peppol-hosted-connector"],
"fulfillmentMode": "operator_provisioned",
"surfaces": {
"productPath": "/marketplace/products/phoenix-peppol-hosted-connector",
"managePath": "/marketplace/entitlements/phoenix-peppol-hosted-connector"
}
},
{
"productSlug": "phoenix-ebics-banking-gateway",
"displayName": "EBICS Banking Gateway",
"entitlementKeys": ["EBICS_GATEWAY_ENTITLED"],
"publisher": "phoenix-cloud-services",
"category": "FINANCIAL_MESSAGING",
"manifestRef": "config/ebics-gateway-marketplace.v1.json",
"onboardingRunbook": "docs/03-deployment/B2B_MARKETPLACE_TENANT_ONBOARDING_RUNBOOK.md",
"catalogSkus": ["ebics-gateway-dedicated"],
"fulfillmentMode": "operator_provisioned",
"surfaces": {
"productPath": "/marketplace/products/phoenix-ebics-banking-gateway",
"managePath": "/marketplace/entitlements/phoenix-ebics-banking-gateway"
}
},
{
"productSlug": "phoenix-chain138-participant-onboard",
"displayName": "Chain 138 Participant Onboarding",
"entitlementKeys": ["CHAIN138_ONBOARD_ENTITLED"],
"publisher": "phoenix-cloud-services",
"category": "BLOCKCHAIN_STACK",
"manifestRef": "config/chain138-onboard-marketplace.v1.json",
"onboardingRunbook": "docs/03-deployment/CHAIN138_ONBOARD_PORTAL_RUNBOOK.md",
"integrationDoc": "docs/11-references/CHAIN138_ONBOARD_SANKOFA_MARKETPLACE_INTEGRATION.md",
"catalogSkus": [
"chain138-onboard-portal-dedicated",
"chain138-besu-sentry-order",
"chain138-besu-core-rpc-order",
"chain138-besu-public-rpc-order",
"chain138-besu-private-rpc-order",
"chain138-besu-permissioned-rpc-order"
],
"fulfillmentMode": "request_only",
"surfaces": {
"productPath": "/marketplace/products/phoenix-chain138-participant-onboard",
"managePath": "/marketplace/entitlements/phoenix-chain138-participant-onboard"
}
},
{
"productSlug": "phoenix-x-road-interoperability",
"displayName": "Phoenix X-Road Interoperability",
"entitlementKeys": ["XROAD_INTEROP_ENTITLED"],
"publisher": "phoenix-cloud-services",
"category": "INTEROPERABILITY_SERVICES",
"manifestRef": null,
"programRef": "config/public-sector-program-manifest.json#x-road-global",
"onboardingRunbook": "docs/11-references/X_ROAD_SANKOFA_ECOSYSTEM_INTEGRATION.md",
"catalogSkus": ["x-road-interop-lab", "x-road-security-server-dedicated", "x-road-federation-connector"],
"fulfillmentMode": "request_only",
"status": "preview"
},
{
"productSlug": "phoenix-aegis-vault-cti",
"displayName": "AEGIS-VAULT by CyberSecur Global",
"entitlementKeys": [
"AEGIS_VAULT_ENTITLED",
"AEGIS_VAULT_ESSENTIALS",
"AEGIS_VAULT_PRO",
"AEGIS_VAULT_ENTERPRISE"
],
"tierEntitlements": {
"aegis-vault-essentials": "AEGIS_VAULT_ESSENTIALS",
"aegis-vault-professional": "AEGIS_VAULT_PRO",
"aegis-vault-enterprise": "AEGIS_VAULT_ENTERPRISE"
},
"publisher": "phoenix-cloud-services",
"partnerPublisher": "CyberSecur Global (AF-CSG-001)",
"category": "SECURITY_SERVICES",
"manifestRef": "config/aegis-vault-cti-marketplace.v1.json",
"onboardingRunbook": "docs/03-deployment/AEGIS_VAULT_MARKETPLACE_TENANT_ONBOARDING_RUNBOOK.md",
"deployScript": "scripts/deployment/deploy-aegis-vault-from-marketplace.sh",
"verifyCommand": "pnpm aegis-vault:validate",
"catalogSkus": ["aegis-vault-essentials", "aegis-vault-professional", "aegis-vault-enterprise"],
"fulfillmentMode": "operator_provisioned",
"surfaces": {
"productPath": "/marketplace/products/phoenix-aegis-vault-cti",
"managePath": "/marketplace/entitlements/phoenix-aegis-vault-cti"
}
}
]
}

3
api/pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,3 @@
allowBuilds:
'@apollo/protobufjs': set this to true or false
esbuild: set this to true or false

4
api/postcss.config.js Normal file
View File

@@ -0,0 +1,4 @@
/** Node API tests only — prevent Vitest from loading ../postcss.config.js (tailwind). */
export default {
plugins: {},
}

136
api/scripts/fix-ts6133.py Normal file
View File

@@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""Fix TS6133 unused variable/import errors reported by tsc."""
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
def run_tsc() -> list[str]:
result = subprocess.run(
[str(ROOT / "node_modules/.bin/tsc"), "--noEmit"],
cwd=ROOT,
capture_output=True,
text=True,
)
lines = (result.stdout + result.stderr).splitlines()
return [l for l in lines if "error TS6133" in l or "error TS6198" in l]
def parse_error(line: str) -> tuple[str, int, str, str] | None:
m = re.match(
r"^(src/[^:]+)\((\d+),(\d+)\): error TS(6133|6198): '([^']+)'",
line,
)
if not m:
return None
path, row, col, code, name = m.groups()
return path, int(row), int(col), name, code
def fix_file(path: Path, row: int, name: str, code: str) -> bool:
lines = path.read_text().splitlines(keepends=True)
if row < 1 or row > len(lines):
return False
idx = row - 1
line = lines[idx]
if code == "6198":
# All destructured elements unused — prefix each binding with _
new_line = re.sub(r"\{([^}]+)\}", lambda m: "{" + ", ".join(
(p.strip() if p.strip().startswith("_") else "_" + p.strip().split(":")[0].strip() + (":" + ":".join(p.strip().split(":")[1:]) if ":" in p.strip() else ""))
for p in m.group(1).split(",")
) + "}", line, count=1)
if new_line != line:
lines[idx] = new_line
path.write_text("".join(lines))
return True
return False
# Unused import — remove named import or whole import line
if re.search(rf"\bimport\b.*\b{re.escape(name)}\b", line):
if re.match(r"import\s+\{" , line):
names = re.search(r"\{([^}]+)\}", line)
if names:
parts = [p.strip() for p in names.group(1).split(",")]
kept = []
for p in parts:
base = p.split(" as ")[0].strip()
if base != name:
kept.append(p)
if not kept:
lines[idx] = ""
path.write_text("".join(lines))
return True
new_import = re.sub(r"\{[^}]+\}", "{" + ", ".join(kept) + "}", line)
lines[idx] = new_import
path.write_text("".join(lines))
return True
if re.match(rf"import\s+{re.escape(name)}\s", line) or re.match(rf"import\s+\*\s+as\s+{re.escape(name)}\s", line):
lines[idx] = ""
path.write_text("".join(lines))
return True
# Parameter or local — prefix with underscore
patterns = [
(rf"\(({re.escape(name)})\:", rf"(_{name}:"),
(rf",\s*{re.escape(name)}\:", rf", _{name}:"),
(rf"\(\s*{re.escape(name)}\s*\)", rf"(_{name})"),
(rf"\(\s*{re.escape(name)}\s*,", rf"(_{name},"),
(rf",\s*{re.escape(name)}\s*\)", rf", _{name})"),
(rf"\b(const|let)\s+{re.escape(name)}\b", rf"\1 _{name}"),
(rf"\basync\s+{re.escape(name)}\b", rf"async _{name}"),
(rf"function\s+{re.escape(name)}\b", rf"function _{name}"),
]
new_line = line
for pat, repl in patterns:
new_line2 = re.sub(pat, repl, new_line)
if new_line2 != new_line:
new_line = new_line2
break
else:
# fallback: word boundary prefix in destructuring { name }
new_line = re.sub(rf"\{{([^}}]*)\b{re.escape(name)}\b", rf"{{\1_{name}", line, count=1)
if new_line == line:
new_line = re.sub(rf"\b{re.escape(name)}\b", f"_{name}", line, count=1)
if new_line != line:
lines[idx] = new_line
path.write_text("".join(lines))
return True
return False
def main() -> int:
fixed = 0
for _ in range(5):
errors = run_tsc()
if not errors:
break
changed = False
seen: set[tuple] = set()
for err in errors:
parsed = parse_error(err)
if not parsed:
continue
rel, row, col, name, code = parsed
key = (rel, row, name)
if key in seen:
continue
seen.add(key)
path = ROOT / rel
if fix_file(path, row, name, code):
fixed += 1
changed = True
if not changed:
break
print(f"Fixed {fixed} TS6133/6198 issues")
remaining = run_tsc()
print(f"Remaining TS6133/6198: {len(remaining)}")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,22 @@
import type { TenantContext } from '../../middleware/tenant-auth'
import type { Context } from '../../types/context'
/** Minimal tenant context for unit tests. */
export function testTenantContext(overrides: Partial<TenantContext> = {}): TenantContext {
return {
userId: 'test-user-id',
email: 'test@example.com',
role: 'USER',
permissions: {},
isSystemAdmin: false,
...overrides,
}
}
/** Build a GraphQL Context stub for tests. */
export function testContext(overrides: Partial<Context> = {}): Context {
return {
db: {} as Context['db'],
...overrides,
}
}

View File

@@ -0,0 +1,126 @@
import { vi } from 'vitest'
/** Default mock query handler for integration tests (no live Postgres). */
export function createIntegrationMockQuery() {
const metricsRows = Array.from({ length: 20 }, (_, i) => ({
timestamp: new Date(Date.now() - (20 - i) * 60000),
value: (50 + (i === 15 ? 150 : 0)).toString(),
labels: {},
}))
return vi.fn().mockImplementation((sql: string) => {
const q = sql.toLowerCase()
if (q.includes('from metrics')) {
return Promise.resolve({ rows: metricsRows })
}
if (q.includes('from tenants')) {
return Promise.resolve({
rows: [
{
id: 'tenant-1',
quota_limits: {
compute: { vcpu: 64, memory: 256, instances: 100 },
storage: { total: 10000 },
network: { bandwidth: 10000, egress: 5000 },
},
metadata: { region: 'US' },
},
],
})
}
if (q.includes('from pillars')) {
return Promise.resolve({
rows: [
{
id: 'pillar-1',
code: 'OPERATIONAL_EXCELLENCE',
name: 'Operational Excellence',
description: 'Ops pillar',
created_at: new Date(),
updated_at: new Date(),
},
],
})
}
if (q.includes('from controls')) {
return Promise.resolve({
rows: [
{
id: 'control-1',
pillar_id: 'pillar-1',
code: 'C1',
name: 'Control 1',
description: 'Test control',
created_at: new Date(),
updated_at: new Date(),
},
],
})
}
if (q.includes('insert into resources')) {
return Promise.resolve({
rows: [
{
id: 'resource-new-1',
name: 'test-vm',
type: 'VM',
status: 'PENDING',
site_id: 'site-1',
tenant_id: null,
metadata: JSON.stringify({ cpu: 4, memory: '8Gi' }),
created_at: new Date(),
updated_at: new Date(),
},
],
})
}
if (q.includes('from resources') && q.includes('where id')) {
return Promise.resolve({
rows: [
{
id: 'resource-new-1',
name: 'test-vm',
type: 'VM',
status: 'PENDING',
site_id: 'site-1',
tenant_id: null,
metadata: JSON.stringify({ cpu: 4, memory: '8Gi' }),
created_at: new Date(),
updated_at: new Date(),
},
],
})
}
if (q.includes('from resources')) {
return Promise.resolve({ rows: [] })
}
if (q.includes('from sites')) {
return Promise.resolve({
rows: [
{
id: 'site-1',
name: 'Site 1',
region: 'us-east-1',
status: 'ACTIVE',
metadata: '{}',
created_at: new Date(),
updated_at: new Date(),
},
],
})
}
if (q.includes('from findings')) {
return Promise.resolve({ rows: [] })
}
if (q.includes('insert into') || q.includes('update ')) {
return Promise.resolve({ rows: [] })
}
return Promise.resolve({ rows: [] })
})
}
export function createMockDbPool(query = createIntegrationMockQuery()) {
return { query }
}

View File

@@ -0,0 +1,429 @@
import { Migration } from '../migrate.js'
export const up: Migration['up'] = async (db) => {
await db.query(`
CREATE TABLE IF NOT EXISTS clients (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(255) UNIQUE NOT NULL,
primary_domain VARCHAR(255),
status VARCHAR(50) NOT NULL DEFAULT 'PENDING'
CHECK (status IN ('ACTIVE', 'PENDING', 'SUSPENDED', 'DELETED')),
metadata JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
)
`)
await db.query(`
CREATE TABLE IF NOT EXISTS client_users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role VARCHAR(50) NOT NULL DEFAULT 'CLIENT_USER'
CHECK (role IN ('CLIENT_OWNER', 'CLIENT_ADMIN', 'CLIENT_BILLING_ADMIN', 'CLIENT_USER', 'CLIENT_VIEWER')),
permissions JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE (client_id, user_id)
)
`)
await db.query(`
ALTER TABLE tenants
ADD COLUMN IF NOT EXISTS client_id UUID REFERENCES clients(id) ON DELETE SET NULL
`)
await db.query(`
CREATE TABLE IF NOT EXISTS service_subscriptions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
tenant_id UUID REFERENCES tenants(id) ON DELETE SET NULL,
offer_code VARCHAR(255) NOT NULL,
offer_name VARCHAR(255) NOT NULL,
offer_type VARCHAR(50) NOT NULL
CHECK (offer_type IN ('native', 'partner')),
commercial_model VARCHAR(50) NOT NULL
CHECK (commercial_model IN ('IRU', 'SaaS', 'managed_service', 'reserved_capacity', 'custom')),
support_owner VARCHAR(50) NOT NULL
CHECK (support_owner IN ('sankofa', 'partner', 'shared')),
fulfillment_mode VARCHAR(50) NOT NULL
CHECK (fulfillment_mode IN ('self_service', 'request_only', 'operator_provisioned')),
billing_mode VARCHAR(50) NOT NULL
CHECK (billing_mode IN ('subscription', 'contract', 'quote')),
status VARCHAR(50) NOT NULL DEFAULT 'PENDING'
CHECK (status IN ('PENDING', 'ACTIVE', 'SUSPENDED', 'CANCELLED', 'PREVIEW', 'REQUEST_ONLY')),
activated_at TIMESTAMP WITH TIME ZONE,
metadata JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
)
`)
await db.query(`
CREATE TABLE IF NOT EXISTS entitlements (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
subscription_id UUID NOT NULL REFERENCES service_subscriptions(id) ON DELETE CASCADE,
tenant_id UUID REFERENCES tenants(id) ON DELETE SET NULL,
entitlement_key VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'PENDING'
CHECK (status IN ('PENDING', 'ACTIVE', 'SUSPENDED', 'REVOKED')),
scope JSONB DEFAULT '{}'::jsonb,
metadata JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
)
`)
await db.query(`
ALTER TABLE billing_accounts
ADD COLUMN IF NOT EXISTS client_id UUID REFERENCES clients(id) ON DELETE SET NULL
`)
await db.query(`
ALTER TABLE billing_accounts
ADD COLUMN IF NOT EXISTS subscription_id UUID REFERENCES service_subscriptions(id) ON DELETE SET NULL
`)
await db.query(`
ALTER TABLE usage_records
ADD COLUMN IF NOT EXISTS client_id UUID REFERENCES clients(id) ON DELETE SET NULL
`)
await db.query(`
ALTER TABLE usage_records
ADD COLUMN IF NOT EXISTS subscription_id UUID REFERENCES service_subscriptions(id) ON DELETE SET NULL
`)
await db.query(`
ALTER TABLE cost_allocations
ADD COLUMN IF NOT EXISTS client_id UUID REFERENCES clients(id) ON DELETE SET NULL
`)
await db.query(`
ALTER TABLE cost_allocations
ADD COLUMN IF NOT EXISTS subscription_id UUID REFERENCES service_subscriptions(id) ON DELETE SET NULL
`)
await db.query(`
ALTER TABLE invoices
ADD COLUMN IF NOT EXISTS client_id UUID REFERENCES clients(id) ON DELETE SET NULL
`)
await db.query(`
ALTER TABLE invoices
ADD COLUMN IF NOT EXISTS subscription_id UUID REFERENCES service_subscriptions(id) ON DELETE SET NULL
`)
await db.query(`
ALTER TABLE payments
ADD COLUMN IF NOT EXISTS client_id UUID REFERENCES clients(id) ON DELETE SET NULL
`)
await db.query(`
ALTER TABLE payments
ADD COLUMN IF NOT EXISTS subscription_id UUID REFERENCES service_subscriptions(id) ON DELETE SET NULL
`)
await db.query(`
ALTER TABLE billing_alerts
ADD COLUMN IF NOT EXISTS client_id UUID REFERENCES clients(id) ON DELETE SET NULL
`)
await db.query(`
ALTER TABLE billing_alerts
ADD COLUMN IF NOT EXISTS subscription_id UUID REFERENCES service_subscriptions(id) ON DELETE SET NULL
`)
await db.query(`
ALTER TABLE budgets
ADD COLUMN IF NOT EXISTS client_id UUID REFERENCES clients(id) ON DELETE SET NULL
`)
await db.query(`
ALTER TABLE budgets
ADD COLUMN IF NOT EXISTS subscription_id UUID REFERENCES service_subscriptions(id) ON DELETE SET NULL
`)
await db.query(`
INSERT INTO clients (name, primary_domain, status, metadata)
SELECT
t.name,
t.domain,
CASE
WHEN t.status = 'ACTIVE' THEN 'ACTIVE'
WHEN t.status = 'SUSPENDED' THEN 'SUSPENDED'
WHEN t.status = 'DELETED' THEN 'DELETED'
ELSE 'PENDING'
END,
jsonb_build_object(
'backfilledFromTenantId', t.id,
'source', '027_client_subscription_entitlements'
)
FROM tenants t
LEFT JOIN clients c ON c.name = t.name
WHERE c.id IS NULL
`)
await db.query(`
UPDATE tenants t
SET client_id = c.id
FROM clients c
WHERE t.client_id IS NULL
AND c.name = t.name
`)
await db.query(`
INSERT INTO client_users (client_id, user_id, role, permissions)
SELECT
t.client_id,
tu.user_id,
CASE tu.role
WHEN 'TENANT_OWNER' THEN 'CLIENT_OWNER'
WHEN 'TENANT_ADMIN' THEN 'CLIENT_ADMIN'
WHEN 'TENANT_BILLING_ADMIN' THEN 'CLIENT_BILLING_ADMIN'
WHEN 'TENANT_VIEWER' THEN 'CLIENT_VIEWER'
ELSE 'CLIENT_USER'
END,
COALESCE(tu.permissions, '{}'::jsonb)
FROM tenant_users tu
JOIN tenants t ON t.id = tu.tenant_id
LEFT JOIN client_users cu ON cu.client_id = t.client_id AND cu.user_id = tu.user_id
WHERE t.client_id IS NOT NULL
AND cu.id IS NULL
`)
await db.query(`
INSERT INTO service_subscriptions (
client_id,
tenant_id,
offer_code,
offer_name,
offer_type,
commercial_model,
support_owner,
fulfillment_mode,
billing_mode,
status,
activated_at,
metadata
)
SELECT
t.client_id,
t.id,
'tenant-workspace',
'Tenant Workspace',
'native',
'custom',
'sankofa',
'operator_provisioned',
'subscription',
CASE
WHEN t.status = 'ACTIVE' THEN 'ACTIVE'
WHEN t.status = 'SUSPENDED' THEN 'SUSPENDED'
ELSE 'PENDING'
END,
CASE WHEN t.status = 'ACTIVE' THEN NOW() ELSE NULL END,
jsonb_build_object(
'tier', t.tier,
'backfilledFromTenantId', t.id,
'source', '027_client_subscription_entitlements'
)
FROM tenants t
LEFT JOIN service_subscriptions s
ON s.tenant_id = t.id
AND s.offer_code = 'tenant-workspace'
WHERE t.client_id IS NOT NULL
AND s.id IS NULL
`)
await db.query(`
INSERT INTO entitlements (
subscription_id,
tenant_id,
entitlement_key,
status,
scope,
metadata
)
SELECT
s.id,
s.tenant_id,
'tenant.workspace',
CASE
WHEN s.status = 'ACTIVE' THEN 'ACTIVE'
WHEN s.status = 'SUSPENDED' THEN 'SUSPENDED'
ELSE 'PENDING'
END,
jsonb_build_object(
'offerCode', s.offer_code,
'offerType', s.offer_type,
'commercialModel', s.commercial_model
),
jsonb_build_object(
'source', '027_client_subscription_entitlements'
)
FROM service_subscriptions s
LEFT JOIN entitlements e
ON e.subscription_id = s.id
AND e.entitlement_key = 'tenant.workspace'
WHERE s.offer_code = 'tenant-workspace'
AND e.id IS NULL
`)
await db.query(`
UPDATE billing_accounts ba
SET
client_id = t.client_id,
subscription_id = s.id
FROM tenants t
LEFT JOIN service_subscriptions s
ON s.tenant_id = t.id
AND s.offer_code = 'tenant-workspace'
WHERE ba.tenant_id = t.id
AND (ba.client_id IS NULL OR ba.subscription_id IS NULL)
`)
await db.query(`
UPDATE usage_records ur
SET
client_id = t.client_id,
subscription_id = s.id
FROM tenants t
LEFT JOIN service_subscriptions s
ON s.tenant_id = t.id
AND s.offer_code = 'tenant-workspace'
WHERE ur.tenant_id = t.id
AND (ur.client_id IS NULL OR ur.subscription_id IS NULL)
`)
await db.query(`
UPDATE cost_allocations ca
SET
client_id = t.client_id,
subscription_id = s.id
FROM tenants t
LEFT JOIN service_subscriptions s
ON s.tenant_id = t.id
AND s.offer_code = 'tenant-workspace'
WHERE ca.tenant_id = t.id
AND (ca.client_id IS NULL OR ca.subscription_id IS NULL)
`)
await db.query(`
UPDATE invoices i
SET
client_id = t.client_id,
subscription_id = s.id
FROM tenants t
LEFT JOIN service_subscriptions s
ON s.tenant_id = t.id
AND s.offer_code = 'tenant-workspace'
WHERE i.tenant_id = t.id
AND (i.client_id IS NULL OR i.subscription_id IS NULL)
`)
await db.query(`
UPDATE payments p
SET
client_id = t.client_id,
subscription_id = s.id
FROM tenants t
LEFT JOIN service_subscriptions s
ON s.tenant_id = t.id
AND s.offer_code = 'tenant-workspace'
WHERE p.tenant_id = t.id
AND (p.client_id IS NULL OR p.subscription_id IS NULL)
`)
await db.query(`
UPDATE billing_alerts ba
SET
client_id = t.client_id,
subscription_id = s.id
FROM tenants t
LEFT JOIN service_subscriptions s
ON s.tenant_id = t.id
AND s.offer_code = 'tenant-workspace'
WHERE ba.tenant_id = t.id
AND (ba.client_id IS NULL OR ba.subscription_id IS NULL)
`)
await db.query(`
UPDATE budgets b
SET
client_id = t.client_id,
subscription_id = s.id
FROM tenants t
LEFT JOIN service_subscriptions s
ON s.tenant_id = t.id
AND s.offer_code = 'tenant-workspace'
WHERE b.tenant_id = t.id
AND (b.client_id IS NULL OR b.subscription_id IS NULL)
`)
await db.query(`CREATE INDEX IF NOT EXISTS idx_tenants_client_id ON tenants(client_id)`)
await db.query(`CREATE INDEX IF NOT EXISTS idx_clients_primary_domain ON clients(primary_domain) WHERE primary_domain IS NOT NULL`)
await db.query(`CREATE INDEX IF NOT EXISTS idx_client_users_client_id ON client_users(client_id)`)
await db.query(`CREATE INDEX IF NOT EXISTS idx_client_users_user_id ON client_users(user_id)`)
await db.query(`CREATE INDEX IF NOT EXISTS idx_service_subscriptions_client_id ON service_subscriptions(client_id)`)
await db.query(`CREATE INDEX IF NOT EXISTS idx_service_subscriptions_tenant_id ON service_subscriptions(tenant_id)`)
await db.query(`CREATE INDEX IF NOT EXISTS idx_service_subscriptions_status ON service_subscriptions(status)`)
await db.query(`CREATE INDEX IF NOT EXISTS idx_entitlements_subscription_id ON entitlements(subscription_id)`)
await db.query(`CREATE INDEX IF NOT EXISTS idx_entitlements_tenant_id ON entitlements(tenant_id)`)
await db.query(`CREATE INDEX IF NOT EXISTS idx_usage_records_client_id ON usage_records(client_id)`)
await db.query(`CREATE INDEX IF NOT EXISTS idx_invoices_client_id ON invoices(client_id)`)
await db.query(`CREATE INDEX IF NOT EXISTS idx_budgets_client_id ON budgets(client_id)`)
await db.query(`CREATE INDEX IF NOT EXISTS idx_billing_alerts_client_id ON billing_alerts(client_id)`)
await db.query(`
CREATE TRIGGER update_clients_updated_at BEFORE UPDATE ON clients
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()
`)
await db.query(`
CREATE TRIGGER update_client_users_updated_at BEFORE UPDATE ON client_users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()
`)
await db.query(`
CREATE TRIGGER update_service_subscriptions_updated_at BEFORE UPDATE ON service_subscriptions
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()
`)
await db.query(`
CREATE TRIGGER update_entitlements_updated_at BEFORE UPDATE ON entitlements
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()
`)
}
export const down: Migration['down'] = async (db) => {
await db.query(`DROP TRIGGER IF EXISTS update_entitlements_updated_at ON entitlements`)
await db.query(`DROP TRIGGER IF EXISTS update_service_subscriptions_updated_at ON service_subscriptions`)
await db.query(`DROP TRIGGER IF EXISTS update_client_users_updated_at ON client_users`)
await db.query(`DROP TRIGGER IF EXISTS update_clients_updated_at ON clients`)
await db.query(`DROP INDEX IF EXISTS idx_billing_alerts_client_id`)
await db.query(`DROP INDEX IF EXISTS idx_budgets_client_id`)
await db.query(`DROP INDEX IF EXISTS idx_invoices_client_id`)
await db.query(`DROP INDEX IF EXISTS idx_usage_records_client_id`)
await db.query(`DROP INDEX IF EXISTS idx_entitlements_tenant_id`)
await db.query(`DROP INDEX IF EXISTS idx_entitlements_subscription_id`)
await db.query(`DROP INDEX IF EXISTS idx_service_subscriptions_status`)
await db.query(`DROP INDEX IF EXISTS idx_service_subscriptions_tenant_id`)
await db.query(`DROP INDEX IF EXISTS idx_service_subscriptions_client_id`)
await db.query(`DROP INDEX IF EXISTS idx_client_users_user_id`)
await db.query(`DROP INDEX IF EXISTS idx_client_users_client_id`)
await db.query(`DROP INDEX IF EXISTS idx_clients_primary_domain`)
await db.query(`DROP INDEX IF EXISTS idx_tenants_client_id`)
await db.query(`ALTER TABLE budgets DROP COLUMN IF EXISTS subscription_id`)
await db.query(`ALTER TABLE budgets DROP COLUMN IF EXISTS client_id`)
await db.query(`ALTER TABLE billing_alerts DROP COLUMN IF EXISTS subscription_id`)
await db.query(`ALTER TABLE billing_alerts DROP COLUMN IF EXISTS client_id`)
await db.query(`ALTER TABLE payments DROP COLUMN IF EXISTS subscription_id`)
await db.query(`ALTER TABLE payments DROP COLUMN IF EXISTS client_id`)
await db.query(`ALTER TABLE invoices DROP COLUMN IF EXISTS subscription_id`)
await db.query(`ALTER TABLE invoices DROP COLUMN IF EXISTS client_id`)
await db.query(`ALTER TABLE cost_allocations DROP COLUMN IF EXISTS subscription_id`)
await db.query(`ALTER TABLE cost_allocations DROP COLUMN IF EXISTS client_id`)
await db.query(`ALTER TABLE usage_records DROP COLUMN IF EXISTS subscription_id`)
await db.query(`ALTER TABLE usage_records DROP COLUMN IF EXISTS client_id`)
await db.query(`ALTER TABLE billing_accounts DROP COLUMN IF EXISTS subscription_id`)
await db.query(`ALTER TABLE billing_accounts DROP COLUMN IF EXISTS client_id`)
await db.query(`ALTER TABLE tenants DROP COLUMN IF EXISTS client_id`)
await db.query(`DROP TABLE IF EXISTS entitlements`)
await db.query(`DROP TABLE IF EXISTS service_subscriptions`)
await db.query(`DROP TABLE IF EXISTS client_users`)
await db.query(`DROP TABLE IF EXISTS clients`)
}

View File

@@ -0,0 +1,58 @@
import { Pool } from 'pg'
/**
* Extend products.category CHECK for partner/security/interoperability offerings.
*/
export async function up(db: Pool): Promise<void> {
await db.query(`
ALTER TABLE products
DROP CONSTRAINT IF EXISTS products_category_check
`)
await db.query(`
ALTER TABLE products
ADD CONSTRAINT products_category_check
CHECK (category IN (
'COMPUTE',
'NETWORK_INFRA',
'BLOCKCHAIN_STACK',
'BLOCKCHAIN_TOOLS',
'FINANCIAL_MESSAGING',
'INTERNET_REGISTRY',
'AI_LLM_AGENT',
'LEDGER_SERVICES',
'IDENTITY_SERVICES',
'WALLET_SERVICES',
'ORCHESTRATION_SERVICES',
'PLATFORM_SERVICES',
'SECURITY_SERVICES',
'INTEROPERABILITY_SERVICES'
))
`)
}
export async function down(db: Pool): Promise<void> {
await db.query(`
ALTER TABLE products
DROP CONSTRAINT IF EXISTS products_category_check
`)
await db.query(`
ALTER TABLE products
ADD CONSTRAINT products_category_check
CHECK (category IN (
'COMPUTE',
'NETWORK_INFRA',
'BLOCKCHAIN_STACK',
'BLOCKCHAIN_TOOLS',
'FINANCIAL_MESSAGING',
'INTERNET_REGISTRY',
'AI_LLM_AGENT',
'LEDGER_SERVICES',
'IDENTITY_SERVICES',
'WALLET_SERVICES',
'ORCHESTRATION_SERVICES',
'PLATFORM_SERVICES'
))
`)
}

View File

@@ -0,0 +1,55 @@
import 'dotenv/config'
import bcrypt from 'bcryptjs'
import { getDb } from '../index.js'
import { operatingModelService } from '../../services/operating-model.js'
async function main() {
const db = getDb()
const portalEmail = process.env.PORTAL_BOOTSTRAP_EMAIL || 'portal@sankofa.nexus'
const portalPassword = process.env.PORTAL_BOOTSTRAP_PASSWORD || 'admin123'
await db.query(
`INSERT INTO users (email, name, password_hash, role)
VALUES ($1, $2, $3, $4)
ON CONFLICT (email) DO UPDATE SET password_hash = EXCLUDED.password_hash, role = EXCLUDED.role`,
[portalEmail, 'Portal Operator', await bcrypt.hash(portalPassword, 10), 'ADMIN']
)
let tenantId: string
const existing = await db.query(`SELECT id FROM tenants WHERE domain = $1 LIMIT 1`, [
'portal.sankofa.nexus',
])
if (existing.rows.length) {
tenantId = existing.rows[0].id
} else {
const billingAccountId = `portal-${Date.now()}`
const created = await db.query(
`INSERT INTO tenants (name, domain, billing_account_id, status, tier)
VALUES ($1, $2, $3, $4, $5)
RETURNING id`,
['Sankofa Sovereign Console', 'portal.sankofa.nexus', billingAccountId, 'ACTIVE', 'SOVEREIGN']
)
tenantId = created.rows[0].id
}
const users = await db.query(`SELECT id FROM users WHERE email = ANY($1)`, [
[portalEmail, 'admin@sankofa.nexus'],
])
for (const row of users.rows) {
await db.query(
`INSERT INTO tenant_users (tenant_id, user_id, role)
VALUES ($1, $2, $3)
ON CONFLICT (tenant_id, user_id) DO UPDATE SET role = EXCLUDED.role`,
[tenantId, row.id, 'TENANT_OWNER']
)
}
await operatingModelService.bootstrapClientForTenant(tenantId)
console.log(JSON.stringify({ tenantId, portalEmail }))
await db.end()
}
main().catch((err) => {
console.error(err)
process.exit(1)
})

View File

@@ -483,6 +483,114 @@ const services: ServiceDefinition[] = [
features: ['Basic observability', '7-day retention']
}
}
},
{
name: 'DBIS B2B Integration Hub',
slug: 'phoenix-b2b-integration-hub',
category: 'PLATFORM_SERVICES',
description: 'Enterprise B2B orchestration for DBIS and OMNL — EDI, file transfer, PEPPOL and EBICS fan-out to CanonicalMessage and Fineract.',
shortDescription: 'B2B orchestration hub for DBIS and OMNL',
tags: ['b2b', 'edi', 'integration', 'platform'],
featured: true,
documentationUrl: 'https://docs.sankofa.nexus/marketplace/sovereign-stack/b2b-integration-hub-service',
metadata: { architecture: 'docs/marketplace/sovereign-stack/b2b-integration-hub-service.md', entitlement: 'B2B_HUB_ENTITLED' },
pricingType: 'SUBSCRIPTION',
pricingConfig: {
currency: 'USD',
billingPeriod: 'MONTHLY',
billingModel: 'enterprise_contract',
quoteRequired: true,
},
},
{
name: 'Phoenix PEPPOL Hosted Connector',
slug: 'phoenix-peppol-hosted-connector',
category: 'PLATFORM_SERVICES',
description: 'Hosted Peppol Access Point connector — UBL invoice ingest, outbound submit, Peppol ID routing.',
shortDescription: 'Hosted Peppol Access Point connector',
tags: ['peppol', 'invoicing', 'b2b', 'platform'],
featured: false,
documentationUrl: 'https://docs.sankofa.nexus/marketplace/sovereign-stack/peppol-hosted-connector-service',
metadata: { architecture: 'docs/marketplace/sovereign-stack/peppol-hosted-connector-service.md', entitlement: 'PEPPOL_HOSTED_ENTITLED' },
pricingType: 'SUBSCRIPTION',
pricingConfig: {
currency: 'USD',
billingPeriod: 'MONTHLY',
billingModel: 'enterprise_contract',
quoteRequired: true,
},
},
{
name: 'Phoenix EBICS Banking Gateway',
slug: 'phoenix-ebics-banking-gateway',
category: 'FINANCIAL_MESSAGING',
description: 'EBICS 3.0 corporate host-to-host gateway — H004 payment upload, H005 statement download, camt/pain routing.',
shortDescription: 'EBICS 3.0 banking gateway',
tags: ['ebics', 'banking', 'financial-messaging'],
featured: false,
documentationUrl: 'https://docs.sankofa.nexus/marketplace/sovereign-stack/ebics-banking-gateway-service',
metadata: { architecture: 'docs/marketplace/sovereign-stack/ebics-banking-gateway-service.md', entitlement: 'EBICS_GATEWAY_ENTITLED' },
pricingType: 'SUBSCRIPTION',
pricingConfig: {
currency: 'USD',
billingPeriod: 'MONTHLY',
billingModel: 'enterprise_contract',
quoteRequired: true,
},
},
{
name: 'Chain 138 Participant Onboarding',
slug: 'phoenix-chain138-participant-onboard',
category: 'BLOCKCHAIN_STACK',
description: 'Screened institutions subscribe through Sankofa Phoenix marketplace and deploy Besu nodes on Chain 138.',
shortDescription: 'Chain 138 participant and Besu node onboarding',
tags: ['chain138', 'besu', 'onboarding', 'blockchain'],
featured: true,
documentationUrl: 'https://docs.sankofa.nexus/marketplace/sovereign-stack/chain138-participant-onboard-service',
metadata: { architecture: 'docs/marketplace/sovereign-stack/chain138-participant-onboard-service.md', entitlement: 'CHAIN138_ONBOARD_ENTITLED' },
pricingType: 'SUBSCRIPTION',
pricingConfig: {
currency: 'USD',
billingPeriod: 'MONTHLY',
billingModel: 'enterprise_contract',
quoteRequired: true,
},
},
{
name: 'Phoenix X-Road Interoperability',
slug: 'phoenix-x-road-interoperability',
category: 'INTEROPERABILITY_SERVICES',
description: 'NIIS-aligned X-Road sovereign data exchange for cross-organisation interoperability.',
shortDescription: 'X-Road interoperability service',
tags: ['x-road', 'interop', 'sovereign'],
featured: false,
documentationUrl: 'https://docs.sankofa.nexus/marketplace/sovereign-stack/x-road-interoperability-service',
metadata: { architecture: 'docs/marketplace/sovereign-stack/x-road-interoperability-service.md', entitlement: 'XROAD_INTEROP_ENTITLED', status: 'preview' },
pricingType: 'SUBSCRIPTION',
pricingConfig: {
currency: 'USD',
billingPeriod: 'MONTHLY',
billingModel: 'enterprise_contract',
quoteRequired: true,
},
},
{
name: 'AEGIS-VAULT CTI Platform',
slug: 'phoenix-aegis-vault-cti',
category: 'SECURITY_SERVICES',
description: 'Defensive external threat intelligence and dark-web exposure monitoring for financial-sector tenants.',
shortDescription: 'AEGIS-VAULT CTI platform',
tags: ['cti', 'security', 'threat-intelligence'],
featured: false,
documentationUrl: 'https://docs.sankofa.nexus/marketplace/sovereign-stack/aegis-vault-cti-service',
metadata: { architecture: 'docs/marketplace/sovereign-stack/aegis-vault-cti-service.md', entitlement: 'AEGIS_VAULT_ENTITLED' },
pricingType: 'SUBSCRIPTION',
pricingConfig: {
currency: 'USD',
billingPeriod: 'MONTHLY',
billingModel: 'enterprise_contract',
quoteRequired: true,
}
}
]

51
api/src/lib/json-utils.ts Normal file
View File

@@ -0,0 +1,51 @@
/** Narrow unknown JSON / fetch payloads for strict TypeScript. */
export interface PrometheusQueryResponse {
status: string
data?: {
result?: Array<{
values?: Array<[number | string, string]>
}>
}
}
export interface ProxmoxListResponse<T = unknown> {
data: T
}
export function asRecord(value: unknown): Record<string, unknown> {
return (typeof value === 'object' && value !== null ? value : {}) as Record<string, unknown>
}
export function asString(value: unknown, fallback = ''): string {
if (typeof value === 'string') return value
if (typeof value === 'number' || typeof value === 'boolean') return String(value)
return fallback
}
export function asNumber(value: unknown, fallback = 0): number {
if (typeof value === 'number' && !Number.isNaN(value)) return value
if (typeof value === 'string') {
const parsed = Number(value)
return Number.isNaN(parsed) ? fallback : parsed
}
return fallback
}
export function asArray<T = unknown>(value: unknown): T[] {
return Array.isArray(value) ? (value as T[]) : []
}
export function asDate(value: unknown): Date {
if (value instanceof Date) return value
if (typeof value === 'string' || typeof value === 'number') return new Date(value)
return new Date()
}
export function asPrometheusResponse(value: unknown): PrometheusQueryResponse {
return asRecord(value) as unknown as PrometheusQueryResponse
}
export function asProxmoxResponse<T>(value: unknown): ProxmoxListResponse<T> {
return asRecord(value) as unknown as ProxmoxListResponse<T>
}

View File

@@ -142,7 +142,7 @@ export async function tenantAuthMiddleware(
// Set tenant context in database session for RLS policies
const db = getDb()
if (context.userId) {
await db.query(`SET LOCAL app.current_user_id = $1`, [context.userId])
await db.query(`SELECT set_config('app.current_user_id', $1, true)`, [context.userId])
}
}

View File

@@ -0,0 +1,53 @@
/**
* Authenticated marketplace entitlement routes (subscribe, workspace entitlements).
*/
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
import { requireTenant } from '../middleware/tenant-auth.js'
import { marketplaceSubscriptionService } from '../services/marketplace-subscription.js'
import { operatingModelService } from '../services/operating-model.js'
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)
}
export async function registerMarketplaceEntitlementRoutes(fastify: FastifyInstance) {
fastify.post('/api/v1/marketplace/subscribe', async (request, reply) => {
const tenantContext = tenantFromRequest(request, reply)
const body = (request.body || {}) as { productSlug?: string; sku?: string; syncKeycloak?: boolean }
if (!body.productSlug?.trim()) {
return reply.code(400).send({ error: 'productSlug is required' })
}
try {
const result = await marketplaceSubscriptionService.subscribeToProduct(
{ request, user: (request as any).user, tenantContext, db: undefined as any },
{
productSlug: body.productSlug.trim(),
sku: body.sku?.trim(),
syncKeycloak: body.syncKeycloak,
}
)
return reply.send(result)
} catch (error: any) {
const message = error?.message || 'Subscribe failed'
const code = message.includes('Tenant') ? 403 : message.includes('not found') ? 404 : 400
return reply.code(code).send({ error: message })
}
})
fastify.get('/api/v1/marketplace/me/entitlements', async (request, reply) => {
const tenantContext = tenantFromRequest(request, reply)
const entitlements = await marketplaceSubscriptionService.getTenantEntitlements(
tenantContext.tenantId!
)
const subscriptions = await marketplaceSubscriptionService.getTenantSubscriptions(
tenantContext.tenantId!
)
const client = await operatingModelService.getClientByTenantId(tenantContext.tenantId!)
return reply.send({ client, subscriptions, entitlements })
})
}

View File

@@ -0,0 +1,38 @@
/**
* Public marketplace catalog — published products only (no auth).
*/
import type { FastifyInstance } from 'fastify'
import { catalogService } from '../services/catalog.js'
import type { Context } from '../types/context.js'
function publicContext(): Context {
return {
request: {} as Context['request'],
user: undefined,
tenantContext: undefined,
db: undefined as unknown as Context['db'],
}
}
export async function registerMarketplacePublicRoutes(fastify: FastifyInstance) {
fastify.get('/api/v1/marketplace/products', async (request, reply) => {
const q = request.query as { category?: string; search?: string; featured?: string; limit?: string }
const filter = {
...(q.category && { category: q.category }),
...(q.search && { search: q.search }),
...(q.featured === 'true' && { featured: true }),
...(q.limit && { limit: Math.min(Number(q.limit) || 50, 100) }),
}
const products = await catalogService.getProducts(publicContext(), filter)
return reply.send({ products, count: products.length })
})
fastify.get('/api/v1/marketplace/products/:slug', async (request, reply) => {
const { slug } = request.params as { slug: string }
const product = await catalogService.getProductBySlug(publicContext(), slug)
if (!product || product.status !== 'PUBLISHED') {
return reply.code(404).send({ error: 'Product not found', slug })
}
return reply.send({ product })
})
}

View File

@@ -31,6 +31,7 @@ import * as apiMarketplaceService from '../services/api-marketplace'
import * as analyticsService from '../services/analytics'
import * as aiOptimizationService from '../services/ai-optimization'
import { infrastructureResolvers } from '../resolvers/infrastructure'
import { operatingModelService } from '../services/operating-model.js'
import {
createTenantInputSchema,
updateTenantInputSchema,
@@ -199,6 +200,39 @@ export const resolvers = {
return tenantService.getTenant(context.tenantContext.tenantId)
},
myClient: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Authentication required', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const tenantId = context.tenantContext?.tenantId
if (!tenantId) return null
return operatingModelService.getClientByTenantId(tenantId)
},
mySubscriptions: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Authentication required', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const tenantId = context.tenantContext?.tenantId
if (!tenantId) return []
return operatingModelService.listSubscriptions({ tenantId })
},
myEntitlements: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Authentication required', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const tenantId = context.tenantContext?.tenantId
if (!tenantId) return []
return operatingModelService.listEntitlements({ tenantId })
},
tenantUsage: async (
_: unknown,
args: { tenantId: string; timeRange: TimeRange },

View File

@@ -6,7 +6,7 @@ export const typeDefs = gql`
type Query {
# Health check
health: HealthStatus
health: ApiHealthStatus
# Resources
resources(filter: ResourceFilter): [Resource!]!
@@ -73,6 +73,9 @@ export const typeDefs = gql`
tenant(id: ID!): Tenant
tenantByDomain(domain: String!): Tenant
myTenant: Tenant
myClient: OperatingClient
mySubscriptions: [OperatingSubscription!]!
myEntitlements: [OperatingEntitlement!]!
tenantUsage(tenantId: ID!, timeRange: TimeRangeInput!): UsageReport!
# Billing (Superior to Azure Cost Management)
@@ -310,7 +313,47 @@ export const typeDefs = gql`
resourceDeleted(id: ID!): ID!
}
type HealthStatus {
type OperatingClient {
id: ID!
name: String!
primaryDomain: String
status: String!
metadata: JSON
createdAt: DateTime!
updatedAt: DateTime!
}
type OperatingSubscription {
id: ID!
clientId: ID!
tenantId: ID
offerCode: String!
offerName: String!
offerType: String!
commercialModel: String!
supportOwner: String!
fulfillmentMode: String!
billingMode: String!
status: String!
activatedAt: DateTime
metadata: JSON
createdAt: DateTime!
updatedAt: DateTime!
}
type OperatingEntitlement {
id: ID!
subscriptionId: ID!
tenantId: ID
entitlementKey: String!
status: String!
scope: JSON
metadata: JSON
createdAt: DateTime!
updatedAt: DateTime!
}
type ApiHealthStatus {
status: String!
timestamp: DateTime!
version: String!
@@ -660,7 +703,7 @@ export const typeDefs = gql`
end: DateTime!
}
enum HealthStatus {
enum ComponentHealthLevel {
HEALTHY
DEGRADED
UNHEALTHY

View File

@@ -45,12 +45,19 @@ export async function login(email: string, password: string): Promise<AuthPayloa
throw AppErrors.unauthenticated('Invalid email or password')
}
const tenantResult = await db.query(
`SELECT tenant_id FROM tenant_users WHERE user_id = $1 ORDER BY created_at ASC NULLS LAST LIMIT 1`,
[user.id]
)
const tenantId = tenantResult.rows[0]?.tenant_id as string | undefined
const token = jwt.sign(
{
id: user.id,
email: user.email,
name: user.name,
role: user.role,
...(tenantId ? { tenantId } : {}),
},
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }

View File

@@ -0,0 +1,243 @@
/**
* Marketplace subscription — contract/PO-first self-service subscribe + entitlement grants.
*/
import fs from 'fs'
import path from 'path'
import { getDb } from '../db/index.js'
import { logger } from '../lib/logger.js'
import { catalogService } from './catalog.js'
import { operatingModelService } from './operating-model.js'
import { identityService } from './identity.js'
import type { Context } from '../types/context.js'
import type { Entitlement, ServiceSubscription } from '../types/operating-model.js'
interface RegistryProduct {
productSlug: string
entitlementKeys: string[]
fulfillmentMode?: string
tierEntitlements?: Record<string, string>
displayName?: string
}
type FulfillmentMode = 'self_service' | 'operator_provisioned' | 'request_only'
function loadRegistryProducts(): RegistryProduct[] {
const candidates = [
process.env.MARKETPLACE_ENTITLEMENT_REGISTRY_PATH,
path.join(process.cwd(), 'config/marketplace-entitlement-registry.v1.json'),
].filter(Boolean) as string[]
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
const parsed = JSON.parse(fs.readFileSync(candidate, 'utf8')) as { products?: RegistryProduct[] }
return parsed.products || []
}
}
return []
}
function subscriptionStatusForMode(mode: FulfillmentMode): 'ACTIVE' | 'PENDING' | 'REQUEST_ONLY' {
if (mode === 'self_service') return 'ACTIVE'
if (mode === 'request_only') return 'REQUEST_ONLY'
return 'PENDING'
}
class MarketplaceSubscriptionService {
getRegistryEntry(slug: string): RegistryProduct | undefined {
return loadRegistryProducts().find((entry) => entry.productSlug === slug)
}
resolveEntitlementKeys(
slug: string,
productMetadata?: Record<string, unknown>,
sku?: string
): string[] {
const entry = this.getRegistryEntry(slug)
if (sku && entry?.tierEntitlements?.[sku]) {
const tierKey = entry.tierEntitlements[sku]
const base = entry.entitlementKeys.includes('AEGIS_VAULT_ENTITLED')
? ['AEGIS_VAULT_ENTITLED', tierKey]
: [tierKey]
return [...new Set(base)]
}
if (entry?.entitlementKeys?.length) {
return modePrimaryKeys(entry.entitlementKeys)
}
const flag = productMetadata?.entitlementFeatureFlag
if (typeof flag === 'string' && flag.trim()) {
return [flag.trim()]
}
return [`${slug.replace(/-/g, '_').toUpperCase()}_ENTITLED`]
}
async getTenantEntitlements(tenantId: string): Promise<Entitlement[]> {
return operatingModelService.listEntitlements({ tenantId })
}
async getTenantSubscriptions(tenantId: string): Promise<ServiceSubscription[]> {
return operatingModelService.listSubscriptions({ tenantId })
}
async subscribeToProduct(
context: Context,
input: { productSlug: string; sku?: string; syncKeycloak?: boolean }
): Promise<{
subscription: ServiceSubscription
entitlements: Entitlement[]
productSlug: string
fulfillmentMode: FulfillmentMode
keycloakSynced: boolean
}> {
const tenantId = context.tenantContext?.tenantId
const userEmail = context.tenantContext?.email
if (!tenantId) {
throw new Error('Tenant membership required to subscribe')
}
const product = await catalogService.getProductBySlug(context, input.productSlug)
if (!product || product.status !== 'PUBLISHED') {
throw new Error(`Product not found or not published: ${input.productSlug}`)
}
const registryEntry = this.getRegistryEntry(input.productSlug)
const fulfillmentMode = (registryEntry?.fulfillmentMode ||
(product.metadata?.fulfillmentMode as string) ||
'operator_provisioned') as FulfillmentMode
const status = subscriptionStatusForMode(fulfillmentMode)
const entitlementKeys = this.resolveEntitlementKeys(
input.productSlug,
product.metadata,
input.sku
)
const bootstrap = await operatingModelService.bootstrapClientForTenant(tenantId)
const db = getDb()
const existing = await db.query(
`SELECT * FROM service_subscriptions
WHERE tenant_id = $1 AND offer_code = $2
ORDER BY created_at ASC LIMIT 1`,
[tenantId, input.productSlug]
)
let subscriptionRow = existing.rows[0]
if (!subscriptionRow) {
const created = await db.query(
`INSERT INTO service_subscriptions (
client_id, tenant_id, offer_code, offer_name, offer_type,
commercial_model, support_owner, fulfillment_mode, billing_mode,
status, activated_at, metadata
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
RETURNING *`,
[
bootstrap.client.id,
tenantId,
input.productSlug,
product.name,
'marketplace',
'contract_po_first',
'sankofa',
fulfillmentMode,
'manual',
status,
status === 'ACTIVE' ? new Date() : null,
JSON.stringify({
source: 'marketplaceSubscribe',
productSlug: input.productSlug,
sku: input.sku || null,
registryDisplayName: registryEntry?.displayName || product.name,
}),
]
)
subscriptionRow = created.rows[0]
} else if (existing.rows[0].status === 'PENDING' && status === 'ACTIVE') {
const updated = await db.query(
`UPDATE service_subscriptions
SET status = $1, activated_at = COALESCE(activated_at, NOW()), updated_at = NOW()
WHERE id = $2
RETURNING *`,
[status, existing.rows[0].id]
)
subscriptionRow = updated.rows[0]
}
const entitlements: Entitlement[] = []
for (const key of entitlementKeys) {
const found = await db.query(
`SELECT * FROM entitlements
WHERE subscription_id = $1 AND entitlement_key = $2 LIMIT 1`,
[subscriptionRow.id, key]
)
let row = found.rows[0]
if (!row) {
const createdEntitlement = await db.query(
`INSERT INTO entitlements (subscription_id, tenant_id, entitlement_key, status, scope, metadata)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *`,
[
subscriptionRow.id,
tenantId,
key,
status === 'ACTIVE' ? 'ACTIVE' : status === 'REQUEST_ONLY' ? 'REQUEST_ONLY' : 'PENDING',
JSON.stringify({ productSlug: input.productSlug, sku: input.sku || null }),
JSON.stringify({ source: 'marketplaceSubscribe' }),
]
)
row = createdEntitlement.rows[0]
}
entitlements.push({
id: row.id,
subscriptionId: row.subscription_id,
tenantId: row.tenant_id,
entitlementKey: row.entitlement_key,
status: row.status,
scope: row.scope || {},
metadata: row.metadata || {},
createdAt: row.created_at,
updatedAt: row.updated_at,
})
}
let keycloakSynced = false
const shouldSync =
input.syncKeycloak !== false &&
status === 'ACTIVE' &&
process.env.MARKETPLACE_KEYCLOAK_SYNC !== '0'
if (shouldSync && userEmail) {
keycloakSynced = await identityService.mergeUserEntitlementAttributes(
userEmail,
entitlementKeys,
true
)
}
logger.info('Marketplace subscription created', {
tenantId,
productSlug: input.productSlug,
subscriptionId: subscriptionRow.id,
entitlementKeys,
fulfillmentMode,
keycloakSynced,
})
const subscription = (await operatingModelService.listSubscriptions({ tenantId })).find(
(s) => s.id === subscriptionRow.id
)!
return {
subscription,
entitlements,
productSlug: input.productSlug,
fulfillmentMode,
keycloakSynced,
}
}
}
function modePrimaryKeys(keys: string[]): string[] {
if (keys.length <= 1) return keys
const base = keys.find((k) => k.endsWith('_ENTITLED') && !k.includes('_ESSENTIALS') && !k.includes('_PRO') && !k.includes('_ENTERPRISE'))
return base ? [base] : [keys[0]]
}
export const marketplaceSubscriptionService = new MarketplaceSubscriptionService()

View File

@@ -0,0 +1,283 @@
import { getDb } from '../db/index.js'
import { logger } from '../lib/logger.js'
import { Client, Entitlement, ServiceSubscription } from '../types/operating-model.js'
class OperatingModelService {
private formatClient(row: any): Client {
return {
id: row.id,
name: row.name,
primaryDomain: row.primary_domain,
status: row.status,
metadata: row.metadata || {},
createdAt: row.created_at,
updatedAt: row.updated_at,
}
}
private formatSubscription(row: any): ServiceSubscription {
return {
id: row.id,
clientId: row.client_id,
tenantId: row.tenant_id,
offerCode: row.offer_code,
offerName: row.offer_name,
offerType: row.offer_type,
commercialModel: row.commercial_model,
supportOwner: row.support_owner,
fulfillmentMode: row.fulfillment_mode,
billingMode: row.billing_mode,
status: row.status,
activatedAt: row.activated_at,
metadata: row.metadata || {},
createdAt: row.created_at,
updatedAt: row.updated_at,
}
}
private formatEntitlement(row: any): Entitlement {
return {
id: row.id,
subscriptionId: row.subscription_id,
tenantId: row.tenant_id,
entitlementKey: row.entitlement_key,
status: row.status,
scope: row.scope || {},
metadata: row.metadata || {},
createdAt: row.created_at,
updatedAt: row.updated_at,
}
}
async bootstrapClientForTenant(tenantId: string): Promise<{
client: Client
subscription: ServiceSubscription
entitlement: Entitlement
}> {
const db = getDb()
const tenantResult = await db.query(`SELECT * FROM tenants WHERE id = $1`, [tenantId])
if (tenantResult.rows.length === 0) {
throw new Error(`Tenant ${tenantId} not found`)
}
const tenant = tenantResult.rows[0]
let clientRow: any
if (tenant.client_id) {
const clientResult = await db.query(`SELECT * FROM clients WHERE id = $1`, [tenant.client_id])
clientRow = clientResult.rows[0]
} else {
const createdClient = await db.query(
`INSERT INTO clients (name, primary_domain, status, metadata)
VALUES ($1, $2, $3, $4)
RETURNING *`,
[
tenant.name,
tenant.domain,
tenant.status === 'ACTIVE' ? 'ACTIVE' : tenant.status === 'SUSPENDED' ? 'SUSPENDED' : 'PENDING',
JSON.stringify({
source: 'bootstrapClientForTenant',
tenantId: tenant.id,
}),
]
)
clientRow = createdClient.rows[0]
await db.query(`UPDATE tenants SET client_id = $1, updated_at = NOW() WHERE id = $2`, [
clientRow.id,
tenant.id,
])
await db.query(
`INSERT INTO client_users (client_id, user_id, role, permissions)
SELECT
$1,
user_id,
CASE role
WHEN 'TENANT_OWNER' THEN 'CLIENT_OWNER'
WHEN 'TENANT_ADMIN' THEN 'CLIENT_ADMIN'
WHEN 'TENANT_BILLING_ADMIN' THEN 'CLIENT_BILLING_ADMIN'
WHEN 'TENANT_VIEWER' THEN 'CLIENT_VIEWER'
ELSE 'CLIENT_USER'
END,
COALESCE(permissions, '{}'::jsonb)
FROM tenant_users
WHERE tenant_id = $2
ON CONFLICT (client_id, user_id) DO NOTHING`,
[clientRow.id, tenant.id]
)
}
let subscriptionRow: any
const existingSubscription = await db.query(
`SELECT * FROM service_subscriptions WHERE tenant_id = $1 AND offer_code = 'tenant-workspace' ORDER BY created_at ASC LIMIT 1`,
[tenant.id]
)
if (existingSubscription.rows.length > 0) {
subscriptionRow = existingSubscription.rows[0]
} else {
const createdSubscription = await db.query(
`INSERT INTO service_subscriptions (
client_id,
tenant_id,
offer_code,
offer_name,
offer_type,
commercial_model,
support_owner,
fulfillment_mode,
billing_mode,
status,
activated_at,
metadata
)
VALUES ($1, $2, 'tenant-workspace', 'Tenant Workspace', 'native', 'custom', 'sankofa', 'operator_provisioned', 'subscription', $3, $4, $5)
RETURNING *`,
[
clientRow.id,
tenant.id,
tenant.status === 'ACTIVE' ? 'ACTIVE' : tenant.status === 'SUSPENDED' ? 'SUSPENDED' : 'PENDING',
tenant.status === 'ACTIVE' ? new Date() : null,
JSON.stringify({
tier: tenant.tier,
source: 'bootstrapClientForTenant',
}),
]
)
subscriptionRow = createdSubscription.rows[0]
}
let entitlementRow: any
const existingEntitlement = await db.query(
`SELECT * FROM entitlements WHERE subscription_id = $1 AND entitlement_key = 'tenant.workspace' LIMIT 1`,
[subscriptionRow.id]
)
if (existingEntitlement.rows.length > 0) {
entitlementRow = existingEntitlement.rows[0]
} else {
const createdEntitlement = await db.query(
`INSERT INTO entitlements (subscription_id, tenant_id, entitlement_key, status, scope, metadata)
VALUES ($1, $2, 'tenant.workspace', $3, $4, $5)
RETURNING *`,
[
subscriptionRow.id,
tenant.id,
subscriptionRow.status === 'ACTIVE' ? 'ACTIVE' : subscriptionRow.status === 'SUSPENDED' ? 'SUSPENDED' : 'PENDING',
JSON.stringify({
offerCode: subscriptionRow.offer_code,
commercialModel: subscriptionRow.commercial_model,
}),
JSON.stringify({ source: 'bootstrapClientForTenant' }),
]
)
entitlementRow = createdEntitlement.rows[0]
}
await db.query(
`UPDATE billing_accounts
SET client_id = $1, subscription_id = $2, updated_at = NOW()
WHERE tenant_id = $3`,
[clientRow.id, subscriptionRow.id, tenant.id]
)
logger.info('Bootstrapped Phoenix operating model for tenant', {
tenantId: tenant.id,
clientId: clientRow.id,
subscriptionId: subscriptionRow.id,
entitlementId: entitlementRow.id,
})
return {
client: this.formatClient(clientRow),
subscription: this.formatSubscription(subscriptionRow),
entitlement: this.formatEntitlement(entitlementRow),
}
}
async getClient(clientId: string): Promise<Client> {
const db = getDb()
const result = await db.query(`SELECT * FROM clients WHERE id = $1`, [clientId])
if (result.rows.length === 0) {
throw new Error(`Client ${clientId} not found`)
}
return this.formatClient(result.rows[0])
}
async listClients(): Promise<Client[]> {
const db = getDb()
const result = await db.query(`SELECT * FROM clients ORDER BY created_at DESC`)
return result.rows.map((row) => this.formatClient(row))
}
async getClientByTenantId(tenantId: string): Promise<Client | null> {
const db = getDb()
const result = await db.query(
`SELECT c.*
FROM tenants t
JOIN clients c ON c.id = t.client_id
WHERE t.id = $1`,
[tenantId]
)
return result.rows.length > 0 ? this.formatClient(result.rows[0]) : null
}
async listSubscriptions(filter?: { clientId?: string; tenantId?: string }): Promise<ServiceSubscription[]> {
const db = getDb()
const conditions: string[] = []
const params: unknown[] = []
if (filter?.clientId) {
params.push(filter.clientId)
conditions.push(`client_id = $${params.length}`)
}
if (filter?.tenantId) {
params.push(filter.tenantId)
conditions.push(`tenant_id = $${params.length}`)
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
const result = await db.query(
`SELECT * FROM service_subscriptions ${whereClause} ORDER BY created_at DESC`,
params
)
return result.rows.map((row) => this.formatSubscription(row))
}
async getActiveSubscriptionForTenant(tenantId: string): Promise<ServiceSubscription | null> {
const db = getDb()
const result = await db.query(
`SELECT * FROM service_subscriptions
WHERE tenant_id = $1
AND status IN ('ACTIVE', 'PENDING', 'REQUEST_ONLY')
ORDER BY created_at ASC
LIMIT 1`,
[tenantId]
)
return result.rows.length > 0 ? this.formatSubscription(result.rows[0]) : null
}
async listEntitlements(filter?: { tenantId?: string; subscriptionId?: string }): Promise<Entitlement[]> {
const db = getDb()
const conditions: string[] = []
const params: unknown[] = []
if (filter?.tenantId) {
params.push(filter.tenantId)
conditions.push(`tenant_id = $${params.length}`)
}
if (filter?.subscriptionId) {
params.push(filter.subscriptionId)
conditions.push(`subscription_id = $${params.length}`)
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
const result = await db.query(
`SELECT * FROM entitlements ${whereClause} ORDER BY created_at DESC`,
params
)
return result.rows.map((row) => this.formatEntitlement(row))
}
}
export const operatingModelService = new OperatingModelService()

13
api/src/types/graphql.ts Normal file
View File

@@ -0,0 +1,13 @@
import { Context } from './context'
/** Standard GraphQL resolver signature used across this API. */
export type ResolverFn<TArgs = Record<string, unknown>, TResult = unknown> = (
parent: unknown,
args: TArgs,
context: Context,
info?: unknown
) => TResult | Promise<TResult>
export type ResolverMap = {
[key: string]: ResolverFn | ResolverMap | undefined
}

View File

@@ -0,0 +1,39 @@
export interface Client {
id: string
name: string
primaryDomain: string | null
status: 'ACTIVE' | 'PENDING' | 'SUSPENDED' | 'DELETED'
metadata: Record<string, unknown>
createdAt: Date
updatedAt: Date
}
export interface ServiceSubscription {
id: string
clientId: string
tenantId: string | null
offerCode: string
offerName: string
offerType: 'native' | 'partner'
commercialModel: 'IRU' | 'SaaS' | 'managed_service' | 'reserved_capacity' | 'custom'
supportOwner: 'sankofa' | 'partner' | 'shared'
fulfillmentMode: 'self_service' | 'request_only' | 'operator_provisioned'
billingMode: 'subscription' | 'contract' | 'quote'
status: 'PENDING' | 'ACTIVE' | 'SUSPENDED' | 'CANCELLED' | 'PREVIEW' | 'REQUEST_ONLY'
activatedAt: Date | null
metadata: Record<string, unknown>
createdAt: Date
updatedAt: Date
}
export interface Entitlement {
id: string
subscriptionId: string
tenantId: string | null
entitlementKey: string
status: 'PENDING' | 'ACTIVE' | 'SUSPENDED' | 'REVOKED'
scope: Record<string, unknown>
metadata: Record<string, unknown>
createdAt: Date
updatedAt: Date
}

8
api/vitest.setup.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* Vitest global setup — test env vars required by secret-validation and db bootstrap.
*/
process.env.NODE_ENV = process.env.NODE_ENV || 'test'
process.env.DB_PASSWORD = process.env.DB_PASSWORD || 'VitestDbPass1!'
process.env.JWT_SECRET =
process.env.JWT_SECRET ||
'VitestJwtSecret_Abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+'