26 Commits

Author SHA1 Message Date
8412eb6b3e Accept Tessera (Keycloak-compatible) OIDC tokens as API bearer
Adds an additive bearer-verification path: verify RS256 access tokens against
Tessera's JWKS (iss/aud/exp), map sub/preferred_username/email + roles
(realm_access.roles, resource_access.<audience>.roles) to the app's identity.
Existing auth (API keys / app JWTs / sessions) is unchanged. Issuer + audience
are env-configurable. Validated end-to-end against the local sim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 15:11:30 +01:00
9e1909a7e8 refactor(guild-discovery): drop serviceEndpoint (no longer needed)
Pairs with Dialectic.Backend@5cf4302 which removes the backend-driven
broadcast that was the only consumer of serviceEndpoint. With agent-
driven recruitment broadcasts via fabric-send-message, the
service-to-service URL distinction goes away (agents use the regular
guild endpoint).

Removed:
  - GuildNode.serviceEndpoint column (TypeORM will drop on next sync)
  - GET /api/auth/me/guilds + /api/nodes response fields
  - NodeAdminService.setServiceEndpoint()
  - cli 'node set-service-endpoint' subcommand

Kept:
  - GuildNode.purpose (used by fabric-guild-list for intent-based
    channel discovery — still wanted)
2026-05-23 23:48:19 +01:00
3058bccfb6 feat(guild-discovery): add serviceEndpoint for backend-to-backend reachability
A guild's existing 'endpoint' field is its client-facing URL (browser,
remote openclaw plugin) — but in-deployment services on the same docker
network can't always reach it. Concretely, dialectic-backend on
compose_default network sees 'http://server.t3:7002' (from agent-supplied
URL) resolve to dind-network IP which it can't route to. Broadcasts
silently fail with 'connection refused'.

Adds a second URL per guild: serviceEndpoint. Used by other backends
in the same deployment (compose service name + internal port). Plumbed
through:

  - GuildNode.serviceEndpoint (varchar 255 nullable; TypeORM auto-migrates)
  - GET /api/auth/me/guilds returns serviceEndpoint per row
  - GET /api/nodes (admin) returns serviceEndpoint
  - new cli: 'node set-service-endpoint --node-id X --endpoint URL'
    (admin-only via cli — same pattern as set-purpose)

Pairs with Fabric.OpenclawPlugin's fabric-guild-list returning the new
field + workflows teaching agents to use serviceEndpoint for
announce_guild_base_url (NOT the client-facing endpoint).

E2e verified: sim recruiter agent discovered sim-guild-1's
serviceEndpoint=http://fabric-backend-guild:7002, plumbed it into
dialectic_propose_topic, all 4 lifecycle broadcasts (signup_open,
signup_closed, debating, completed) landed in the announce channel.
2026-05-23 22:21:03 +01:00
604f6556fe feat(guild-discovery): add purpose column to guild_nodes + cli set-purpose
Adds a free-form 'purpose' text field on GuildNode so admins can describe
what each guild is for (debate broadcasts / agent triage / dev sandbox /
etc.). Surfaced through:

  - GET /api/auth/me/guilds   (existing user-facing list)
  - GET /api/nodes             (existing admin-facing list)
  - new cli: node set-purpose --node-id X --purpose 'text'

Admin-only writes via CLI (HangmanLab pattern) — never HTTP. Empty string
clears. TypeORM synchronize auto-adds the column on first startup.

