mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-30 20:57:01 +00:00
1. Multiple registry domains + per-user domain preference
The biggest feature. The appview can serve several registry domains (e.g. buoy.cr, atcr.io),
and users can now pick which one shows up in their pull/push commands.
- Lexicon/record: adds registryDomain (and documents ociClient)
to the sailor profile (lexicons/.../profile.json, pkg/atproto/lexicon.go).
- DB: new registry_domain column on users (schema.sql + migration 0027),
with GetUserByDID/Handle reads, UpdateUserRegistryDomain writer,
and Jetstream caching it on profile updates (writes unconditionally so clearing propagates).
- UI/handlers: new UpdateRegistryDomainHandler + /api/profile/registry-domain route,
a <select> in the user settings panel (only shown when >1 domain configured), and resolveRegistryURL()
which falls back to the primary domain if the user's pref is stale/removed. Tests added for all of it.
2. default_hold_did removed → first managed_holds entry is the default
Consolidates two overlapping config fields into one. ServerConfig.DefaultHoldDID is gone;
PrimaryHoldDID() now returns managed_holds[0]. managed_holds is now REQUIRED.
Updated in config, validation, server wiring, test harness, example YAML, and the deploy template.
3. Admin long-running operations → generic background-job framework
New pkg/hold/admin/jobs.go introduces a reusable startJob/jobRegistry pattern
(a detached context.Background() job + a /admin/api/jobs/{key}/status polling endpoint).
This replaces the bespoke scan-backfill goroutine state machine, and now also wraps crew tier remap and crew import
all three previously looped synchronously on the request context and got 504'd/cancelled mid-run by the reverse proxy.
Forms switched from POST-redirect to htmx fragments (job_progress.html, job_result.html, crew_import_results.html)
the old crew_import_results.html page and scan_backfill_progress.html partial were deleted.
This is also captured as a new rule in CLAUDE.md.
4. Cascade-delete manifest on last-tag deletion
DeleteTagHandler now, after removing the last tag pointing to a digest, cascade-deletes the manifest itself
(PDS + DB + hold blob purge) — but only if it's not a child of a manifest list (multi-arch parent).
New GetTagDigest and ShouldCascadeDeleteManifest queries back it, plus cascade_delete_test.go.
Also switches tag rkey computation to the atproto.RepositoryTagToRKey helper.
5. Billing simplification
Drops the OwnerBadge config option (hold-owner supporter badge).
The user-profile template no longer special-cases an "owner" badge value (only "Captain").
Example tiers renamed to the nautical scheme (deckhand/bosun/quartermaster).
6. Build/deploy: go generate always runs via Make
make generate is now a phony target that always runs go generate ./... (regenerating cbor_gen, icon sprites, etc.),
and build-trixie depends on it. The deploy tooling (provision.go/update.go)
drops its own runGenerate calls since the Makefile handles it.
7. New cmd/firehose-tap tool (untracked)
A standalone CLI that subscribes to a com.atproto.sync.subscribeRepos endpoint and pretty-prints events,
with emphasis on Sync 1.1 compliance fields (per-op prev CIDs, commit prevData) and a --validate CI mode.
Fits with the recent "more sync1.1 compliant" commit.
84 lines
3.7 KiB
Go
84 lines
3.7 KiB
Go
package billing
|
|
|
|
// Config holds appview billing/Stripe configuration.
|
|
// Parsed from the appview config YAML's billing section.
|
|
type Config struct {
|
|
// Stripe secret key (sk_test_... or sk_live_...).
|
|
// Can also be set via STRIPE_SECRET_KEY env var (takes precedence over config).
|
|
// Billing is enabled automatically when this key is set (requires -tags billing build).
|
|
StripeSecretKey string `yaml:"stripe_secret_key" comment:"Stripe secret key. Can also be set via STRIPE_SECRET_KEY env var (takes precedence). Billing is enabled automatically when set."`
|
|
|
|
// Stripe webhook signing secret (whsec_...).
|
|
// Can also be set via STRIPE_WEBHOOK_SECRET env var (takes precedence over config).
|
|
WebhookSecret string `yaml:"webhook_secret" comment:"Stripe webhook signing secret. Can also be set via STRIPE_WEBHOOK_SECRET env var (takes precedence)."`
|
|
|
|
// Currency code for Stripe checkout (e.g. "usd").
|
|
Currency string `yaml:"currency" comment:"ISO 4217 currency code (e.g. \"usd\")."`
|
|
|
|
// URL to redirect after successful checkout. {base_url} is replaced at runtime.
|
|
SuccessURL string `yaml:"success_url" comment:"Redirect URL after successful checkout. Use {base_url} placeholder."`
|
|
|
|
// URL to redirect after cancelled checkout. {base_url} is replaced at runtime.
|
|
CancelURL string `yaml:"cancel_url" comment:"Redirect URL after cancelled checkout. Use {base_url} placeholder."`
|
|
|
|
// Subscription tiers with Stripe price IDs.
|
|
Tiers []BillingTierConfig `yaml:"tiers" comment:"Subscription tiers ordered by rank (lowest to highest)."`
|
|
}
|
|
|
|
// BillingTierConfig represents a single tier with optional Stripe pricing.
|
|
type BillingTierConfig struct {
|
|
// Tier name (matches hold quota tier names for rank mapping).
|
|
Name string `yaml:"name" comment:"Tier name. Position in list determines rank (0-based)."`
|
|
|
|
// Short description shown on the plan card.
|
|
Description string `yaml:"description,omitempty" comment:"Short description shown on the plan card."`
|
|
|
|
// List of features included in this tier (rendered as bullet points).
|
|
Features []string `yaml:"features,omitempty" comment:"List of features included in this tier."`
|
|
|
|
// Stripe price ID for monthly billing. Empty = free tier.
|
|
StripePriceMonthly string `yaml:"stripe_price_monthly,omitempty" comment:"Stripe price ID for monthly billing. Empty = free tier."`
|
|
|
|
// Stripe price ID for yearly billing.
|
|
StripePriceYearly string `yaml:"stripe_price_yearly,omitempty" comment:"Stripe price ID for yearly billing."`
|
|
|
|
// Maximum number of webhooks for this tier (-1 = unlimited).
|
|
MaxWebhooks int `yaml:"max_webhooks" comment:"Maximum webhooks for this tier (-1 = unlimited)."`
|
|
|
|
// Whether all webhook trigger types are available (not just first-scan).
|
|
WebhookAllTriggers bool `yaml:"webhook_all_triggers" comment:"Allow all webhook trigger types (not just first-scan)."`
|
|
|
|
// Whether AI Image Advisor is available for this tier.
|
|
AIAdvisor bool `yaml:"ai_advisor" comment:"Enable AI Image Advisor for this tier."`
|
|
|
|
// Whether this tier earns a supporter badge on user profiles.
|
|
SupporterBadge bool `yaml:"supporter_badge" comment:"Show supporter badge on user profiles for subscribers at this tier."`
|
|
}
|
|
|
|
// GetTierByPriceID finds the tier that contains the given Stripe price ID.
|
|
// Returns the tier name and rank, or empty string and -1 if not found.
|
|
func (c *Config) GetTierByPriceID(priceID string) (string, int) {
|
|
if c == nil || priceID == "" {
|
|
return "", -1
|
|
}
|
|
for i, tier := range c.Tiers {
|
|
if tier.StripePriceMonthly == priceID || tier.StripePriceYearly == priceID {
|
|
return tier.Name, i
|
|
}
|
|
}
|
|
return "", -1
|
|
}
|
|
|
|
// TierRank returns the 0-based rank of a tier by name, or -1 if not found.
|
|
func (c *Config) TierRank(name string) int {
|
|
if c == nil {
|
|
return -1
|
|
}
|
|
for i, tier := range c.Tiers {
|
|
if tier.Name == name {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|