cgalo5758 235ee3c438 Resolve a sign-in in provisioning.ResolveSignIn
CallbackHandler (gocognit 30) held the whole resolution of who a
verified sign-in is: the subject lookup, the first sign-in, and the
returning sign-in with its login stamp, person resync, and organization
and workspace binding. That code moves to internal/provisioning/signin.go
as ResolveSignIn, firstSignIn, returningSignIn, resyncPerson and
sessionWorkspace, and returns a SignedIn (user, person, organization
and workspace ids). The handler calls it once and answers a failure
with "Sign-in failed", 500. Measured scores (golangci-lint's gocognit):
CallbackHandler 11 (was 30), ResolveSignIn 1, firstSignIn 1,
returningSignIn 3, resyncPerson 4, sessionWorkspace 4.

The resolution's log lines move with it onto logging.FromContext(ctx),
the logger the handler used, with the same text, level, attributes and
order. A failure is logged where it happens and returns the unexported
sentinel errSignIn ("sign-in could not be resolved"), which no caller
prints: the handler renders the failure page. provisioning gains one
import, internal/logging.

The handler's second parse of the ID token for the nonce claim, its log
line ("could not parse the ID token's nonce claim") and the struct tag
json:"nonce" are deleted. The comparison reads idToken.Nonce, which
the verification has already read from the same verified payload as a
string; decoding that payload into a struct of one string field cannot
fail, so no input reached the removed failure branch.

The session-binding order (first organization, first workspace, each by
name) is unchanged and now sits in sessionWorkspace alone. The two READMEs
and the model document describe the resolution where it now lives.
TestReturningLoginWritesNoDesiredStateNotice calls ResolveSignIn with
changed claims in place of its copy of the callback's two writes. Its
assertion that a returning login writes no outbox notice for the person,
organization or pool is WIDER: the writes are now ResolveSignIn's own,
the same UpdateUserLogin and UpdatePerson plus its reads, instead of a
copy. Because ResolveSignIn only logs a failed stamp or resync, the test
reads back last_login_ip, display_name and primary_email, which keeps the
two checks that the old test made by failing on a write error (same).

How no output changed:
- go test over ./internal/auth ./internal/provisioning ./internal/server,
  run at the parent and here after resetting the test databases, gives
  1676 test and subtest names with identical results, none failing. The
  sign-in callback goldens and exact log lists are unchanged.
- Literals of internal/auth/auth.go and internal/provisioning's
  provisioning.go, workspace.go and (here) signin.go, summed per file:
  the log message "could not parse the ID token's nonce claim" and the
  tag `json:"nonce"` are gone, "sign-in could not be resolved" is added,
  the other messages are present once each as before, and fmt.Errorf
  formats are 21 at both. Repeat counts of "error", "email", "person_id",
  "org_id" and import paths change where lines moved.
- 44 breaks (68 records, 48 of them one-side) are CAUGHT by the old tests
  at the parent and by the same tests here.
- The operator walkthroughs (./test/e2e/operator-walkthroughs) run on the
  demo state at the parent and here: the same names, results and skip
  messages, 16 passing and 6 skipping.

Unprobed, with the reason:
- Deleting the handler's SetDeadline line is an equivalent edit: the
  session manager's RenewToken, called just before it, sets the same
  deadline, so no break can fail on it. The tests pin the full lifetime
  itself.
- The error half of the organization and workspace guards (err != nil
  beside the empty-list test): neither the generated queries nor the
  test fakes return rows together with an error, so dropping it changes
  no row.
- A first sign-in that succeeds is exercised only against the database.
- RenewToken failing in a real store: only the test wrapper makes it fail.
- A given_name or family_name of another JSON type, which also refuses
  the sign-in: pinned for email_verified only.
- Two first sign-ins of one subject at once: no test runs two callbacks
  concurrently.
- That a returning login writes no outbox row is not probed by a
  one-line edit of signin.go, because the triggers that write those rows
  watch other tables.
- Which attributes each log line carries, and the session's name and
  username keys, beyond the ones the breaks probe: the golden holds
  them all.
- The failure page's texts: the rows share two refusal values, whose
  texts the existing failure-page tests pin.
2026-10-11 19:40:39 -05:00
2026-02-02 22:19:16 -06:00
2026-09-19 19:47:15 -05:00
2026-09-07 21:32:14 -05:00
2026-10-07 01:42:54 -05:00
2026-09-09 13:25:43 -05:00
2026-09-06 02:29:42 -05:00
2026-09-06 02:29:42 -05:00
2026-09-07 21:32:14 -05:00

member-console

One system for memberships and the hosted services they pay for.

member-console is open-source software for offering hosted services on a paid membership. In one place it handles the pieces that otherwise get stitched together by hand — who each member is, what they're entitled to, what they pay — then turns that into the actual, metered services running on their behalf, kept in sync as members join, upgrade, and leave. New kinds of service attach through a shared contract instead of reshaping how membership or billing work, so the offering can grow on one foundation.

It has two sides: an operator panel where staff run the service, and a member panel where members sign in to manage their own plan and workspace.

Developed by the Wiki Cafe Multistakeholder Cooperative, and built for any organization to run.

Operator overview — at-a-glance deployment counts, recent activity, and system status

The operator landing surface

Status: pre-production. The engine is feature-complete for its first service tiers, but this is not yet a hardened, turnkey deployment. Expect rough edges in first-run setup. docs/production-deployment.md sequences a real path to a running production instance, but it is a self-hoster's guide to follow and adapt, not a one-command install — the test stack remains the fastest way to see the console running. See status/ for the honest state of every milestone.

License: AGPL-3.0-only, with commercial licensing available — see LICENSE and COMMERCIAL.md.

What it does

  • Governance spine — identity (via any OIDC provider) → organization → workspace, with auto-provisioning of a default workspace for new members.
  • Entitlements — grants resolve to numeric limits (e.g. FedWiki site count and storage) materialized per resource pool; plan ladders model upgrade/downgrade paths with a generic transition primitive.
  • Billing — Stripe is the billing execution engine. Products, prices, customers, subscriptions, and invoices are mirrored via a webhook pipeline; the console never holds card data.
  • Integrations — an integration contract (manifest, namespaced resource keys, lifecycle verbs) lets a new hosted service conform without reshaping the operator panel or entitlement layer. FedWiki (wiki site provisioning) and Discourse (forum group membership) are the two conforming integrations today.

Architecture

  • Go 1.23, server-rendered HTMX multi-page app — no SPA framework — under a strict Content-Security-Policy (script-src 'self', style-src 'self'; all JS and CSS is self-hosted, no inline <script>/style=).
  • PostgreSQL with a schema-ownership boundary between core and integrations: the console's own data lives in a single core schema, and each integration owns its schema (fedwiki, discourse, stripe) and its own migration ledger — so adding one renumbers nothing (core-first is the only ordering rule). Queries are generated with sqlc; migrations run with goose.
  • Temporal for durable workflows (provisioning, integration sync, billing sweeps).
  • OIDC for authentication — member-console is identity-provider-agnostic and works with any OIDC-compliant IDP (the test stack ships Keycloak).

Full documentation is indexed in docs/, grouped by audience (developer / admin / user). Highlights: deployment architecture, hosting, building an integration, database management, and Stripe.

Running it

What you need:

  • At least one conforming integration — FedWiki (wiki site provisioning) and Discourse (forum group membership) are the two available today (see "What it does" above); a deployment with neither has no product to actually deliver to a paying member. See fedwiki-setup.md and discourse-setup.md.
  • An OIDC identity provider — any OIDC-compliant provider; the console authenticates members and operators against it and never stores passwords itself. See identity-provider-setup.md.
  • PostgreSQL, Valkey, and Temporal — the console's own data store, session store, and durable-workflow engine (provisioning, integration sync, billing sweeps). See database-management.md and deployment-architecture.md.
  • Stripe — optional, only needed to sell paid products; a deployment offering only free or unmetered products can skip it. See stripe.md.

First run, in order:

  1. Deploy the console and the dependencies above — see docs/production-deployment.md for the sequenced path from a built image to a running instance.
  2. Sign in as an operator (the account holding the operator-member role configured on the identity provider).
  3. Follow the in-app setup checklist at /operator/setup, which walks through everything still needed to reach a deliverable product.

Quickstart (local development)

Prerequisites: Docker (with Compose), Go 1.23+, and make.

The test stack brings up Postgres, Valkey, Temporal, and Keycloak in containers, then runs the console from source against them. All commands run from the test/ directory:

cd test
./bootstrap-stack.sh              # generate a per-worktree .env with collision-free ports
docker compose up -d              # Postgres, Valkey, Temporal, Keycloak
set -a; . ./.env; set +a          # load the generated host ports / secrets
go run .. start --config mc-config.yaml

Integrations are opt-in, not baked into the stack: a FedWiki farm or a live Discourse forum is composed by adding its name to the COMPOSE_PROFILES knob in the generated test/.env (and uncommenting that integration's MC_* block in the same file). The console runs fine with neither.

bootstrap-stack.sh prints the URL table it allocated. With default ports the console is at http://member-console.localhost:9481; sign in to the operator panel as alice / password.

On a fresh stack the console rides out Temporal's cold-start with a bounded connect retry, so the first start may pause briefly before the server comes up — no re-run needed.

See test/AGENTS.md for the full stack contract (idempotency, Stripe webhook constraints, teardown, running multiple worktrees concurrently).

Project status & roadmap

See status/ for the development roadmap, milestones, and notes. Substantial changes are tracked as OpenSpec proposals under openspec/.

Contributing

Contributions are welcome — see CONTRIBUTING.md for the development workflow, building and testing, and the project conventions.

Security

To report a vulnerability, see SECURITY.md. Please do not open a public issue for security reports.

License

member-console is free software, licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-only). See LICENSE for the full text.

AGPLv3 is and will remain the default license for this project. If you use, modify, or deploy member-console — including over a network — you must comply with the AGPLv3, which includes making the corresponding source available to users of the service.

If the AGPLv3 does not fit your needs — for example, you want to embed member-console in a proprietary product or run it as a hosted service without the AGPL's source-sharing obligations — a separate commercial license is available. See COMMERCIAL.md.

As the sole copyright holder, Christian Galo can offer member-console under both the AGPLv3 and separate commercial terms. Contributions are accepted under a Contributor License Agreement that preserves that ability (CLA.md; how to sign is in CONTRIBUTING.md), and commercial licenses fund continued development. Anything already published under the AGPLv3 stays under the AGPLv3; the public license is not withdrawn retroactively.

Third-party components redistributed in this repository keep their own licenses. See NOTICE.

S
Description
Member console application for users to create, acccess, and manage their accounts associated with the Wiki Cafe MSC.
Readme AGPL-3.0
25 MiB
Languages
Go 90.6%
HTML 4.2%
PLpgSQL 2.7%
Shell 1.4%
JavaScript 0.5%
Other 0.4%