cmd

package
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 38 Imported by: 0

Documentation

Overview

Package cmd holds the CLI subcommand implementations.

Index

Constants

View Source
const TargetOrgExternalID = "authio:target-org"

TargetOrgExternalID is the sentinel org external_id used when target_organization_id pins imports to an existing Authio org.

Variables

This section is empty.

Functions

func Apply

func Apply(args []string) error

Apply builds the same plan as Check and executes it action by action, stopping at the first failure (already-applied actions stay applied — re-running apply is safe because every action is idempotent by identity key).

func Bootstrap

func Bootstrap(args []string) error

Bootstrap dispatches `authio bootstrap <subcommand>`.

Today the only subcommand is `mint`, which calls the authio_management-api admin endpoint at POST /v1/admin/bootstrap-tokens to mint a single-use, hashed bootstrap token. The plaintext is printed to stdout exactly once; the CLI does NOT persist the plaintext to ~/.authio or anywhere else (that would defeat the single-use property).

Auth: the call carries a `sk_live_…` key as a Bearer token. The key must belong to the platform admin project (AUTHIO_ADMIN_PROJECT_ID on the management-api). Source order:

  1. --api-key flag
  2. AUTHIO_API_KEY env var
  3. ~/.authio/credentials.toml (`--profile`, default `default`)

The API URL follows the same order via --api-url / AUTHIO_MGMT_API_URL / the saved profile.

func Check

func Check(args []string) error

Check loads authio.yaml, builds the plan, prints it, and exits 2 when there is drift — CI-friendly (0 = in sync, 1 = error, 2 = drift).

func Clearance

func Clearance(args []string) error

Clearance is `authio clearance <login|serve|init|explain>` — the local sidecar for Authio Clearance (agent authorization). See internal/clearance.

func Dev

func Dev(args []string) error

Dev runs a local HTTP proxy that forwards to the configured auth-core (or --target=...) and pretty-prints every request/response in real time. Useful for SDK customers debugging integrations against the live alpha.

func Doctor

func Doctor(version string, args []string) error

Doctor diagnoses the local Authio setup and prints a pass/warn/fail checklist (or JSON with --json). `version` is the running CLI version, injected from main.

authio doctor [--profile name] [--json] [--repo owner/name] [--no-webhook-ping]

func Domains

func Domains(args []string) error

Domains handles `authio domains <subcommand>`.

The project comes from the secret key. There is no project id argument, so a key cannot be aimed at a different tenant.

func Env

func Env(args []string) error

Env surfaces and switches the active Authio environment for CLI operations.

authio env [show]          show the active profile's environment
authio env list [--json]   list configured profiles + their environment
authio env use <profile>   make a profile active for future commands

Design note (environments / A3): an Authio api key is environment- scoped — a `sk_test_` key only ever sees its non-production project's data, a `sk_live_` key only its production project's. There is no sk_-authed route to enumerate a tenant's *other* environments (that surface is `/v1/session/environments`, which requires a dashboard session, not an api key). So the CLI models "environments" as named credential profiles: each profile holds one environment-scoped key, and `env use` selects which one subsequent commands target. `env list` resolves each profile against `/v1/projects/me` to show its real environment + tenant.

func Import

func Import(args []string) error

Import dispatches `authio import <provider> [flags]`.

Two flag dialects are supported in the same surface:

Legacy streaming (per-user POST + cursor; only auth0|clerk|cognito|firebase|supabase):
  authio import <provider> --file <path> [--profile name] [--dry-run] [--force] [--rate-limit-rps N]

Plan-based (all 8 providers, what the migration wizard uses):
  authio import <provider> --input <path>
     --management-api-url <url>   (or rely on --profile)
     --api-key <key>              (or rely on --profile)
     [--orgs-table <file>]   only for providers without native orgs
     [--dry-run]              prints the ImportPlan as JSON
     [--json]                 stream NDJSON progress events (used by dashboard)
     [--default-org-name N]   name for the synthetic org for org-less providers

The two dialects are picked apart by which flag is present: `--input` (or `--plan`) triggers plan-mode. Plain `--file` keeps the legacy path.

func Init

func Init(_ []string) error

Init scaffolds an example app — points at create-authio-app.

func Keys

func Keys(args []string) error

Keys handles `authio keys <subcommand>`.

authio keys rotate [--profile name] [--name label]

