feat(contributions): host-side contribution slots primitive#105
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
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
Test plan
Follow-ups (separate PRs)
🤖 Generated with Claude Code