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
15 changes: 12 additions & 3 deletions billing-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
"name": "@fuzefront/billing-client",
"version": "1.0.0",
"description": "Typed HTTP client for the FuzeFront billing-service REST API",
"main": "dist/index.js",
"main": "dist/index.cjs",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsc -p tsconfig.json",
"build": "tsup",
"type-check": "tsc --noEmit",
"test": "jest",
"clean": "rimraf dist",
Expand Down Expand Up @@ -38,6 +38,15 @@
"openapi-typescript": "^7.4.0",
"rimraf": "^5.0.1",
"ts-jest": "29.1.1",
"typescript": "5.1.6"
"typescript": "5.1.6",
"tsup": "^8.0.2"
},
"module": "dist/index.js",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}
17 changes: 13 additions & 4 deletions billing-client/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"lib": [
"ES2020"
],
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
Expand All @@ -11,8 +13,15 @@
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true
"resolveJsonModule": true,
"ignoreDeprecations": "6.0"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"tests"
]
}
18 changes: 18 additions & 0 deletions billing-client/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineConfig } from 'tsup';

// Dual build: ESM (.js) + CJS (.cjs) + .d.ts. The ESM output keeps `export *`
// re-exports STATIC so the host bundler (vite/rollup, which bundles billing-ui
// with billing-client external) can resolve named exports. Plain tsc emitted CJS
// __exportStar(require()) → "X is not exported" at host build time (same fix as
// @fuzefront/chat-client).
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
sourcemap: true,
clean: true,
treeshake: true,
outExtension({ format }) {
return { js: format === 'cjs' ? '.cjs' : '.js' };
},
});
36 changes: 36 additions & 0 deletions deploy/contabo/sealed/billing-secrets.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# =============================================================================
# SealedSecret: billing-secrets (namespace: fuzefront)
# -----------------------------------------------------------------------------
# Per-service secret for billing-service ONLY (least privilege). Holds just the
# three billing-specific keys. Shared platform creds (DB_PASSWORD, PERMIT_API_KEY)
# stay in fuzefront-secrets — billing-service reads those from there. A compromised
# billing pod therefore can't read Authentik/SMTP/OAuth/LLM secrets.
#
# Because this secret is isolated to billing (which is enabled: false until these
# are populated), committing/syncing it empty is SAFE — it can't clobber any other
# service's keys (unlike the shared fuzefront-secrets).
#
# Populate (credential-free; --merge-into preserves the others):
# deploy/scripts/seal-secret.sh STRIPE_SECRET_KEY
# deploy/scripts/seal-secret.sh STRIPE_WEBHOOK_SECRET
# deploy/scripts/seal-secret.sh BILLING_INTERNAL_TOKEN --in ~/.fuzefront-secrets/billing-internal-token.txt
# deploy/scripts/seal-secret.sh BILLING_DB_PASSWORD --in ~/.fuzefront-secrets/billing-db-password.txt
# (the script defaults to scope fuzefront/billing-secrets). Then commit + push ->
# Argo (FuzeInfra-operated) decrypts it in-cluster.
#
# Keys: STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, BILLING_INTERNAL_TOKEN, BILLING_DB_PASSWORD
# (BILLING_DB_PASSWORD = the billing_svc role's password, created by the
# billing-db-bootstrap Job; generated to ~/.fuzefront-secrets/billing-db-password.txt)
# =============================================================================
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: billing-secrets
namespace: fuzefront
spec:
encryptedData: {} # populate via deploy/scripts/seal-secret.sh
template:
metadata:
name: billing-secrets
namespace: fuzefront
type: Opaque
76 changes: 76 additions & 0 deletions deploy/helm/fuzefront/templates/billing-db-bootstrap-job.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{{- if and .Values.billingService.enabled .Values.billingService.dbBootstrap.enabled }}
apiVersion: batch/v1
kind: Job
metadata:
name: fuzefront-billing-db-bootstrap
labels:
{{- include "fuzefront.labels" . | nindent 4 }}
app.kubernetes.io/component: billing-db-bootstrap
annotations:
# Idempotent, privileged provisioning of the least-privilege billing_svc role
# and the `billing` schema it owns. Runs BEFORE billing-service starts (which
# self-migrates its schema on boot as billing_svc). Runs as the FuzeInfra
# Postgres superuser — the only place that needs CREATEROLE.
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-4"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
backoffLimit: 3
ttlSecondsAfterFinished: 600
template:
metadata:
labels:
app.kubernetes.io/part-of: fuzefront
app.kubernetes.io/component: billing-db-bootstrap
spec:
restartPolicy: Never
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: billing-db-bootstrap
image: postgres:15
command: ["/bin/sh", "-ec"]
args:
- |
export PGPASSWORD="$DB_SUPERUSER_PASSWORD"
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_SUPERUSER" -d "$DB_NAME" \
-v ON_ERROR_STOP=1 -v billing_pw="$BILLING_DB_PASSWORD" <<'SQL'
DO $do$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'billing_svc') THEN
CREATE ROLE billing_svc LOGIN PASSWORD :'billing_pw';
ELSE
ALTER ROLE billing_svc WITH LOGIN PASSWORD :'billing_pw';
END IF;
END
$do$;
CREATE SCHEMA IF NOT EXISTS billing AUTHORIZATION billing_svc;
GRANT USAGE ON SCHEMA public TO billing_svc;
SQL
echo "billing_svc role + billing schema ensured"
env:
- name: DB_HOST
value: {{ .Values.fuzeinfra.postgres.host | quote }}
- name: DB_PORT
value: {{ .Values.fuzeinfra.postgres.port | quote }}
- name: DB_NAME
value: {{ .Values.database.name | quote }}
- name: DB_SUPERUSER
value: {{ .Values.database.bootstrap.superuser.username | quote }}
- name: DB_SUPERUSER_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.database.bootstrap.superuser.secretName | default (include "fuzefront.secretName" .) }}
key: {{ .Values.database.bootstrap.superuser.secretKey | quote }}
# billing_svc's own password — lives in the per-service billing-secrets.
- name: BILLING_DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.billingService.secretName | default "billing-secrets" }}
key: BILLING_DB_PASSWORD
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { cpu: 200m, memory: 128Mi }
{{- end }}
19 changes: 8 additions & 11 deletions deploy/helm/fuzefront/templates/billing-service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,31 +42,28 @@ spec:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "fuzefront.secretName" . }}
key: DB_PASSWORD
name: {{ .Values.billingService.secretName | default "billing-secrets" }}
key: BILLING_DB_PASSWORD
- name: DATABASE_URL
value: "postgresql://{{ .Values.billingService.dbUser }}:$(DB_PASSWORD)@{{ .Values.fuzeinfra.postgres.host }}:{{ .Values.fuzeinfra.postgres.port }}/{{ .Values.database.name }}"
{{- if .Values.secret.stripeSecretKey }}
# Always rendered when billing is enabled — the per-service billing-secrets
# SealedSecret provides these at runtime (the pod stays pending until the
# keys are sealed). No Helm-values gate (values are empty under SealedSecrets).
- name: STRIPE_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ include "fuzefront.secretName" . }}
name: {{ .Values.billingService.secretName | default "billing-secrets" }}
key: STRIPE_SECRET_KEY
{{- end }}
{{- if .Values.secret.stripeWebhookSecret }}
- name: STRIPE_WEBHOOK_SECRET
valueFrom:
secretKeyRef:
name: {{ include "fuzefront.secretName" . }}
name: {{ .Values.billingService.secretName | default "billing-secrets" }}
key: STRIPE_WEBHOOK_SECRET
{{- end }}
{{- if .Values.secret.billingInternalToken }}
- name: BILLING_INTERNAL_TOKEN
valueFrom:
secretKeyRef:
name: {{ include "fuzefront.secretName" . }}
name: {{ .Values.billingService.secretName | default "billing-secrets" }}
key: BILLING_INTERNAL_TOKEN
{{- end }}
- name: PERMIT_API_KEY
valueFrom:
secretKeyRef:
Expand Down
13 changes: 13 additions & 0 deletions deploy/helm/fuzefront/templates/ingress.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ spec:
port:
number: {{ .Values.applicationsService.port }}
{{- end }}
{{- if .Values.billingService.enabled }}
# Billing webhook carve-out: the Stripe webhook is the ONLY publicly
# reachable billing path (Stripe-signature verified via STRIPE_WEBHOOK_SECRET).
# Exact match so the rest of /api/v1/billing/* stays internal (no ingress) —
# internal callers reach it in-cluster with BILLING_INTERNAL_TOKEN.
- path: /api/v1/billing/webhooks/stripe
pathType: Exact
backend:
service:
name: fuzefront-billing-service
port:
number: {{ .Values.billingService.port }}
{{- end }}
{{- if or .Values.securityService.enabled .Values.applicationsService.enabled }}
# Thin backend: health + any remaining /api/* (still dual-serving the
# domain routes during the soak; this is the catch-all under the
Expand Down
33 changes: 18 additions & 15 deletions deploy/helm/fuzefront/values-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,28 @@ provisioningService:
tag: "" # CI's release sed writes the real image SHA here

