Files
zeekay e4b1f3c75c a Base is addressed at /v1/bases, and Base stops running a second auth
Two things wore the name "platform" here, and neither is the PaaS at
platform.hanzo.ai. That collision is why "where are my Bases?" had no answer a
person could act on: one prefix, two products, and the one you reached was
whichever you happened to mean.

A Base is what an org gets — its own collections, its own files, its own key —
so the resource a client addresses is /v1/bases. Not /v1/orgs: the org is IAM's
noun and Base does not get a second one. Everything scoped to an org now hangs
off the Base it belongs to (/v1/bases/{id}/config, /creds, /customers), and
compliance moves to /v1/compliance, a sibling of /v1/iam rather than a child of
Base's own resource. The package is `org`, which seven of its files already
were. Nothing addressed the old prefix — no client, no UI, no manifest — so this
renames a surface rather than breaking one.

The auth proxy is DELETED, not renamed. Its five routes reshaped bodies and
forwarded to /v1/iam/login, /signup, /userinfo and /send-verification-code —
which the opaque /v1/iam/{path...} passthrough beside it already reaches. So
Base carried a hand-written login, signup and one-time-code flow whose only job
was to be a second way to reach IAM. Auth is IAM's, and one way is the whole
point of that rule.

The prefix test that already banned /api/ bans this too, so the name cannot come
back: it reads route LITERALS through go/ast, so a comment explaining the ban is
prose and a route is a route. Verified to fail on the exact regression.
2026-08-10 19:18:08 -07:00

55 lines
2.0 KiB
Go

package tasks
import (
"context"
"os"
"strings"
"github.com/hanzoai/tasks/pkg/sdk/client"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
)
// iamTokenSource mints and auto-refreshes a short-lived IAM client_credentials
// bearer from this process's own service identity, for cloud's identity-gated
// tasks engine (Embedded.ServeGated, RequireIdentity). The bearer rides every
// RPC and the gated engine org-scopes the request to the token owner (tasks
// CONTRACT §6) — the credential that lets a hanzo-base app run durable work on
// cloud instead of the retired standalone tasksd.
//
// Credentials + endpoint use the canonical base convention, the same env
// base/plugins/org reads: $IAM_CLIENT_ID, $IAM_CLIENT_SECRET, and
// $IAM_ENDPOINT (falling back to $IAM_URL). The endpoint MUST resolve to the
// in-cluster IAM Service: a server-side POST to the public issuer host is 403'd
// by Cloudflare (edge 1006). The token's iss stays the configured public issuer
// regardless of the mint URL, matching cloud's validator. AuthStyleInParams puts
// the credentials in the form body — hanzo.id does not accept Basic.
//
// Returns nil when the identity is unconfigured: the caller then dials the engine
// ungated, correct for a dev/loopback frontend.
func iamTokenSource() client.TokenSource {
clientID := strings.TrimSpace(os.Getenv("IAM_CLIENT_ID"))
clientSecret := strings.TrimSpace(os.Getenv("IAM_CLIENT_SECRET"))
base := strings.TrimSpace(os.Getenv("IAM_ENDPOINT"))
if base == "" {
base = strings.TrimSpace(os.Getenv("IAM_URL"))
}
if clientID == "" || clientSecret == "" || base == "" {
return nil
}
cc := &clientcredentials.Config{
ClientID: clientID,
ClientSecret: clientSecret,
TokenURL: strings.TrimRight(base, "/") + "/v1/iam/oauth/token",
AuthStyle: oauth2.AuthStyleInParams,
}
ts := cc.TokenSource(context.Background()) // caches + refreshes on expiry
return func(context.Context) (string, error) {
t, err := ts.Token()
if err != nil {
return "", err
}
return t.AccessToken, nil
}
}