Skip to content

feat(client)!: move the client API under /projects/{projectID}#262

Merged
jeroenrinzema merged 4 commits into
mainfrom
feat/client-api-project-in-url
Jun 24, 2026
Merged

feat(client)!: move the client API under /projects/{projectID}#262
jeroenrinzema merged 4 commits into
mainfrom
feat/client-api-project-in-url

Conversation

@jeroenrinzema

Copy link
Copy Markdown
Contributor

The client API derived the project from the authenticating credential, which left trusted-issuer JWTs resolved by their self-asserted iss with no project scope — a cross-tenant resolution gap. This makes the project an explicit path segment on every authenticated client endpoint.

/api/client/...  ->  /api/client/projects/{projectID}/...

{projectID} is the project UUID. Auth headers are unchanged — same API key / trusted-issuer JWT / session token; only the URL gains the project. This is a deliberate hard cut — no backwards-compatible routes.

What changed

  • Spec + bindings: all /api/client/* routes moved under /api/client/projects/{projectID}/…; regenerated chi-server bindings (every handler takes projectID). /unsubscribe and /preferences/{projectID}/{userID} (public pages) are untouched.
  • Auth (the fix): every credential type fails closed unless its project matches the URL (enforceURLProject). Trusted-issuer resolution is now (project_id, issuer)-scoped, so a self-asserted iss can never reach another project's method.
  • Store: GetTrustedIssuer(projectID, issuer); the Redis issuer-cache key is project-namespaced; UNIQUE(project_id, issuer) among active methods (denormalised project column + hard-delete of the trusted-issuer child on soft-delete, so an issuer is freed for re-registration).
  • Docs: 9 MDX files updated to the new paths.

Contract delta for clients / SDKs

Every authenticated client endpoint gains /projects/{projectID} after /api/client; the session-mint endpoint becomes /api/client/projects/{projectID}/auth-methods/{authMethodID}/sessions. A credential presented on a different project's URL is rejected (401). SDK PRs are open: lunogram/go-sdk#2, lunogram/js-sdk#7, lunogram/py-sdk#1.

Verification

go build/vet ./..., gofmt clean; auth / store-management / client + management controller / rbac suites pass. New tests cover project mismatch rejection, same-issuer-different-project, and re-registration after delete.

@jeroenrinzema jeroenrinzema changed the base branch from feat/ap-10-event-allowlist to main June 23, 2026 13:55
@jeroenrinzema

Copy link
Copy Markdown
Contributor Author

Held back from the batch admin-merge. The rest of the stack (#243#255, #261) is now in main.

Why it can't be merged mechanically: #261 (now merged) replaced the grant_constraints JSON model with the grant_instances row model + request-time CreateConstraints.Enforce. This branch still carries #253's older grant_constraints subsystem (constraints.go, management controller, OpenAPI resources.yml/_gen.go, store, tests). Merging main in produces real semantic conflicts in security-critical auth code — both sides independently rewrote invalidateCaches and DeleteAuthMethod, and the constraints API surface diverged.

What this PR actually wants is the trusted-issuer project-scoping (project_id column + project-scoped cache keys + child-row cleanup on delete) and the /projects/{projectID} URL move. That work should be rebased onto the new grant_instances model: take main's constraints subsystem wholesale, re-apply only the project-scoping diffs, and renumber the migration (1764116042_trusted_issuer_project_scope collides with main's 1764116042_grant_instances → bump to 1764116043).

The client API derived the project from the authenticating credential, so a
trusted-issuer JWT was resolved by its self-asserted `iss` with no project
scope — a cross-tenant resolution gap. The project is now an explicit path
segment on every authenticated client endpoint:

  /api/client/...  ->  /api/client/projects/{projectID}/...

- spec + regenerated chi-server bindings; every handler takes projectID.
- auth: every credential type fails closed unless its project matches the URL
  (enforceURLProject); trusted-issuer resolution is now (project_id, issuer).
- store: GetTrustedIssuer is project-scoped; the issuer cache key is
  project-namespaced; UNIQUE(project_id, issuer) among active methods
  (denormalized project column + hard-delete child on soft-delete).
- docs updated.

BREAKING CHANGE: all client API paths require the project UUID; no legacy routes.
…sts on main

Two PR merges collided in the v1/client test package, breaking `make lint`
and `make test` on main (the build failure masked the second issue):

- `capturingPublisher` was declared in both inbox_idor_test.go and
  ownscope_helpers_test.go. Drop the duplicate (simpler) copy from
  inbox_idor_test.go and adapt publishedIdentifiers to the canonical
  captured()/capturedMessage API in ownscope_helpers_test.go.

- ownDataActor/allDataActor built actors with a random UUID and no
  auth-method row. After per-grant create-constraint enforcement landed
  (#261), CreateConstraints.Enforce looks the method up by the actor id
  and fails closed on a missing row (sql.ErrNoRows -> 500), breaking every
  PostUserEvents own/all-data test. Provision a real, unconstrained auth
  method and use its id as the actor identity, mirroring actorContext.

go vet ./... and the full v1/client suite pass.
…RL rework

These suites landed on main after this branch's base and exercise the
pre-rework contracts; rebasing onto the grant_instances model surfaced them:

- store invariants: the trusted-issuer invariant flipped from "globally
  unique" to "unique per project, reusable across projects". Rewrite the
  subtest to assert per-project uniqueness plus cross-tenant resolution
  isolation (each project resolves the shared iss to its own method).

- auth middleware: WithSession / WithTrustedIssuer now bind the credential
  to the URL project (enforceURLProject / project-scoped GetTrustedIssuer).
  Drive the existing positive tests through clientRequestCtx so they carry
  the URL project, and update the issuerDB fake to match the new
  (project_id, issuer) query.

- client controllers: every chi-server handler now takes a ProjectID path
  param. Thread it through the own-scope/guard/session/inbox/event test
  call sites (uuid.Nil where the handler derives the project from the actor;
  the real project where CreateSession enforces URL == method project).

go vet ./... clean; full suite green.
@jeroenrinzema jeroenrinzema force-pushed the feat/client-api-project-in-url branch from 453f75e to cdcf9a9 Compare June 23, 2026 16:10
@jeroenrinzema

Copy link
Copy Markdown
Contributor Author

Reworked onto grant_instances — now mergeable ✅

Rebased the two original commits onto current main and reconciled the conflicts the grant_constraintsgrant_instances refactor (#261) introduced. Branch history force-pushed; the feature itself (project-in-URL + trusted-issuer project scoping) is unchanged.

What the rebase required:

  • Store model: dropped the carried-over setGrantConstraints (main removed the grant_constraints column); kept this PR's DeleteAuthMethod trusted-issuer hard-delete doc. The project-scoped insertTrustedIssuer / GetTrustedIssuer(projectID, issuer) and UNIQUE(project_id, issuer) are intact.
  • Migration: renumbered 1764116042_trusted_issuer_project_scope1764116043 (collided with main's 1764116042_grant_instances; now ordered after it).
  • Store invariants test: the trusted-issuer invariant flipped from globally unique to unique per project, reusable across projects — rewrote it to assert per-project uniqueness and cross-tenant resolution isolation.
  • Auth middleware tests (main's, added after this branch's base): WithSession / WithTrustedIssuer now bind to the URL project, so the positive tests run through clientRequestCtx; the issuerDB fake matches the new (project_id, issuer) query.
  • Client controller tests: threaded the new ProjectID path param through every stale handler call site (uuid.Nil where the controller derives the project from the actor; the real project where CreateSession enforces URL == method project).

Verification: go vet ./... clean; full go test ./... green (37 packages).

Dependency note: this branch includes the fix(client/tests) commit from #266 (the standalone main-CI fix) so it builds and tests green on its own. #266 is the minimal fix for main and should merge first; once it lands, that commit becomes a no-op here on rebase. Merging this PR alone also resolves the main-CI break.

@jeroenrinzema jeroenrinzema merged commit 9b20d38 into main Jun 24, 2026
6 checks passed
@jeroenrinzema jeroenrinzema deleted the feat/client-api-project-in-url branch June 24, 2026 10:39
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.

1 participant