# Billing-service (Stripe). Independently-lifecycled — synced by its own Argo
# Application (deploy/argocd/applications/billing.yaml). Keep enabled: false in
# prod until the SealedSecret `fuzefront-secrets` holds STRIPE_SECRET_KEY,
# STRIPE_WEBHOOK_SECRET, and BILLING_INTERNAL_TOKEN, AND the billing_svc DB role
# exists (follow-up). NEVER hand-deploy — flip via GitOps.
# Application (deploy/argocd/applications/billing.yaml). Enabled — but it only
# actually serves once the per-service `billing-secrets` SealedSecret holds
# STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, BILLING_INTERNAL_TOKEN, BILLING_DB_PASSWORD
# (seal them with deploy/scripts/seal-secret.sh). The billing_svc role + `billing`
# schema are provisioned by the pre-install billing-db-bootstrap Job. Until the
# secret keys exist the pod stays pending on them (it will NOT crash other services,
# since billing has its own isolated secret). NEVER hand-deploy — flip via GitOps.
billingService:
enabled: false
enabled: true
image:
repository: ghcr.io/izzywdev/fuzefront-billing-service
tag: "" # CI's release sed writes the real image SHA here
# Steer onto node-2 (keep LLM/chat/billing load off the DB-heavy node-1).
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: kubernetes.io/hostname
operator: In
values: ["fuzefront-node-2"]

permit:
enabled: true
Expand Down Expand Up @@ -183,13 +196,3 @@ chatService:
operator: In
values: ["fuzefront-node-2"]

billingService:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: kubernetes.io/hostname
operator: In
values: ["fuzefront-node-2"]
12 changes: 12 additions & 0 deletions deploy/helm/fuzefront/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,18 @@ billingService:
tag: local
port: 3006
replicas: 1
# Dedicated least-privilege DB role (created by the billing-db-bootstrap Job),
# NOT the shared fuzefront_user. Its password is BILLING_DB_PASSWORD in the
# per-service billing-secrets SealedSecret.
dbUser: billing_svc
# Per-service secret (billing-only keys: STRIPE_*, BILLING_INTERNAL_TOKEN,
# BILLING_DB_PASSWORD). Shared creds (PERMIT_API_KEY) stay in fuzefront-secrets.
secretName: billing-secrets
meterFlushIntervalSec: 60
permitPdpUrl: "http://fuzefront-permit-pdp:7000"
# Pre-install Job that creates the billing_svc role + `billing` schema + grants.
dbBootstrap:
enabled: true
kafka:
brokers: "kafka.fuzeinfra.svc.cluster.local:9092"
clientId: "billing-service"
Expand Down
Loading
Loading