Skip to content

MinistryofMany/Deforum

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

73 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Deforum

The forum where a badge gets you in and a pseudonym is all anyone sees (deforum.space). A Reddit-shaped forum where every sub-forum is gated by Minister badges, provable facts about you (a verified email domain, a GitHub account with some history behind it), and each one sets its own rules for who reads, posts, and votes. You take part under a stable pseudonym, one per sub-forum, with no karma trailing you and no global identity ever exposed.

The foundation wave shipped the scaffold, the @ministryofmany/* primitive wiring, the data model + migrations, and the identity / membership helpers. The SPACES + JOIN wave (this layer) adds the live Sign in with Minister OIDC routes + flow-state, sub-forum create/configure with preset-toggle capability matrices, the badge-gated join flow (gate -> membership -> pseudonym -> device leaf -> snapshot), and per-role tree narrowing. The full forum UI, posting / comment flows, governance, ranking, and the directory land in later waves.

Design spec: ../ecosystem-planner/design/deforum-spec.md.

Stack

SvelteKit 2 + Svelte 5 + Drizzle ORM + Postgres + @sveltejs/adapter-node, pnpm, TypeScript strict. ZK proving is client-side only (@semaphore-protocol/proof is SSR-excluded in vite.config.ts).

Consuming the @ministryofmany/* primitives

npm publish of the @ministryofmany/* scope is deferred. Until then Deforum is its own pnpm workspace whose pnpm-workspace.yaml includes the sibling minister-client checkout via a relative glob:

packages:
  - '.'
  - '../minister-client' # @ministryofmany/client (the OIDC RP SDK)
  - '../minister-client/packages/*' # @ministryofmany/{policy,minister-verify,nullifier,identity,membership,blind-token,poll,rln}

This resolves each primitive as a workspace package, which is what makes their internal workspace:* inter-deps (e.g. @ministryofmany/poll -> @ministryofmany/membership) resolve correctly - a per-package file: link cannot do that. The packages ship a committed/built dist/, so after editing one in minister-client you rebuild it there (pnpm -r build) and the change is picked up here on the next install.

Commands

pnpm install
pnpm exec svelte-kit sync
pnpm run check        # svelte-check / typecheck
pnpm run build        # vite build + adapter-node
pnpm run db:generate  # drizzle-kit generate -> migrations/
pnpm run db:migrate   # apply migrations (advisory-locked)
pnpm run smoke        # exercise each @ministryofmany/* primitive with a real call
pnpm test             # smoke + DB-backed integration (integration skips if no DATABASE_URL)

Layout

src/lib/server/
  db/        Drizzle schema (deforum-spec section 7) + lazy pooled client
  identity/  the two-layer nullifier scheme + per-sub-forum device identity
  membership/ DeforumGroupProvider (per-role tree narrowing) + Drizzle SnapshotStore
              + createMembership wiring + join() (the gated join pipeline)
  auth/      Sign in with Minister (OIDC RP) + flow-state + id_token/badge verify
              + account resolution + app sessions (HMAC cookie)
  subforum/  sub-forum create/configure + the preset-toggle capability matrix
  policy/    badge-policy gating (@ministryofmany/policy)
src/routes/
  api/auth/oidc/{start,callback}/  the OIDC dance (single-use flow-state)
  api/join/                        finish a gated join (POST device commitment)
  j/[subforumId]/                  minimal join page (client derives device identity)
src/lib/smoke/  the @ministryofmany/* smoke test
tests/          DB-backed integration: membership, join flow, OIDC flow-state
scripts/migrate.ts  advisory-locked Drizzle migrator

Sign in with Minister + the join flow

  1. GET /api/auth/oidc/start?subforum=<slug>&role=<name> builds PKCE + state + nonce, sends the role's badge policy as minister_policy, and persists the single-use flow-state to deforum_oidc_sessions.
  2. GET /api/auth/oidc/callback consumes that flow-state atomically by state (one DELETE ... RETURNING gated on not-expired - the replay guard), exchanges the code, re-verifies the id_token + disclosed badges via @ministryofmany/minister-verify, resolves the platform account (hashing the raw sub to the user nullifier - never stored raw), issues an app session, and for a gated join stashes the verified sub + badges in a short-lived single-use deforum_pending_joins row (the only place the raw sub lives, transiently).
  3. The join page derives the per-device Semaphore identity client-side and POST /api/join { pseudonym, deviceId, identityCommitment }. The server re-runs the badge gate (@ministryofmany/policy evaluate on the verified disclosure), creates the membership keyed on the user-sub-forum nullifier, records which roles the badges satisfied, enrolls the device leaf, and refreshes the member snapshot.

Per-role tree narrowing. DeforumGroupProvider.listEligible admits a member to a role-named sub-tree iff a deforum_membership_roles row ties them to that role (set at join from the verified badges); the anon:<action> sub-tree admits all active, non-banned members. Banned/revoked are always excluded.

Presets are additive toggles, not exclusive modes (fork 11): applying qa + members-only composes both capability sets into one matrix the operator can then edit.

Identity & nullifier model (deforum-spec section 2)

  • User nullifier = the verified Minister pairwise sub, stored hashed (HMAC over USER_NULLIFIER_HASH_SECRET, namespaced by issuer). The raw sub is never persisted.
  • User-sub-forum nullifier = poseidon2(toField(sub), toField(subforumId)) via @ministryofmany/nullifier - the membership + ban anchor. Rows are keyed only on this value, never the raw sub.
  • Per-sub-forum Semaphore identity = @ministryofmany/identity.deriveIdentity(seed, subforumId) - one backed-up device seed yields a distinct, unlinkable commitment per sub-forum.

The app session cookie is HMAC-signed with SESSION_SECRET (<sessionId>.<HMAC(sessionId)>); a tampered cookie fails the constant-time check and is treated as signed-out.

Cross-repo coupling

The badge-VC issuer DID is derived from the OIDC issuer host (did:web:<host>) by @ministryofmany/client, with no override. MINISTER_ISSUER's host must equal Minister's issuer domain, or every disclosed badge silently lands in rejected (fails closed: login still works, but no badge counts). The verifier surfaces a rejected-badge count so this misconfiguration is observable.

About

Deforum (deforum.space) - badge-gated anonymous-but-verified forum. SvelteKit; sub-forums gated by Minister badges with a configurable per-role action policy.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors