Skip to content

Gate signup with IDENTITY_SIGNUP_MODE (#78) - #88

Merged
AntoineToussaint merged 6 commits into
mainfrom
issue-78-signup-mode
Aug 9, 2026
Merged

Gate signup with IDENTITY_SIGNUP_MODE (#78)#88
AntoineToussaint merged 6 commits into
mainfrom
issue-78-signup-mode

Conversation

@AntoineToussaint

Copy link
Copy Markdown
Contributor

Closes #78.

Summary

  • Signup was unconditionally open self-serve. This adds an identity workspace config key, IDENTITY_SIGNUP_MODE = open | invite | waitlist, that gates provisioning of first-seen identities only — Login and Invite are never gated, so an invitee can always obtain an account while a stranger cannot, and existing users are never locked out.
  • open is the default (empty value → open), so upgrading an existing deployment changes nothing. Unrecognised values fail startup closed via auth.ParseSignupMode, consistent with OAuthRequestPolicy/CORS; the parser also returns the most-restrictive mode on error so an ignored error still rejects strangers.
  • invite wires the existing invitations subsystem into access control: signup requires a pending, unexpired invitation for the verified email, and the invitee is provisioned into the inviting org. waitlist wires the existing WAITLIST_STATE_* machine in for the first time: signup requires an approved or invited entry.

Implementation notes

  • The gate lives in the Postgres resolver's Signup branch, inside the existing serializable auth transaction, so the invitation/waitlist checks are consistent with provisioning. SetSignupMode mirrors the existing SetBootstrapAdminEmail composition setter; work.go reads IDENTITY_SIGNUP_MODE through the Codefly identity configuration and refuses to start on an unrecognised value.
  • The invitation membership-and-accept tail is now a shared acceptInvitation helper used by both the token-based invite flow and the invite-mode signup flow.
  • Out of scope / non-goal: the invite-mode signup path (email match, no token) does not emit the auth.invitation.accepted business event that the explicit token flow does; the membership and acceptance are still persisted transactionally in the resolver.
  • The referenced module/docs/IDENTITY_ACCESS_PLAN.md design doc does not exist in the repo; this change follows the issue's stated scope and the #77 intent split directly.

Test plan

  • pure: TestParseSignupMode / TestParseSignupMode_UnrecognisedFailsClosed cover the parser, the empty→open default, and fail-closed on garbage.
  • pipeline (real Postgres), one case per mode:
    • open: unknown email signs up successfully
    • invite: unknown email with no invitation is rejected and provisions nothing
    • invite: invited email signs up successfully and lands in the inviting org
    • invite: an existing user can still log in (login is not gated)
    • waitlist: pending entry rejected; approved entry accepted
  • Full pkg/auth/pg suite green (confirms the resolveInvite refactor did not regress the token flow).
  • node module/tools/base-integrity.mjs check passes; base-manifest.json regenerated for the edited canonical files.

🤖 Generated with Claude Code

Add an identity workspace config key that gates provisioning of first-seen
identities: open (default) | invite | waitlist. Login and Invite are never
gated. Unrecognised values fail startup closed via ParseSignupMode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AntoineToussaint and others added 5 commits August 5, 2026 07:45
…xpiry shadowing

The invite- and waitlist-mode signup gates authorized on email equality
without requiring EmailVerified, so an identity presenting an unverified
but matching email could sign up — in invite mode inheriting an org and
role addressed to that email. Check EmailVerified before any email-keyed
lookup (no enumeration oracle, fails closed), matching the invite-token
path and the Claims contract. requireApprovedWaitlist now takes *Claims
so the verification fact travels with the address.

Invited signup also picked the newest pending invitation and then checked
expiry in Go, letting a newer expired invitation shadow an older valid
one. Move the expiry predicate into SQL (expires_at > CURRENT_TIMESTAMP),
the codebase's canonical usable-invitation predicate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first-party vulnerability gate (npm audit --omit=dev --audit-level=high)
went red after new advisories landed: fast-uri (GHSA-7p8r-x3mc-p8w7, high)
and brace-expansion (GHSA-rgw5-rvv9-x895, high). The existing overrides
pinned the previously-patched versions (fast-uri 3.1.4, brace-expansion
5.0.8), which the new advisories now cover.

Widen the override selectors to the whole vulnerable range and pin the
patched versions (fast-uri 3.1.5, brace-expansion 5.0.9). Both are within
their consumers' semver ranges, so no direct dependency changes. Lockfile
regenerated and base manifest updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ync-drift

The nextjs:0.0.124 companion no longer emits
src/gen/saas-starter_auth-sidecar_grpc_pb.ts — the proto carries only a
file descriptor, so the current protoc-gen-es produces nothing for it.
The committed file is a leftover from an older generator, and it went
undetected until this PR touched frontend/package.json, which marks the
frontend service affected and runs its sync-drift check.

Remove the stale artifact (matching `codefly sync service` against the
pinned companion) so generated sources match the companion, and update
the base manifest. No source references the deleted file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n start

Codefly's runtime health-checks test dependencies (minio, redis, …) at
host.docker.internal. Docker Desktop provides that name on macOS/Windows,
but Linux GitHub runners do not resolve it on the host — and a runner-image
change dropped it, so the frontend test phase began failing on every
frontend-affected PR with:
  minio is not ready: Get "http://host.docker.internal:60430/minio/health/live":
  dial tcp: lookup host.docker.internal on 127.0.0.53:53: no such host

Frontend tests passed on 2026-08-03 and broke by 2026-08-07, matching a
runner change rather than any repo change. Published dependency ports bind
to the host loopback, so map host.docker.internal → 127.0.0.1 in the
runner's /etc/hosts before the gate. Idempotent and additive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first-party vulnerability gate went red again on advisories published
since the last override refresh:
  - js-yaml GHSA-5p4m-2wfm-xmqj (CVE-2026-59870, high) — frontend
  - nanoid  GHSA-2v37-7h3g-55p8 (high) — frontend and marketing

js-yaml is a direct frontend dependency, so bump its declared range to
^4.3.1 (patched) rather than override it (npm rejects an override that
conflicts with a direct dependency). nanoid is transitive in both apps —
pin it to 3.3.17 via a range-scoped override. Both are patch bumps within
their consumers' ranges. Lockfiles regenerated, base manifest updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AntoineToussaint
AntoineToussaint merged commit b9c4297 into main Aug 9, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Identity access A2] Gate signup with IDENTITY_SIGNUP_MODE (open | invite | waitlist)

1 participant