Skip to content

feat(lifecycle): run non-tunnel upgrades under the provided r_* credential#133

Merged
I-am-nothing merged 2 commits into
mainfrom
feat/upgrade-lifecycle-credential
Jul 3, 2026
Merged

feat(lifecycle): run non-tunnel upgrades under the provided r_* credential#133
I-am-nothing merged 2 commits into
mainfrom
feat/upgrade-lifecycle-credential

Conversation

@I-am-nothing

Copy link
Copy Markdown
Contributor

What

Closes the documented UpgradeHandler KNOWN GAP (referenced from api-platform's module_lifecycle.go upgradeRequest doc comment): a deployed (non-tunnel) upgrade now runs its (from, to] migrations under the provided per-(app, module) r_* credential with search_path = the provided app schema — exactly like Install already does.

Pairs with the api-platform deployed-lifecycle-transport PR: the api side already sends {from, to, appId, schema, credential:{username, token}} on the non-tunnel path (field names mirror installRequest verbatim, by design, so this adoption needs no wire change).

How

  • UpgradeRequest = VersionRequest + InstallRequest — the api-side upgradeRequest shape, verbatim.
  • Extracted injectLifecycleContext out of injectInstallContext; install, upgrade, and downgrade now share the single context-folding implementation, so credential/schema semantics cannot drift between the lifecycle verbs. Same rules as install: env-URL supplies host/port/database, body supplies username/token, "credential": {} falls through to the dev pool.
  • Module.Tx (the ScopeApp runner) already resolves the pool from db.CredentialFrom(ctx) and SET LOCALs search_path from db.SchemaFrom(ctx) — no runner changes needed.
  • Back-compat: a bare {from, to} body (the dev-tunnel path) behaves byte-for-byte as before — no schema/credential injected, env-pool fallback.
  • Downgrade shares the decoder and gets the identical treatment (a deployed downgrade had the identical gap).

Lambda-envelope reachability (checked, no change needed)

The lifecycle routes are mounted on m.router (mountSystemRoutes), and both the real Lambda transport (lambda.Start(runtime.NewLambdaHandler(m.router))) and the dev /__mirrorstack/lambda-invoke shim wrap that same router. X-MS-Internal-Secret is exempt from the envelope's identity-header strip (msAuthSecretHeaders), so internalAuth on /__mirrorstack/platform/* validates it normally. Lifecycle install/upgrade are therefore servable through the envelope as-is.

Platform-side note (out of scope here)

EnsureModuleRole (api-platform) does not set ALTER ROLE ... IN DATABASE ... SET search_path. That is harmless for this path (the SDK explicitly SET LOCALs the schema inside every migration transaction) and for runtime invokes that carry AppSchema in the envelope — but any invoke arriving without AppSchema leaves the r_* role on the cluster-default "$user", public search_path, which is what the hand-built E2E had to patch with a manual ALTER ROLE. Recommend the api-platform transport PR either always populate AppSchema or add the ALTER ROLE default as defense-in-depth.

Tests

  • Upgrade with credential+schema → runner receives ctx with credential (env-base host/port/db + body username/token) and correct schema.
  • Bare {from, to} → no schema, no credential (dev path pinned unchanged).
  • Malformed credential/schema (wrong JSON types) → clean 400, no panic.
  • "credential": {} → falls through like install.
  • Downgrade credential parity pinned.

go build ./..., go vet ./..., go test ./... green.

🤖 Generated with Claude Code

I-am-nothing and others added 2 commits July 3, 2026 23:20
…ntial

Closes the documented UpgradeHandler KNOWN GAP: the platform's
non-tunnel upgradeRequest already carries {from, to, appId, schema,
credential:{username, token}} (field names mirror InstallRequest
verbatim), but the SDK's decodeVersionRequest read only from/to — a
deployed module's (from, to] migrations silently fell back to the env
pool instead of running under the per-(app, module) r_* role against
the app schema.

- UpgradeRequest = VersionRequest + InstallRequest, adopting the
  api-side wire shape with no wire change.
- injectLifecycleContext extracted from injectInstallContext and shared
  by install, upgrade, and downgrade, so the credential + search_path
  semantics cannot drift between the lifecycle verbs. Empty-credential
  shape compat ({}) and the env-base host/port/database split are
  identical to install.
- Bare {from, to} bodies (dev-tunnel path) behave exactly as before:
  no schema or credential injected, env-pool fallback.
- Downgrade gets the same treatment for free by sharing the decoder —
  a deployed downgrade has the identical gap.

Lifecycle reachability through the lambda envelope was verified, not
changed: the routes live on m.router, which both lambda.Start and the
dev lambda-invoke shim wrap via runtime.NewLambdaHandler, and
X-MS-Internal-Secret survives the identity-header strip for
internalAuth to validate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A deployed-module lifecycle call arrives through the lambda envelope — the
envelope IS the platform authentication (shim secret gate / Lambda IAM) and
there is no per-session tunnel token to present, so internalAuth rejected it
(header_present=false). Honor auth.PayloadTrusted exactly like RequireProxy
already does; the mark is set only by runtime.NewLambdaHandler, never from
inbound request data. E2E-found on the local deploy sim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@I-am-nothing I-am-nothing merged commit cee22a6 into main Jul 3, 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