Skip to content

feat(contributions): host-side contribution slots primitive#105

Merged
I-am-nothing merged 3 commits into
mainfrom
feat/contributions-primitives
May 25, 2026
Merged

feat(contributions): host-side contribution slots primitive#105
I-am-nothing merged 3 commits into
mainfrom
feat/contributions-primitives

Conversation

@I-am-nothing

Copy link
Copy Markdown
Contributor

Summary

Introduces an SDK primitive for host modules to declare contribution slots that other modules can register against. Closes the boilerplate gap where every host module (today: oauth-core) hand-rolls a registrations table + register/unregister/list HTTP endpoints for each contribution surface.

Stacks on: #103 (PlatformAuth trusted-forwarder) + #104 (UIPage.Route rename). Branched off `feat/uipage-route-icon-drop` (PR #104's branch) which itself contains the PR #103 commit cherry-picked. After #103 and #104 land in main, this PR rebases cleanly to a single net commit.

API

// Host module declares the slot at init:
ms.DefineContribute[ProviderContribution]("providers")

// SDK auto-mounts under /__mirrorstack/contrib (Internal-scoped):
//   POST   /providers/{id}   register/upsert (validates against T)
//   DELETE /providers/{id}   unregister (404 if not present)
//   GET    /providers        list (newest-first, LIMIT 1000)

T's shape gates incoming payloads. `json.Decoder.DisallowUnknownFields()` so a contributor can't sneak in extra fields the host doesn't expect.

Storage

One per-module table `_contributions` (slot text, contribution_id text, payload jsonb, registered_at timestamptz, PK on slot+id). Table name is `pgx.Identifier.Sanitize`'d once at `NewStorage` to match house convention. Auto-`CREATE TABLE IF NOT EXISTS` on `Start` when any slot is declared; prod schema management belongs to the lifecycle install hook in a follow-up.

Manifest

New `definedContributions[]` field surfaces declared slots so the catalog + agents can introspect what a host accepts.

Scope drift: localDevCORS middleware

I also bundled in a small CORS middleware in `core/module.go` that's parallel to the existing auth bypass: when `MS_INTERNAL_SECRET` is unset (local dev, no tunnel), echo the request Origin with credentials so module bundles hosted on the platform domain can fetch the module's own routes cross-origin. Tunnel/prod leaves CORS off — bundles in those modes proxy through the platform same-origin. Conceptually separate from contributions; bundled for review velocity. Happy to split if preferred.

/simplify consensus applied

  • `slices.SortFunc` (was hand-rolled insertion sort)
  • `DisallowUnknownFields` on the validator (closes silent payload pollution)
  • `LIMIT 1000` on `Storage.List`
  • `pgx.Identifier.Sanitize` on the table name
  • Dropped redundant inner `MaxBytesReader` (outer `httputil.MaxBytes` middleware already caps)
  • Trimmed verbose doc comments
  • Single `RLock` in `Registry.List` (no TOCTOU between Keys + row read)
  • `Registry.Len()` instead of `len(Keys())` for the count-only path

Test plan

  • `go build ./...` clean
  • `go test ./...` clean (existing tests; new package has no unit tests yet — usage tests will come with the oauth-core consumer PR that migrates the `providers` slot to use this)
  • End-to-end via oauth-core's `providers` slot in a follow-up PR

Follow-ups (separate PRs)

  • oauth-core consumer migration (the proof-of-use)
  • Lifecycle-hook table creation (prod schema management instead of dev-only auto-CREATE)
  • Unit tests for the contributions package (currently covered transitively by oauth-core integration tests)
  • Optional split of `localDevCORS` middleware to its own PR if reviewers prefer

🤖 Generated with Claude Code

I-am-nothing and others added 3 commits May 25, 2026 12:09
…ypass

Closes the loop on the tunnel-auth workstream's SDK slice (memory:
feedback_tunnel_auth_no_bypass). PlatformAuth now has three identity
sources, in priority order:

  1. Identity preset in ctx (Lambda authorizer path — unchanged).
  2. Trusted-forwarder injection: a request carrying a valid
     X-MS-Internal-Secret may also assert user identity via
     X-MS-User-ID / X-MS-App-ID / X-MS-App-Role headers, which
     PlatformAuth promotes to ctx.Identity. Same trust signal
     InternalAuth uses; no second secret needed.
  3. Local-dev bypass: when MS_INTERNAL_SECRET is unset AND we're
     not in Lambda, a synthetic admin identity is injected so
     `mirrorstack dev` (no tunnel) can render platform-scope
     routes without the developer wiring auth manually. Matches
     InternalAuth's local bypass shape.

