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
5 changes: 5 additions & 0 deletions .changeset/eso-bearer-refresher.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@smooai/config': minor
---

SMOODEV-1523: Add an ESO bearer-token refresher (`@smooai/config/eso-refresher` + `smooai-config-eso-refresher` bin). It re-mints the OAuth2 `client_credentials` access token on a short interval (reusing `TokenProvider`) and writes it into the ExternalSecrets bootstrap Kubernetes Secret, so ESO's webhook provider always reads a fresh, non-expired bearer. This is what lets workload secrets sync via ESO instead of being Pulumi-baked at SST deploy time — decoupling `@smooai/config` secret-value changes from the ~1h platform deploy (epic SMOODEV-1522).
40 changes: 40 additions & 0 deletions Dockerfile.eso-refresher
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# ESO bearer-token refresher sidecar (SMOODEV-1523, epic SMOODEV-1522).
#
# Builds the @smooai/config package and runs the standalone refresher process
# (`smooai-config-eso-refresher` bin). Deployed into the cluster to keep the
# ESO bootstrap Secret's bearer token fresh — see docs/ESO-Bearer-Refresher.md.
#
# Build context = repo root (~/dev/smooai/config). Built + pushed by the
# image pipeline; the k8s Deployment + RBAC live in the smooai monorepo
# (SMOODEV-1525).

# ---- build stage ----------------------------------------------------------
FROM node:22-slim AS build
WORKDIR /app

# Corepack gives us the repo-pinned pnpm.
RUN corepack enable

# Install with the lockfile first (cache-friendly), then build.
COPY package.json pnpm-lock.yaml .npmrc ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build:lib

# Prune to production deps for the runtime stage.
RUN pnpm prune --prod

# ---- runtime stage --------------------------------------------------------
FROM node:22-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production

# Run as a non-root, unprivileged user.
USER node

COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/dist ./dist
COPY --from=build --chown=node:node /app/package.json ./package.json

# The refresher is a long-lived loop; SIGTERM is handled for graceful drain.
ENTRYPOINT ["node", "dist/eso-refresher/run.mjs"]
46 changes: 46 additions & 0 deletions docs/ESO-Bearer-Refresher.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# ESO Bearer-Token Refresher

`@smooai/config/eso-refresher` (bin `smooai-config-eso-refresher`) — SMOODEV-1523, epic SMOODEV-1522.

## Problem

The [ExternalSecrets Operator](https://external-secrets.io) (ESO) `webhook` provider authenticates to the `@smooai/config` HTTP API (`api.smoo.ai`) with a **static** bearer token it reads from a Kubernetes Secret (`external-secrets/smooai-config-bootstrap`, key `bearer-token`).

But `@smooai/config` issues short-lived OAuth2 `client_credentials` JWTs (~1h TTL). A static token therefore expires within the hour and every ESO sync silently begins to 401. That is precisely why workload secrets (litellm, voice, api-prime) were instead **Pulumi-baked** into Kubernetes Secrets at SST deploy time (SMOODEV-1347) — which couples _every_ secret-value change to a ~1h platform deploy.

## What the refresher does

A small sidecar/Deployment that, on a short interval (default 15m, well under the JWT TTL):

1. Re-mints a fresh access token using the same `TokenProvider` the runtime SDK uses (`invalidate()` then `getAccessToken()` so the token always has a near-full TTL ahead).
2. Patches `bearer-token` in the bootstrap Secret via a JSON merge-patch.

ESO then always reads a fresh bearer. A `th config set <key> <value> --environment=production` becomes live on ESO's next `refreshInterval` plus a `kubectl rollout restart` of the consuming workload — **no platform deploy**.

The initial mint+write is awaited at startup and **fails loud** (non-zero exit → visible crash-loop) on misconfiguration. Later loop failures are logged and retried on the next tick — the existing Secret token is still valid for the remainder of its TTL.

## Env contract

| Var | Required | Default | Purpose |
| ------------------------------------- | -------- | ------------------------- | -------------------------------------------------------------- |
| `SMOOAI_CONFIG_CLIENT_ID` | yes | — | M2M OAuth client id (config-read scoped) |
| `SMOOAI_CONFIG_CLIENT_SECRET` | yes | — | M2M OAuth client secret (legacy alias `SMOOAI_CONFIG_API_KEY`) |
| `SMOOAI_CONFIG_AUTH_URL` | no | `https://auth.smoo.ai` | OAuth issuer base URL |
| `SMOOAI_ESO_SECRET_NAMESPACE` | no | `external-secrets` | Bootstrap Secret namespace |
| `SMOOAI_ESO_SECRET_NAME` | no | `smooai-config-bootstrap` | Bootstrap Secret name |
| `SMOOAI_ESO_SECRET_KEY` | no | `bearer-token` | Data key to write |
| `SMOOAI_ESO_REFRESH_INTERVAL_SECONDS` | no | `900` | Re-mint + write interval |

`orgId` / `environment` are **not** needed — those are query params ESO supplies when it calls the config API; the token itself is org-agnostic.

## Deployment

- **Image**: `Dockerfile.eso-refresher` (this repo) builds the `smooai-config-eso-refresher` process.
- **k8s wiring** (Deployment + RBAC + the refresher's own M2M Secret) lives in the smooai monorepo under SMOODEV-1525. RBAC must allow `patch` on the single bootstrap Secret only.
- **Root-of-trust**: the refresher's `SMOOAI_CONFIG_CLIENT_ID/SECRET` are provided as a one-time Kubernetes Secret (or IRSA-fronted). This is the only secret that does not flow through ESO — everything else syncs from it.

## Related

- Epic: SMOODEV-1522 (restore ESO secret sync).
- SMOODEV-1524 — schema-driven ESO manifest generator.
- SMOODEV-1525 — smooai: repoint ClusterSecretStore to `api.smoo.ai`, restore per-workload `ExternalSecret`s, drop the Pulumi-bake.
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"url": "https://github.com/SmooAI/config.git"
},
"bin": {
"smooai-config": "./dist/cli.mjs"
"smooai-config": "./dist/cli.mjs",
"smooai-config-eso-refresher": "./dist/eso-refresher/run.mjs"
},
"files": [
"dist/**"
Expand Down Expand Up @@ -170,6 +171,11 @@
"import": "./dist/container/index.mjs",
"require": "./dist/container/index.js"
},
"./eso-refresher": {
"types": "./dist/eso-refresher/index.d.ts",
"import": "./dist/eso-refresher/index.mjs",
"require": "./dist/eso-refresher/index.js"
},
"./platform/client": {
"types": "./dist/platform/client.d.ts",
"browser": {
Expand Down Expand Up @@ -249,6 +255,7 @@
},
"dependencies": {
"@isaacs/ttlcache": "^1.4.1",
"@kubernetes/client-node": "^1.4.0",
"@smooai/fetch": "^3.3.5",
"@smooai/logger": "^4.1.4",
"@smooai/utils": "^1.3.3",
Expand Down
Loading
Loading