Motivates: agents discover the right guild by intent (purpose substring
match) before picking a channel, so dialectic/announce workflows don't
need hardcoded guild ids.
2026-05-23 19:21:49 +01:00
ca7c6b408e Merge pull request 'feat(auth): center-scoped single admin + GET /admin-email + cli' (#1) from feat/admin-user into main 2026-05-22 21:59:14 +00:00
1c1f280553 fix(cli): set-admin transaction uses isAdmin=true filter (TypeORM rejects empty-where)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 22:25:06 +01:00
f1ca33f2a2 feat(auth): center-scoped single admin + GET /admin-email + cli
Triage channels in Guilds need to deliver every message to a single
"observer" admin user across the whole Center deployment (regardless of
on-duty / mention). This adds the data + auth surface for that.

## Schema

`users.isAdmin TINYINT DEFAULT 0` (synchronize:true auto-applies; cli
`user set-admin` enforces at-most-one in a transaction).

## CLI (subject `user`)

- `set-admin --email <e>` — clears every other admin row first, then
  marks the target. Returns `{admin: {email, userId}}`
- `clear-admin` — unsets all (returns `{cleared: N}`)
- `show-admin` — prints `{admin: {email, userId}}` or `{admin: null}`

## HTTP

`GET /auth/admin-email` (NOT @Public — requires a guild-node api key
via the existing CenterApiKeyGuard). Returns:
  - `{email: "...", userId: "..."}` if an admin exists
  - `null` (literal JSON) if no admin

Guild backends cache the result (1 day TTL per spec; with cli refresh
override on guild side).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 22:09:58 +01:00
9f7216565b fix(center): graceful 4xx when OIDC issuer/token endpoint unreachable
Wrap discovery + token-exchange fetch in try/catch so a DNS/network
failure returns BadRequest/Unauthorized instead of a bare 500.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 09:47:28 +01:00
2a394969d2 feat(center): OIDC login (auto-provision by email) + CLI config-oidc
- OidcConfig entity (single row) + DB registration.
- OidcService: discovery, Authorization Code + PKCE, state/ticket as
  short-lived signed JWTs (no server session), userinfo/id_token claim
  resolution. Auto-provisions a passwordless user by email.
- Public endpoints /auth/oidc/{status,start,callback,exchange}; callback
  bounces a one-time ticket to the SPA in the URL fragment.
- AuthService.provisionOidcUser + issueSessionForUserId (login shape).
- CLI: 'config oidc' upserts issuer/clientId/secret/callback/redirect/
  scopes/enabled (secret masked on print).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 09:44:49 +01:00
6afb935302 fix(security): close Critical auth gaps (C1/C2/C3)
C1: replace fragile path.endsWith() guard whitelist with a metadata
    @Public() decorator + Reflector (no more path-shape bypass surface).
C2: CenterApiKeyGuard attaches the authenticated GuildNode; introspect
    & resolve-names now reject when body.guildNodeId != that node
    (stops one node probing/enumerating another guild's identities).
C3: heartbeat/status are self-only (a node can't revoke/hijack another);
    GET /nodes no longer returns apiKeyHash (credential-hash leak).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:47:01 +01:00
aa9d59a952 docs: rewrite README to match current architecture
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 12:53:22 +01:00
44aa34d1ff refactor: migrate to ES modules
package.json type=module, tsconfig module/moduleResolution=NodeNext,
target es2022, explicit .js on all relative imports. Center: jsonwebtoken
& bcryptjs switched to default imports (ESM/CJS interop). Verified:
builds, boots, full auth + plugin round-trip work under ESM.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:47:35 +01:00
dea946653b feat(center): API-key agent auth
UserApiKey (apiKeyHash->userId); CLI 'user apikey --email'; POST
/auth/agent/login {apiKey} -> normal user session (api-key-guard exempt).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:52:42 +01:00
3da51a60bc feat(center): POST /auth/resolve-names
Resolve display names/emails to userIds within a guild node's active
members (api-key auth). Used by guild for <@user.name:NAME> translation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:47:01 +01:00
bc0d1ba8bf feat(center): user display name + GET/PATCH /auth/me
- User.name column, defaults to email on register
- GET /auth/me, PATCH /auth/me to view/change own name (api-key exempt)
- login + guild members responses now include name

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:09:40 +01:00
nav
2792f78ada fix(center): exempt guild join/members auth endpoints from api-key guard 2026-05-15 00:32:33 +00:00
nav
1eb30348a2 feat(center): guild join and guild members APIs; stop auto-joining all guilds 2026-05-14 16:57:57 +00:00
nav
ebc3571823 fix(center): enable CORS for auth preflight and desktop origins 2026-05-14 16:26:55 +00:00
nav
0b32dc8e3c feat(cli): move user and guild registration from API to local CLI 2026-05-14 14:43:59 +00:00
nav
7afd220b4a feat(auth): split api-key boundary for frontend auth flow 2026-05-14 14:17:07 +00:00
nav
81dfc227e3 refactor(center): prefix environment variables with FABRIC_BACKEND_CENTER 2026-05-13 12:58:28 +00:00
nav
0a4cb62065 refactor(center): local-only guild register endpoint without shared secret 2026-05-13 08:41:45 +00:00
nav
1c07f43032 refactor(center): introspect relies on api key auth instead of shared secret 2026-05-13 08:36:06 +00:00
nav
cfa5ccdfaf feat(center): enforce API key on all APIs except node register 2026-05-13 08:17:42 +00:00
root
a924bf656d feat(center): issue per-guild tokens and add introspection API 2026-05-13 07:59:27 +00:00
nav
03a3342d2a feat: bootstrap from Fabric monorepo 2026-05-13 07:06:02 +00:00