Documentation
¶
Index ¶
- func ParseInviteCode(raw string) (code, origin string, err error)
- func ValidateAPIOrigin(raw string) (string, error)
- type API
- type CredentialStore
- type Options
- type ResetOutcome
- type Result
- type Service
- func (s Service) Create(ctx context.Context, options Options) (Result, error)
- func (s Service) CreateAdditional(ctx context.Context, options Options, deviceID, token string) (Result, error)
- func (s Service) CreateOnNewBackend(ctx context.Context, options Options) (Result, error)
- func (s Service) CredentialState(ctx context.Context, configRoot string) (hosted.CredentialStatus, config.Config, error)
- func (s Service) Join(ctx context.Context, options Options, joinCode string) (Result, error)
- func (s Service) JoinAdditional(ctx context.Context, options Options, deviceID, token, joinCode string) (Result, error)
- func (s Service) JoinOnNewBackend(ctx context.Context, options Options, joinCode string) (Result, error)
- func (s Service) Promote(ctx context.Context, options Options, projectID string) (Result, error)
- func (s Service) Reset(ctx context.Context, configRoot string, force bool) (ResetOutcome, error)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ParseInviteCode ¶
ParseInviteCode extracts the invite code, and the backend origin it names, from whatever form the member pasted.
Three forms are accepted: the bare code, the desktop deep link, and the join-page URL whose fragment carries the code (a fragment never reaches server logs). Only the https form knows where the Project lives, so only it returns an origin; the other two return an empty one, meaning "the backend the caller selected". This is what lets a member on a purely local profile paste a link and join a team Project on a server this Mac has never used, without being asked which server that was.
func ValidateAPIOrigin ¶
ValidateAPIOrigin canonicalizes a backend origin a member supplied.
The rule is exactly hosted.New's, because that is the client every later call goes through: an HTTPS origin, or loopback HTTP for a backend running on this Mac, with no path, query, fragment, or userinfo. Checking it here means the member is told at the field rather than by the first request that fails.
Types ¶
type API ¶
type API interface {
CreateProject(context.Context, hosted.NewProject) (hosted.Project, error)
CreateInvite(context.Context, string, int, int) (hosted.Invite, error)
Enroll(context.Context, string, string, string, string, string) (hosted.Enrollment, error)
JoinProject(context.Context, string, string, string, string, string) (hosted.Membership, error)
Bootstrap(context.Context) (hosted.Bootstrap, error)
CreateDashboardTicket(context.Context, string) (hosted.DashboardTicket, error)
RevokeDevice(context.Context, string) error
}
type CredentialStore ¶
type Options ¶
type Options struct {
ConfigRoot, RepositoryRoot, ProjectLabel, DeviceLabel, AppVersion string
// DisplayName is optional; empty means the member has not chosen one yet.
DisplayName string
// SkipInvite creates the Project without minting a one-use invite. A local
// Project has no second member to hand one to, and offering a code is how a
// screen implies that inviting somebody is the next step (ADR-072).
SkipInvite bool
// ProjectID reuses an identifier the caller already holds rather than being
// issued a new one. Empty - which is every path today - means the server
// issues one, and that is what an ordinary creation wants. It is set when a
// Project is being re-created on a second backend and has to stay the same
// Project: its identifier is salted into every repository fingerprint and
// stamped on every event envelope the device has queued, so changing it
// would orphan all of that evidence from the Project it describes.
ProjectID string
}
type ResetOutcome ¶
type ResetOutcome struct {
Status hosted.CredentialStatus
BackendID string
APIBaseURL string
DeviceID string
ClearedWorkspaces int
CredentialDeleted bool
}
ResetOutcome describes what a reset changed on this device.
type Service ¶
type Service struct {
Backend config.Backend
Client func(token string) (API, error)
Creds CredentialStore
Register func(context.Context, string, string, string, config.Workspace) error
// Rebind moves an existing Project onto this flow's backend. Like Register
// it is a seam: the desktop points it at the running service so a move takes
// effect without a restart, and it falls back to editing the profile.
Rebind func(ctx context.Context, configRoot, projectID, apiBaseURL, deviceID string) error
}
Service is one onboarding flow against one backend. The backend travels with the flow rather than with the profile: a Mac holds a device identity per backend (ADR-069, ADR-074), so "which server" and "which credential" are one question and are answered together.
func (Service) CreateAdditional ¶
func (s Service) CreateAdditional(ctx context.Context, options Options, deviceID, token string) (Result, error)
CreateAdditional creates another Project for a device that is already enrolled in the local profile. The existing credential is deliberately reused: one per-user service has one device identity across its Projects. Unlike first enrollment, a local registration failure must never revoke the shared device and strand its existing Projects.
func (Service) CreateOnNewBackend ¶
CreateOnNewBackend creates a Project on this flow's backend, whether or not the profile has used that backend before.
A backend the profile has never seen gets a device identity minted for it; one it already has an identity for reuses that identity, because a second credential for the same server would strand the Projects the first one holds. Both cases are one member action - "add a Project on this server" - so the choice is made here rather than in each caller.
func (Service) CredentialState ¶
func (s Service) CredentialState(ctx context.Context, configRoot string) (hosted.CredentialStatus, config.Config, error)
CredentialState reports whether the credential stored for this flow's backend is still accepted by it. A backend this profile has no device identity for is reported as OK because there is nothing that could have been rejected.
It is per backend, not per profile: one revoked team Project says nothing about the local Project beside it.
func (Service) JoinAdditional ¶
func (s Service) JoinAdditional(ctx context.Context, options Options, deviceID, token, joinCode string) (Result, error)
JoinAdditional redeems an invite for a device that is already enrolled in the local profile, adding a second Project without a second device identity.
It is the join counterpart of CreateAdditional and shares its rule: the existing credential is reused, and a local registration failure must never revoke the shared device and strand the Projects this Mac already has. That is why it does not route through Join, whose rollback revokes the device it just created - here that device is the one holding every other Project.
func (Service) JoinOnNewBackend ¶
func (s Service) JoinOnNewBackend(ctx context.Context, options Options, joinCode string) (Result, error)
JoinOnNewBackend redeems an invite on this flow's backend, minting a device identity for a backend the profile has never used and reusing the one it has otherwise. Joining a friend's team Project from a purely local profile is the first case, and it is the common one.
func (Service) Promote ¶ added in v0.1.2
Promote moves a Project that lives on one backend onto this flow's backend, keeping its identifier, its repositories, and its agent bindings.
This is how a local Project becomes a shared one. Nothing about the repository changes and nothing is re-enrolled: the Project is re-created on the new backend under the identifier it already has, this device takes an identity there, and the profile is repointed. The coordination record does not travel - the new backend learns the current state from the next scan - which is what keeps this a change of address rather than a migration.
It is written to be safe to run twice. Every step short of the rebind is either idempotent or recoverable, because the failure that matters here is the one that happens after the new Project exists.
func (Service) Reset ¶
Reset forgets this device's identity on one backend so the member can enroll against it again. It deletes that backend's stored credential and removes the backend, its Projects, and the repositories registered to them. Every other backend on the profile is untouched, which is the whole point of binding Projects to backends: a revoked team Project must not take the local Project down with it.
It refuses unless that backend has actually rejected the credential. Being offline is not being locked out, and erasing a working enrollment cannot be undone. force exists for support and headless recovery, where the operator has already established that the enrollment is dead.