Behavior matrix mirrors InternalAuth:

  inLambda + secret unset → 503 (operator misconfig)
  inLambda + secret set   → enforce (Lambda authorizer wins if preset)
  local    + secret unset → bypass with synthetic admin
  local    + secret set   → enforce (tunnel mode — CLI sets the
                            secret via mirrorstack-cli #33)

The trusted-forwarder design unblocks the cascade:
  - api-platform proxy/dispatch attaches signed headers when
    forwarding browser requests on behalf of an authenticated
    platform user → modules see the right Identity automatically.
  - In local dev with no tunnel, the bypass lets module UIs render
    against platform-scope routes without coordinating env between
    web-applications and the running module.

Test plan:
  - All 7 auth.PlatformAuth* tests pass (preset ctx, local bypass,
    Lambda 503, missing secret, wrong secret, missing identity
    headers, valid headers → identity injected).
  - internal/core.TestPlatform_LocalDevBypass_InjectsSyntheticAdmin
    and TestPlatform_SecretSet_RejectsNoHeader replace the old
    TestPlatform_RejectsNoAuth, pinning the new contract end-to-end
    through the SDK's scope-routes plumbing.
  - Whole-repo `go test ./...` clean except for a pre-existing
    flake in internal/refcache.TestSlowPath_SingleflightCoalesces
    (concurrency timing, unrelated to auth).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two additions, both layered on the existing manifest/auth scaffolding
in this branch (PR #103 + #104 land first):

1. internal/contributions/ — NEW package
   - Slot, Registry, Storage, Handlers types.
   - SDK auto-mounts under /__mirrorstack/contrib:
       POST   /<slot>/<id>   register/upsert (Internal-scoped)
       DELETE /<slot>/<id>   unregister (404 on miss; idempotency at
                              the HTTP layer is a no — callers see 404
                              on double-delete so they know it landed)
       GET    /<slot>        list (newest-first, LIMIT 1000 cap so
                              runaway contributors can't blow up
                              response size)
   - Per-module table <id>_contributions (jsonb payload + slot/id PK),
     auto-CREATE TABLE IF NOT EXISTS on Start when any slot is
     declared; prod schema management belongs to the lifecycle install
     hook in a follow-up.
   - DefineContribute[T] generic API. The validator runs
     json.Decoder.DisallowUnknownFields() so an unknown field gets
     rejected at register time instead of polluting the jsonb column.
   - Table name is pgx.Identifier.Sanitize'd once at NewStorage,
     matches the SDK's convention for identifier interpolation in
     handler-written SQL.
   - Manifest grows a DefinedContributions []SlotInfo field so the
     catalog can introspect which slots a host accepts.
   - mirrorstack.go exports DefineContribute[T] and Contributions().

2. internal/core/module.go — local-dev CORS middleware
   - Installs an origin-echoing CORS layer on the router only when
     MS_INTERNAL_SECRET is unset (i.e. parallel to the auth bypass).
     Frees a module's bundle hosted on the platform domain (e.g.
     localhost:3001) to fetch the module's /platform/* and /me
     endpoints cross-origin with credentials. Tunnel/prod leaves
     CORS off — bundles in those modes proxy through the platform
     at same-origin.
   - Bundled here for review-velocity. Conceptually a separate concern;
     reviewers can ask for a split if preferred.

/simplify pass consensus fixes applied:
  - slices.SortFunc (was hand-rolled insertion sort)
  - DisallowUnknownFields on the validator
  - LIMIT on Storage.List
  - pgx.Identifier.Sanitize on the table name
  - Drop redundant inner MaxBytesReader (outer middleware caps)
  - Trim verbose doc comments
  - Single RLock in Registry.List (no TOCTOU between Keys + read)
  - Registry.Len() instead of len(Keys()) when only count is needed

Build + go test ./... clean. No new test files yet — usage tests come
with the oauth-core consumer PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@I-am-nothing I-am-nothing merged commit 3d5b890 into main May 25, 2026
1 check 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.

1 participant