Creates a replacement workspace secret key, updates credentials.toml, then revokes the previous key. Uses existing /v1/api-keys endpoints (create + delete) — the roll endpoint cannot rotate the key that is currently authenticating the request.

func Listen

func Listen(args []string) error

Listen forwards Authio events to a local HTTP endpoint — the Authio answer to `stripe listen`.

authio listen --forward http://localhost:3000/webhooks [flags]

Implementation (v1, zero server changes): the CLI POLLS the existing sk_-authed Events API (GET /v1/events) — the same cursor-paginated, project-scoped surface SDK consumers use — and replays each new event to the local target as a fully-formed Authio webhook: identical JSON envelope, an `Authio-Signature` HMAC computed with the exact scheme the webhooks worker uses, plus `Authio-Event-Id` / `Authio-Event-Action` headers. Because it polls, deliveries arrive with up to one poll interval of latency (default 2s) — fine for local development, not a production transport.

Signature passthrough: pass `--secret whsec_…` (a real endpoint's signing secret) to reproduce that endpoint's exact signature so your existing verification code runs unchanged locally. Omit it and the CLI generates a throwaway secret and prints it — set it in your local handler to verify.

func Login

func Login(args []string) error

Login runs the device-code flow against the management-api.

func Logs

func Logs(args []string) error

Logs is `authio logs tail`. Phase 3.5 streams from authio_audit's query API; for now, we resolve the saved credentials and show how to query the audit log via curl. This is a stop-gap until the streaming endpoint lands.

func MCP

func MCP(args []string) error

MCP serves a newline-delimited JSON-RPC MCP server on stdio. Tools call the project-scoped secret-key API only.

func Migrate

func Migrate(args []string) error

Migrate dispatches `authio migrate <subcommand>`.

The only public subcommand today is `migrate run --job-id <id>`, invoked by the authio_management-api when it queues a live-credentials import. The CLI:

  1. Asks the management-api for the job + the credential envelope (over HTTP, using AUTHIO_MIGRATE_WORKER_TOKEN as bearer).
  2. Decrypts the envelope using AUTHIO_IMPORT_CREDS_KEY.
  3. Runs the per-provider PullLive.
  4. Pipes the resulting plan through PlanRunner against the Authio management-API (using the same bearer).
  5. PATCHes progress / POSTs finish back to /v1/migrate/jobs/... so the dashboard's polling UI sees live updates.

`migrate plan --provider <p> --live-token <t> [--auth0-domain …]` bypasses the DB and prints the plan as JSON — useful for previews and for the e2e harness.

func Orgs

func Orgs(args []string) error

Orgs handles `authio orgs <subcommand>`.

authio orgs create --name Acme [--slug acme] [--domain acme.com] [--profile name] [--json]

func Redirects

func Redirects(args []string) error

Redirects handles `authio redirects <subcommand>`.

func Users

func Users(args []string) error

Users dispatches `authio users <subcommand>`.

func UsersImport

func UsersImport(args []string) error

UsersImport runs `authio users import`.

func Webhook

func Webhook(args []string) error

Webhook handles `authio webhook listen <url>` — currently a placeholder that documents the recommended `ngrok http` workflow until we wire a first-party tunnel.

func Webhooks

func Webhooks(args []string) error

Webhooks handles `authio webhooks <subcommand>`.

authio webhooks create --url https://… [--events a,b] [--description d] [--org org_…] [--profile name] [--json]

Distinct from the legacy `authio webhook listen` (ngrok helper).

func Whoami

func Whoami(args []string) error

Whoami resolves the active profile, calls GET /v1/projects/me, and prints who the current key authenticates as: tenant, environment, key family (test/live) and the management API it targets.

authio whoami [--profile name] [--json]

Types

type Auth0User

type Auth0User struct {
	UserID        string `json:"user_id"`
	Email         string `json:"email"`
	EmailVerified bool   `json:"email_verified"`
	Name          string `json:"name"`
	Nickname      string `json:"nickname"`
}

type CredEnvelopeRaw

type CredEnvelopeRaw struct {
	KekID         string `json:"kek_id"`
	Alg           string `json:"alg"`
	Nonce         string `json:"nonce"`
	Tag           string `json:"tag"`
	Ciphertext    string `json:"ciphertext"`
	DekNonce      string `json:"dek_nonce"`
	DekTag        string `json:"dek_tag"`
	DekCiphertext string `json:"dek_ciphertext"`
}

CredEnvelopeRaw mirrors the JSONB envelope the management-api stores. See authio_management-api/src/import_credentials.ts for the canonical shape; both sides MUST stay in lockstep.

func SealForTest

func SealForTest(projectID, masterKey string, creds LiveCredentials) (CredEnvelopeRaw, error)

Exported test helpers — used by the e2e tests in authio_e2e-tests and the in-package tests in import_live_*_test.go.

SealForTest produces an envelope identical to what the management-api would write for `creds`. Tests use this to fake an import_credentials row without booting Postgres.

type Cursor

type Cursor struct {
	Provider      string        `json:"provider"`
	File          string        `json:"file"`
	FileSize      int64         `json:"file_size"`
	LastIndex     int           `json:"last_index"`
	Completed     bool          `json:"completed"`
	StartedAt     time.Time     `json:"started_at"`
	LastUpdatedAt time.Time     `json:"last_updated_at"`
	Summary       CursorSummary `json:"summary"`
}

Cursor is what gets written next to the input file as a resume marker. We key resume safety on (provider, file, fileSize); a size mismatch indicates the source file changed and we refuse to continue without --force.

type CursorSummary

type CursorSummary struct {
	Created int `json:"created"`
	Existed int `json:"existed"`
	Skipped int `json:"skipped"`
	Errored int `json:"errored"`
}

CursorSummary tallies what the runner did across this import.

type IdentityRecord

type IdentityRecord struct {
	UserExternalID string         `json:"user_external_id"`
	Kind           string         `json:"kind"`
	Subject        string         `json:"subject"`
	Metadata       map[string]any `json:"metadata,omitempty"`
}

type ImportPlan

type ImportPlan struct {
	Provider        string                `json:"provider"`
	Users           []UserRecord          `json:"users"`
	Orgs            []OrgRecord           `json:"orgs"`
	Memberships     []MembershipRecord    `json:"memberships"`
	Identities      []IdentityRecord      `json:"identities"`
	SsoConnections  []SsoConnectionRecord `json:"sso_connections"`
	ScimDirectories []ScimDirectoryRecord `json:"scim_directories"`
	Warnings        []string              `json:"warnings"`
	Stats           PlanStats             `json:"stats"`
}

ImportPlan is the canonical, provider-neutral shape that every plan parser emits. The plan-runner consumes it to drive idempotent writes against the Authio management-api.

Idempotency contract: every record carries an ExternalID of the form "<provider>:<source_id>". Re-running the importer with the same source export is safe — users are upserted on (project_id, email), orgs on (project_id, slug), memberships on (project_id, user_id, org_id), scim directories on (project_id, organization_id).

type ImportRunner

type ImportRunner struct {
	Parser    Parser
	File      string
	APIKey    string
	APIURL    string
	DryRun    bool
	Force     bool
	RateLimit int          // requests per second; defaults to 50
	HTTP      *http.Client // injectable for tests
	Out       io.Writer    // pretty-prints progress; defaults to os.Stdout
	NowFunc   func() time.Time
	SleepFunc func(time.Duration)
	UserAgent string
}

ImportRunner drives the standardized import flow: stream from a Parser, dedupe by cursor, rate-limit, POST /v1/users, count summaries.

func (*ImportRunner) Run

func (r *ImportRunner) Run(ctx context.Context) (*Cursor, error)

Run reads the file, validates the cursor, streams through the parser, and dispatches each user. Returns the final cursor + summary.

type LiveCredentials

type LiveCredentials struct {
	// Auth0
	Domain string `json:"domain,omitempty"`
	Token  string `json:"token,omitempty"`

	// Clerk
	SecretKey string `json:"secret_key,omitempty"`

	// WorkOS
	APIKey string `json:"api_key,omitempty"`

	// Stytch (also reused by Descope for ProjectID)
	ProjectID     string `json:"project_id,omitempty"`
	ProjectSecret string `json:"project_secret,omitempty"`

	// Descope
	MgmtKey string `json:"mgmt_key,omitempty"`

	// Cognito
	AccessKeyID     string `json:"access_key_id,omitempty"`
	SecretAccessKey string `json:"secret_access_key,omitempty"`
	Region          string `json:"region,omitempty"`
	UserPoolID      string `json:"user_pool_id,omitempty"`

	// Firebase
	ServiceAccountJSON string `json:"service_account_json,omitempty"`

	// Supabase
	PAT        string `json:"pat,omitempty"`
	ProjectRef string `json:"project_ref,omitempty"`
}

LiveCredentials is the union of every provider's admin-API credential shape. Most fields are nil for any single provider; the registered PullLive picks the ones it needs.

AUTHIO_REDACT — every field on this struct is a high-risk secret. Never log, never include in error messages verbatim, never write to stdout. The management-api ships these to the CLI via decrypted import_credentials.envelope and the CLI tosses them when the job terminates.

type LiveOptions

type LiveOptions struct {
	// BaseURLOverride lets tests redirect provider API calls at a
	// localhost httptest.Server. Empty means use the real provider host.
	BaseURLOverride string
	// MaxPages caps pagination so a runaway export doesn't dominate a
	// dev machine. 0 = unbounded.
	MaxPages int
	// RateLimitPerSec caps requests/sec. 0 falls back to providerDefault.
	RateLimitPerSec float64
	// HTTPClient lets the caller inject a custom transport (used by the
	// migrate-run command for retries + telemetry).
	HTTPClient *http.Client
	// ProgressFn, if set, is called every batch with (kind, count). It
	// powers the import_jobs.progress JSONB updates.
	ProgressFn func(kind string, completed int)
	// TargetOrganizationID pins memberships to an existing Authio org.
	TargetOrganizationID string
	// SourceWorkOSOrganizationID filters WorkOS live pulls to one org.
	SourceWorkOSOrganizationID string
}

LiveOptions tweaks the puller's behavior (max page size, rate limit, base URL override for tests).

type LivePuller

type LivePuller interface {
	Name() string
	PullLive(ctx context.Context, creds LiveCredentials, opts LiveOptions) (*ImportPlan, error)
}

LivePuller is what each provider implements.

func LivePullerFor

func LivePullerFor(provider string) (LivePuller, error)

LivePullerFor returns the registered puller or a helpful error.

type MembershipRecord

type MembershipRecord struct {
	UserExternalID string `json:"user_external_id"`
	OrgExternalID  string `json:"org_external_id"`
	Role           string `json:"role"`
	Status         string `json:"status"`
}

type OrgRecord

type OrgRecord struct {
	ExternalID string `json:"external_id"`
	Name       string `json:"name"`
	Slug       string `json:"slug"`
	Domain     string `json:"domain,omitempty"`
}

type Parser

type Parser interface {
	// Name is the provider key used in cursor + UA.
	Name() string
	// Help returns a short string describing the expected file format.
	// Surfaced via `authio import <provider> --help`.
	Help() string
	// Parse reads from r and calls emit for every record. Records that
	// should be skipped (disabled, banned, missing email, etc.) are
	// emitted with Email == "" so the runner can count them as skipped
	// without breaking the cursor's count-of-records-seen invariant.
	Parse(ctx context.Context, r io.Reader, emit func(SourceUser) error) error
}

Parser streams users from a provider-specific export format. The runner passes a fresh `emit` callback each run; returning a non-nil error from emit (e.g. context cancellation) aborts parsing cleanly.

type PlanOptions

type PlanOptions struct {
	// OrgsTablePath, when non-empty, is a path to a JSON file describing
	// an org graph for providers (Supabase, Firebase) that don't have a
	// first-class org concept. Format:
	//
	//   {
	//     "orgs":[{"external_id":"...", "name":"...", "slug":"...",
	//              "domain":"...", "members":[{"email":"...","role":"admin"}]}]
	//   }
	OrgsTablePath string
	// MergeDuplicateEmails enables WorkOS-style merge: source users with
	// the same email across multiple source orgs collapse into one Authio
	// user with N memberships. Default true.
	MergeDuplicateEmails bool
	// DefaultOrgName is used when a provider has zero orgs in its export
	// (Supabase without --orgs-table, Firebase, Consumer Stytch). All
	// users land under this org as members.
	DefaultOrgName string
	// TargetOrganizationID pins every imported membership (and SSO/SCIM)
	// to this existing Authio org. WorkOS org rows are not created.
	TargetOrganizationID string
	// SourceWorkOSOrganizationID limits the import to one WorkOS org's
	// users/memberships when set (optional).
	SourceWorkOSOrganizationID string
}

PlanOptions modifies how a parser builds the plan.

type PlanParser

type PlanParser interface {
	Name() string
	Help() string
	ParsePlan(ctx context.Context, r io.Reader, opts PlanOptions) (*ImportPlan, error)
}

PlanParser is what each "production" importer implements. It reads the export file once and returns a fully-built ImportPlan.

Parsers do not hit the network and never see a project_id / API key — they only translate the source export. The PlanRunner does the writes.

func PlanParserFor

func PlanParserFor(provider string) (PlanParser, error)

PlanParserFor returns the registered PlanParser for the given provider key, or nil + a helpful error.

type PlanRunner

type PlanRunner struct {
	APIURL   string
	APIKey   string
	HTTP     *http.Client
	Out      io.Writer // progress sink; defaults to os.Stdout
	EmitJSON bool      // emit NDJSON events instead of human lines
	DryRun   bool
	// ExtraHeaders are merged into every outbound request. Used by the
	// migrate worker to send X-Authio-Worker + X-Authio-Project-Id so
	// the management-api accepts the call without a real API key.
	ExtraHeaders map[string]string
	// TargetOrganizationID pins memberships to an existing Authio org.
	TargetOrganizationID string
	// RecordErrors collects per-record failures for the dashboard.
	RecordErrors []RecordError
}

PlanRunner executes a parsed ImportPlan against the Authio management- API. It is idempotent on every record: users on (project_id, email), orgs on (project_id, slug), memberships on (project_id, user_id, org_id), scim directories on (project_id, organization_id).

The runner emits a structured per-record progress event when EmitJSON is true (used by the dashboard wizard). When false, it prints human- readable lines suitable for terminal use.

func (*PlanRunner) Run

func (p *PlanRunner) Run(ctx context.Context, plan *ImportPlan) (PlanStats, error)

Run applies the plan and returns the final stats. The plan's Stats fields are updated in place.

type PlanStats

type PlanStats struct {
	SourceUsers            int `json:"source_users"`
	MergedUsers            int `json:"merged_users"`
	UsersCreated           int `json:"users_created"`
	UsersExisted           int `json:"users_existed"`
	OrgsCreated            int `json:"orgs_created"`
	OrgsExisted            int `json:"orgs_existed"`
	MembershipsCreated     int `json:"memberships_created"`
	IdentitiesCreated      int `json:"identities_created"`
	SsoConnectionsCreated  int `json:"sso_connections_created"`
	ScimDirectoriesCreated int `json:"scim_directories_created"`
	Errored                int `json:"errored"`
	Warnings               int `json:"warnings"`
}

type RecordError

type RecordError struct {
	Kind string `json:"kind"`
	Key  string `json:"key"`
	Msg  string `json:"msg"`
}

RecordError is one failed/skipped import row surfaced to the wizard.

type ScimDirectoryRecord

type ScimDirectoryRecord struct {
	OrgExternalID string `json:"org_external_id"`
	Name          string `json:"name"`
}

type SourceUser

type SourceUser struct {
	Email          string
	Name           string
	EmailVerified  bool
	SourceID       string
	SourceProvider string
	Metadata       map[string]any
}

SourceUser is the canonical shape every parser emits. The runner doesn't care which provider it came from — it just calls POST /v1/users with (email, name, email_verified) and emits enrollment for created users.

type SsoConnectionRecord

type SsoConnectionRecord struct {
	OrgExternalID string         `json:"org_external_id"`
	Name          string         `json:"name"`
	Kind          string         `json:"kind"`
	Metadata      map[string]any `json:"metadata,omitempty"`
}

type UserRecord

type UserRecord struct {
	ExternalID            string         `json:"external_id"`
	Email                 string         `json:"email"`
	EmailVerified         bool           `json:"email_verified"`
	Name                  string         `json:"name,omitempty"`
	AvatarURL             string         `json:"avatar_url,omitempty"`
	Metadata              map[string]any `json:"metadata,omitempty"`
	MigrationPendingEmail bool           `json:"migration_pending_email"`
	MfaEnrolled           bool           `json:"mfa_enrolled,omitempty"`
	// SourceExternalIDs is the list of every source-system ID that
	// merged into this Authio user. For most providers it is a 1-element
	// list. WorkOS produces N-element lists when the same email appears
	// in multiple WorkOS orgs (the "marquee" merge moment).
	SourceExternalIDs []string `json:"source_external_ids,omitempty"`
}

UserRecord — a single user, deduped by email within the plan.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL