Auto-derive session cookie domain; AI prompt page = formula builder hub
Session (no env needed): - SESSION_COOKIE_DOMAIN env removed. Config.SessionCookieParentDomain() derives the cookie Domain from WEB_ORIGIN + PUBLIC_API_URL, which the API already requires: sibling hosts of one parent (descrybe.io + api.descrybe.io) share the parent domain so SvelteKit SSR (/admin gate, user switching) receives the session cookie; localhost, IPs, same-host, and unrelated hosts stay host-only. Deploying the new build is the whole fix — nothing to configure. AI generation prompt page: - Each section now embeds its formula editor next to the per-language prompt instructions: Title = full title formula builder (preview, elements, separator, variable selector, custom variables), Description = description formula sections editor (type + instructions + export id, drag reorder), Meta = meta title / meta description formula fields. One Save writes categories.prompt + title_template + description_template together; Assign copies all three to the selected categories. - New $lib/categories/formula-variables.ts loads every usable field for the builder: custom variables (/api/variables), company attributes (/api/attributes — attribute_key, name, unit, example), and standard fields (/api/standard-fields). Used by both the prompt page and the title-formula page (which previously ignored attributes). Verified locally: svelte-check clean for changed files, unit tests pass, and the full save contract exercised over HTTP as the page does it (login → load variables/attributes/standard-fields → PATCH prompt + title-formula + description-formula → round-trip read), then the test category restored via repair-category-prompts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,7 +36,6 @@ type Config struct {
|
||||
// (e.g. redis/postgres) so boot can warn that memory was forced.
|
||||
RateLimitBackendRequested string
|
||||
SessionCookieName string
|
||||
SessionCookieDomain string
|
||||
SessionSecure bool
|
||||
CSRFCookieName string
|
||||
PublicAPIURL string
|
||||
@@ -142,10 +141,6 @@ func Load() (Config, error) {
|
||||
RateLimitMultiReplica: getenvBool("RATE_LIMIT_MULTI_REPLICA", false),
|
||||
RateLimitBackend: "memory",
|
||||
SessionCookieName: getenv("SESSION_COOKIE_NAME", "descrybe_session"),
|
||||
// Empty = host-only cookie (localhost). Set to the parent domain
|
||||
// (e.g. descrybe.io) when web + api run on sibling subdomains so
|
||||
// SvelteKit SSR (descrybe.io) receives the session cookie too.
|
||||
SessionCookieDomain: getenv("SESSION_COOKIE_DOMAIN", ""),
|
||||
// Default Secure=true when APP_ENV is production|prod so cookies are HTTPS-only
|
||||
// even if SESSION_SECURE is unset; explicit false still fails closed in validate.
|
||||
SessionSecure: getenvBool("SESSION_SECURE", isProductionEnvValue(appEnv)),
|
||||
@@ -251,6 +246,69 @@ func (c Config) InsecureLocalProductionActive() bool {
|
||||
return c.IsProduction() && c.AllowInsecureLocalProduction && isLoopbackWebOriginHost(c.WebOrigin)
|
||||
}
|
||||
|
||||
// SessionCookieParentDomain derives the session cookie Domain attribute from
|
||||
// WEB_ORIGIN and PUBLIC_API_URL — no extra env needed. When web and API run on
|
||||
// sibling hosts of one parent domain (descrybe.io + api.descrybe.io), the
|
||||
// shared parent is returned so the browser also sends the session cookie to
|
||||
// the web host; SvelteKit SSR gates (/admin) forward it to /api/auth/me and
|
||||
// would otherwise always see 401. Same hostname (localhost dev, single-host
|
||||
// deploys), IPs, or unrelated hosts → "" (host-only cookie, old behavior).
|
||||
func (c Config) SessionCookieParentDomain() string {
|
||||
web := hostnameOfURL(c.WebOrigin)
|
||||
api := hostnameOfURL(c.PublicAPIURL)
|
||||
if web == "" || api == "" || web == api {
|
||||
return ""
|
||||
}
|
||||
if net.ParseIP(web) != nil || net.ParseIP(api) != nil ||
|
||||
!strings.Contains(web, ".") || !strings.Contains(api, ".") {
|
||||
return ""
|
||||
}
|
||||
// Direct parent/child: one host is the other's registrable parent.
|
||||
if strings.HasSuffix(api, "."+web) {
|
||||
return web
|
||||
}
|
||||
if strings.HasSuffix(web, "."+api) {
|
||||
return api
|
||||
}
|
||||
// Sibling subdomains (app.x.y + api.x.y): share the deepest common suffix,
|
||||
// but only when it has at least two labels (never a bare TLD).
|
||||
if suffix := commonDotSuffix(web, api); strings.Count(suffix, ".") >= 1 {
|
||||
return suffix
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func hostnameOfURL(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil || u.Hostname() == "" {
|
||||
return ""
|
||||
}
|
||||
return strings.ToLower(u.Hostname())
|
||||
}
|
||||
|
||||
// commonDotSuffix returns the longest label-aligned common suffix of two hostnames
|
||||
// ("app.descrybe.io", "api.descrybe.io" → "descrybe.io"); "" when nothing matches.
|
||||
func commonDotSuffix(a, b string) string {
|
||||
la := strings.Split(a, ".")
|
||||
lb := strings.Split(b, ".")
|
||||
n := 0
|
||||
for n < len(la) && n < len(lb) {
|
||||
if la[len(la)-1-n] != lb[len(lb)-1-n] {
|
||||
break
|
||||
}
|
||||
n++
|
||||
}
|
||||
// Never return one of the full hostnames itself (that is the parent/child case).
|
||||
if n == 0 || n == len(la) || n == len(lb) {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(la[len(la)-n:], ".")
|
||||
}
|
||||
|
||||
// ShouldWarnRateLimits reports whether operators opted into multi-replica rate-limit
|
||||
// awareness or requested an unsupported shared backend.
|
||||
func (c Config) ShouldWarnRateLimits() bool {
|
||||
|
||||
Reference in New Issue
Block a user