@columnus/shared 0.1.63 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,7 +33,7 @@ function hostnameFromHostHeader(host) {
33
33
  }
34
34
  function tenantSlugFromHost(host) {
35
35
  const hostname = hostnameFromHostHeader(host);
36
- const match = hostname.match(/^([a-z0-9-]+)\.columnus\.(app|localhost)$/);
36
+ const match = hostname.match(/^([a-z0-9-]+)\.columnus\.(app|dev|localhost)$/);
37
37
  if (!match) return null;
38
38
  const slug = match[1];
39
39
  if (!slug || !isValidTenantSlug(slug)) return null;
@@ -45,7 +45,7 @@ function isValidPreviewTenantSlug(slug) {
45
45
  }
46
46
  function previewSlugFromHost(host) {
47
47
  const hostname = hostnameFromHostHeader(host);
48
- const match = hostname.match(/^(preview-[a-z0-9-]+)\.columnus\.(app|localhost)$/);
48
+ const match = hostname.match(/^(preview-[a-z0-9-]+)\.columnus\.(app|dev|localhost)$/);
49
49
  if (!match) return null;
50
50
  const slug = match[1];
51
51
  if (!slug || !isValidPreviewTenantSlug(slug)) return null;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tenantHosts.ts","../src/convexSiteUrl.ts","../src/studioBoot.ts"],"names":[],"mappings":"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","file":"studioBoot.js","sourcesContent":["/** Reserved first labels under `*.columnus.app` / `*.columnus.localhost` — not tenants. */\nexport const RESERVED_TENANT_SLUGS = [\"manage\", \"deploy\", \"www\"] as const;\n\nconst RESERVED_SLUG_SET = new Set<string>(RESERVED_TENANT_SLUGS);\n\n/** Lowercase slug: letters/digits with optional hyphen segments (e.g. `app`, `acme-prod`). */\nexport const TENANT_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nexport function isReservedTenantSlug(slug: string): boolean {\n const normalized = slug.trim().toLowerCase();\n if (!normalized) return true;\n if (RESERVED_SLUG_SET.has(normalized)) return true;\n if (normalized.startsWith(\"preview-\")) return true;\n return false;\n}\n\nexport function isValidTenantSlug(slug: string): boolean {\n const trimmed = slug.trim();\n // Require already-lowercase slugs (hosts are normalized before parse).\n if (!TENANT_SLUG_RE.test(trimmed)) return false;\n return !isReservedTenantSlug(trimmed);\n}\n\n/** Strip optional port from a Host header or `window.location.host` value. */\nexport function hostnameFromHostHeader(host: string): string {\n const trimmed = host.trim().toLowerCase();\n if (!trimmed) return \"\";\n // IPv6 bracket form is not used for tenant hosts; strip :port for hostname:port.\n if (trimmed.startsWith(\"[\")) {\n const end = trimmed.indexOf(\"]\");\n return end === -1 ? trimmed : trimmed.slice(1, end);\n }\n const colon = trimmed.lastIndexOf(\":\");\n if (colon > -1 && /^\\d+$/.test(trimmed.slice(colon + 1))) {\n return trimmed.slice(0, colon);\n }\n return trimmed;\n}\n\n/**\n * Extract tenant slug from a hostname.\n * Supports `{slug}.columnus.app` and `{slug}.columnus.localhost`.\n * Returns null for apex, reserved, invalid, or unrecognized hosts.\n */\nexport function tenantSlugFromHost(host: string): string | null {\n const hostname = hostnameFromHostHeader(host);\n const match = hostname.match(/^([a-z0-9-]+)\\.columnus\\.(app|localhost)$/);\n if (!match) return null;\n const slug = match[1];\n if (!slug || !isValidTenantSlug(slug)) return null;\n return slug;\n}\n\nexport type TenantPublicHostEnv = \"prod\" | \"local\";\n\n/** Public hostname for a tenant slug (no scheme). */\nexport function tenantPublicHost(slug: string, env: TenantPublicHostEnv = \"prod\"): string {\n const normalized = slug.trim().toLowerCase();\n return env === \"local\" ? `${normalized}.columnus.localhost` : `${normalized}.columnus.app`;\n}\n\n/** Preview tenant slug: `preview-<label>` (e.g. `preview-12`). */\nexport const PREVIEW_TENANT_SLUG_RE = /^preview-[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nexport function isValidPreviewTenantSlug(slug: string): boolean {\n return PREVIEW_TENANT_SLUG_RE.test(slug.trim().toLowerCase());\n}\n\n/**\n * Extract preview tenant slug from hostname (`preview-12.columnus.app`).\n * Returns null for non-preview or invalid hosts.\n */\nexport function previewSlugFromHost(host: string): string | null {\n const hostname = hostnameFromHostHeader(host);\n const match = hostname.match(/^(preview-[a-z0-9-]+)\\.columnus\\.(app|localhost)$/);\n if (!match) return null;\n const slug = match[1];\n if (!slug || !isValidPreviewTenantSlug(slug)) return null;\n return slug;\n}\n\n/**\n * Resolve either a standard tenant slug or a registered preview slug from a host.\n * Does not check Management DB — only host shape.\n */\nexport function anyTenantSlugFromHost(host: string): string | null {\n return tenantSlugFromHost(host) ?? previewSlugFromHost(host);\n}\n","/** Convex deployment cloud URL (.convex.cloud) → HTTP actions / auth issuer (.convex.site). */\nexport function convexCloudToSiteUrl(convexUrl: string): string {\n const trimmed = convexUrl.trim().replace(/\\/$/, \"\");\n if (trimmed.includes(\".convex.site\")) return trimmed;\n if (trimmed.includes(\".convex.cloud\")) {\n return trimmed.replace(/\\.convex\\.cloud\\b/, \".convex.site\");\n }\n return trimmed;\n}\n\nexport function convexSiteToCloudUrl(siteUrl: string): string {\n const trimmed = siteUrl.trim().replace(/\\/$/, \"\");\n if (trimmed.includes(\".convex.cloud\")) return trimmed;\n if (trimmed.includes(\".convex.site\")) {\n return trimmed.replace(/\\.convex\\.site\\b/, \".convex.cloud\");\n }\n return trimmed;\n}\n\ntype ProcessEnvLike = Record<string, string | undefined>;\n\nfunction trimOptional(value?: string): string | undefined {\n const trimmed = value?.trim();\n return trimmed || undefined;\n}\n\nfunction normalizeTrailingSlash(url: string): string {\n return url.trim().replace(/\\/$/, \"\");\n}\n\nfunction deriveSiteFromCloudUrl(cloud?: string): string | undefined {\n const trimmed = cloud?.trim();\n if (!trimmed) return undefined;\n if (trimmed.includes(\".convex.cloud\") || trimmed.includes(\".convex.site\")) {\n return convexCloudToSiteUrl(trimmed);\n }\n return undefined;\n}\n\n/** Docker runtime-base verify placeholder — must not be used for real BFF prepare. */\nexport function isPlaceholderManagementSiteUrl(url: string): boolean {\n try {\n const host = new URL(/:\\/\\//.test(url) ? url : `https://${url}`).hostname;\n return host === \"placeholder.convex.site\" || host.endsWith(\".placeholder.convex.site\");\n } catch {\n return /placeholder\\.convex\\.site/i.test(url);\n }\n}\n\n/**\n * Management `.convex.site` origin for BFF `prepare`.\n * Prefers explicit management site env vars, else derives from `VITE_MANAGEMENT_CONVEX_URL`.\n * Does not read `VITE_CONVEX_SITE_URL` — on web that is the tenant site, not Manage.\n */\nexport function resolveBffManagementSiteUrl(env: ProcessEnvLike = process.env): string | undefined {\n for (const key of [\"COLUMNUS_MANAGEMENT_SITE_URL\", \"MANAGEMENT_CONVEX_SITE_URL\"] as const) {\n const value = trimOptional(env[key]);\n if (value && !isPlaceholderManagementSiteUrl(value)) {\n return normalizeTrailingSlash(value);\n }\n }\n const managementCloud = trimOptional(env.VITE_MANAGEMENT_CONVEX_URL);\n if (managementCloud) return convexCloudToSiteUrl(managementCloud);\n return undefined;\n}\n\n/**\n * Runtime BFF management site: prefer image-baked URL over Coolify env.\n * Preview Coolify often injects prod `MANAGEMENT_CONVEX_SITE_URL`, which shadows the\n * ephemeral Manage preview bake and makes prepare return HTTP 404 (no routes on prod).\n */\nexport function resolveRuntimeManagementSiteUrl(options: {\n bakedSiteUrl?: string | null;\n env?: ProcessEnvLike;\n}): string | undefined {\n const bakedSite = trimOptional(options.bakedSiteUrl ?? undefined);\n if (bakedSite && !isPlaceholderManagementSiteUrl(bakedSite)) {\n return normalizeTrailingSlash(bakedSite);\n }\n return resolveBffManagementSiteUrl(options.env ?? process.env);\n}\n\nfunction convexDeploymentSlug(url: string): string | undefined {\n const match = url.match(/https:\\/\\/([^.]+)\\.convex\\.(?:cloud|site)/);\n return match?.[1];\n}\n\n/** Deployment name from a Convex cloud/site URL (`https://{name}.convex.cloud`). */\nexport function convexDeploymentSlugFromUrl(url: string): string | undefined {\n return convexDeploymentSlug(url);\n}\n\n/**\n * Best-effort `CONVEX_DEPLOYMENT` when only the cloud URL is known (e.g. after `columnus-api pull`).\n * Defaults to `dev:{slug}` — preview/prod deployments may need an explicit value in `.env.local`.\n */\nexport function deriveConvexDeploymentFromCloudUrl(cloudUrl: string): string | undefined {\n const slug = convexDeploymentSlug(cloudUrl);\n return slug ? `dev:${slug}` : undefined;\n}\n\nfunction explicitSiteMatchesCloud(explicit: string, cloud: string): boolean {\n const explicitSlug = convexDeploymentSlug(explicit);\n const cloudSlug = convexDeploymentSlug(cloud);\n if (!explicitSlug || !cloudSlug) return true;\n return explicitSlug === cloudSlug;\n}\n\nexport type ConvexSiteUrlEnv = {\n /** CONVEX_SITE_URL */\n convexSiteUrl?: string;\n /** VITE_CONVEX_SITE_URL */\n viteConvexSiteUrl?: string;\n /** CONVEX_URL */\n convexUrl?: string;\n /** VITE_CONVEX_URL */\n viteConvexUrl?: string;\n /** CONVEX_CLOUD_URL */\n convexCloudUrl?: string;\n};\n\n/** @deprecated Use `ConvexSiteUrlEnv` */\nexport type ResolveConvexSiteUrlOptions = ConvexSiteUrlEnv;\n\nexport function readConvexSiteUrlEnv(env: ProcessEnvLike): ConvexSiteUrlEnv {\n return {\n convexSiteUrl: env.CONVEX_SITE_URL,\n viteConvexSiteUrl: env.VITE_CONVEX_SITE_URL,\n convexUrl: env.CONVEX_URL,\n viteConvexUrl: env.VITE_CONVEX_URL,\n convexCloudUrl: env.CONVEX_CLOUD_URL,\n };\n}\n\n/**\n * Resolve .convex.site URL from env: explicit site URLs first, then derive from Convex cloud/site URLs.\n * Returns empty string when nothing is configured.\n */\nexport function resolveConvexSiteUrlFromEnv(env: ConvexSiteUrlEnv): string {\n const explicit = trimOptional(env.convexSiteUrl) ?? trimOptional(env.viteConvexSiteUrl);\n const cloud =\n trimOptional(env.convexUrl) ??\n trimOptional(env.viteConvexUrl) ??\n trimOptional(env.convexCloudUrl);\n\n if (explicit && cloud && !explicitSiteMatchesCloud(explicit, cloud)) {\n return deriveSiteFromCloudUrl(cloud) ?? normalizeTrailingSlash(explicit);\n }\n\n if (explicit) return normalizeTrailingSlash(explicit);\n\n return deriveSiteFromCloudUrl(cloud) ?? \"\";\n}\n\n/** Better Auth `baseURL` from standard Convex process env. */\nexport function convexSiteUrlFromProcessEnv(env: ProcessEnvLike = process.env): string {\n return resolveConvexSiteUrlFromEnv(readConvexSiteUrlEnv(env));\n}\n\n/** Resolve .convex.site URL for OTP sign-in and scripts (throws when unset). */\nexport function resolveConvexSiteUrl(env?: ConvexSiteUrlEnv): string {\n const url = resolveConvexSiteUrlFromEnv(env ?? {});\n if (!url) {\n throw new Error(\"Convex URL required to derive .convex.site URL for OTP sign-in\");\n }\n return url;\n}\n\nexport type AuthConfigIssuerEnv = ConvexSiteUrlEnv & {\n /** BAKED_AUTH_ISSUER patched at deploy time */\n bakedAuthIssuer?: string;\n};\n\n/**\n * Auth issuer for deploy scripts: baked deploy patch → convex site URL env chain.\n */\nexport function resolveAuthConfigIssuerFromEnv(env: AuthConfigIssuerEnv): string {\n const baked = trimOptional(env.bakedAuthIssuer);\n if (baked) return normalizeTrailingSlash(baked);\n\n return resolveConvexSiteUrlFromEnv(env);\n}\n\nexport type AuthConfigIssuerFromProcessEnvOptions = {\n bakedAuthIssuer?: string;\n /** Derive from CONVEX_URL / VITE_CONVEX_URL when issuer env vars are unset (deploy scripts). */\n deriveFromCloudUrl?: boolean;\n};\n\n/** JWT issuer from process env (auth.config.ts bundle vs deploy scripts). */\nexport function authConfigIssuerFromProcessEnv(\n env: ProcessEnvLike = process.env,\n options: AuthConfigIssuerFromProcessEnvOptions = {},\n): string {\n return resolveAuthConfigIssuerFromEnv({\n bakedAuthIssuer: options.bakedAuthIssuer,\n convexSiteUrl: env.CONVEX_SITE_URL,\n ...(options.deriveFromCloudUrl\n ? {\n convexUrl: env.CONVEX_URL,\n viteConvexUrl: env.VITE_CONVEX_URL,\n }\n : {}),\n });\n}\n\n/** Comma-separated browser origins (SITE_URL) for Better Auth trustedOrigins / crossDomain. */\nexport function parseTrustedSiteUrls(siteUrlRaw?: string): string[] {\n const raw = siteUrlRaw?.trim() ?? \"\";\n if (!raw) return [];\n return raw\n .split(\",\")\n .map((url) => url.trim())\n .filter(Boolean);\n}\n\nexport function primaryTrustedSiteUrl(siteUrlRaw?: string): string {\n const urls = parseTrustedSiteUrls(siteUrlRaw);\n return urls[0] ?? siteUrlRaw?.trim() ?? \"\";\n}\n\n/** Better Auth `trustedOrigins` from SITE_URL. */\nexport function betterAuthTrustedOrigins(siteUrlRaw?: string): string[] {\n const urls = parseTrustedSiteUrls(siteUrlRaw);\n if (urls.length > 0) return urls;\n const primary = primaryTrustedSiteUrl(siteUrlRaw);\n return primary ? [primary] : [];\n}\n","import { makeFunctionReference } from \"convex/server\";\n\n/** Public tenant record from Management's `tenants:resolveTenantByHost` (never secrets). */\nexport type PublicTenant = {\n slug: string;\n displayName: string;\n convexUrl: string;\n convexSiteUrl: string;\n status: \"active\" | \"disabled\";\n};\n\n/**\n * Management's public host → tenant query. It lives on the separately\n * configured Management deployment, so tenant apps cannot use their own\n * generated `api`; keep the cross-deployment name explicit here.\n */\nexport const resolveTenantByHostRef = makeFunctionReference<\n \"query\",\n { host: string },\n PublicTenant | null\n>(\"tenants:resolveTenantByHost\");\n\nimport { anyTenantSlugFromHost, hostnameFromHostHeader } from \"./tenantHosts.ts\";\nimport { resolveConvexSiteUrlFromEnv } from \"./convexSiteUrl.ts\";\n\nexport type StudioBootEnv = {\n host: string;\n managementConvexUrl?: string;\n viteConvexUrl?: string;\n viteConvexSiteUrl?: string;\n};\n\nexport type StudioBootPlan =\n | { mode: \"management\"; host: string; slug: string; managementConvexUrl: string }\n | {\n mode: \"baked\";\n convexUrl: string;\n convexSiteUrl: string;\n reason: \"preview\" | \"no-tenant-host\" | \"no-management-url\";\n }\n | { mode: \"error\"; code: \"missing_config\" | \"not_a_tenant_host\"; message: string };\n\n/** Preview Studio hosts (`preview-*.columnus.app`). */\nexport function isPreviewStudioHost(host: string): boolean {\n const hostname = hostnameFromHostHeader(host);\n return /^preview-[a-z0-9-]+\\.columnus\\.app$/i.test(hostname);\n}\n\nfunction normalizeUrl(url: string): string {\n return url.trim().replace(/\\/+$/, \"\");\n}\n\nfunction siteUrlFromCloudOrExplicit(cloudUrl: string, explicitSite?: string): string {\n return resolveConvexSiteUrlFromEnv({\n convexUrl: cloudUrl,\n convexSiteUrl: explicitSite,\n });\n}\n\n/**\n * Decide how Studio should obtain its tenant Convex URL.\n * Does not call the network — management mode still needs resolveTenantByHost.\n */\nexport function planStudioBoot(env: StudioBootEnv): StudioBootPlan {\n const management = env.managementConvexUrl?.trim();\n const baked = env.viteConvexUrl?.trim();\n const host = env.host;\n\n if (management) {\n const slug = anyTenantSlugFromHost(host);\n if (slug) {\n return {\n mode: \"management\",\n host: hostnameFromHostHeader(host),\n slug,\n managementConvexUrl: normalizeUrl(management),\n };\n }\n // Multi-tenant default: hard-gate apex / loopback / unknown hosts (D49).\n return {\n mode: \"error\",\n code: \"not_a_tenant_host\",\n message:\n \"This host is not a tenant subdomain (expected {slug}.columnus.app or {slug}.columnus.localhost). Open a registered tenant host such as https://app.columnus.localhost.\",\n };\n }\n\n // A baked single-deployment host does not need Management resolution.\n if (isPreviewStudioHost(host)) {\n if (!baked) {\n return {\n mode: \"error\",\n code: \"missing_config\",\n message:\n \"Preview hosts require VITE_MANAGEMENT_CONVEX_URL (Manage resolve) or VITE_CONVEX_URL (legacy bake).\",\n };\n }\n return {\n mode: \"baked\",\n convexUrl: normalizeUrl(baked),\n convexSiteUrl: siteUrlFromCloudOrExplicit(normalizeUrl(baked), env.viteConvexSiteUrl),\n reason: \"preview\",\n };\n }\n\n if (baked) {\n return {\n mode: \"baked\",\n convexUrl: normalizeUrl(baked),\n convexSiteUrl: siteUrlFromCloudOrExplicit(normalizeUrl(baked), env.viteConvexSiteUrl),\n reason: \"no-management-url\",\n };\n }\n\n return {\n mode: \"error\",\n code: \"missing_config\",\n message:\n \"Set VITE_MANAGEMENT_CONVEX_URL (multi-tenant) or VITE_CONVEX_URL (single deployment).\",\n };\n}\n\nexport type StudioHardGateReason = \"unknown\" | \"disabled\" | \"resolve_failed\" | \"config\";\n\nexport type StudioBootResult =\n | {\n ok: true;\n convexUrl: string;\n convexSiteUrl: string;\n source: \"management\" | \"baked\";\n slug?: string;\n displayName?: string;\n }\n | {\n ok: false;\n reason: StudioHardGateReason;\n message: string;\n slug?: string;\n displayName?: string;\n };\n\n/**\n * Guest share URLs (`/s/{token}`) must open the Convex WebSocket without a JWT.\n * `expectAuth: true` pauses the socket until `setAuth`, which never runs for\n * anonymous recipients — they stay stuck on \"Opening secure link…\".\n */\nexport function shouldExpectConvexAuth(pathname: string): boolean {\n return !pathname.startsWith(\"/s/\");\n}\n"]}
1
+ {"version":3,"sources":["../src/tenantHosts.ts","../src/convexSiteUrl.ts","../src/studioBoot.ts"],"names":[],"mappings":"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","file":"studioBoot.js","sourcesContent":["/** Reserved first labels under `*.columnus.app` / `*.columnus.localhost` — not tenants. */\nexport const RESERVED_TENANT_SLUGS = [\"manage\", \"deploy\", \"www\"] as const;\n\nconst RESERVED_SLUG_SET = new Set<string>(RESERVED_TENANT_SLUGS);\n\n/** Lowercase slug: letters/digits with optional hyphen segments (e.g. `app`, `acme-prod`). */\nexport const TENANT_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nexport function isReservedTenantSlug(slug: string): boolean {\n const normalized = slug.trim().toLowerCase();\n if (!normalized) return true;\n if (RESERVED_SLUG_SET.has(normalized)) return true;\n if (normalized.startsWith(\"preview-\")) return true;\n return false;\n}\n\nexport function isValidTenantSlug(slug: string): boolean {\n const trimmed = slug.trim();\n // Require already-lowercase slugs (hosts are normalized before parse).\n if (!TENANT_SLUG_RE.test(trimmed)) return false;\n return !isReservedTenantSlug(trimmed);\n}\n\n/** Strip optional port from a Host header or `window.location.host` value. */\nexport function hostnameFromHostHeader(host: string): string {\n const trimmed = host.trim().toLowerCase();\n if (!trimmed) return \"\";\n // IPv6 bracket form is not used for tenant hosts; strip :port for hostname:port.\n if (trimmed.startsWith(\"[\")) {\n const end = trimmed.indexOf(\"]\");\n return end === -1 ? trimmed : trimmed.slice(1, end);\n }\n const colon = trimmed.lastIndexOf(\":\");\n if (colon > -1 && /^\\d+$/.test(trimmed.slice(colon + 1))) {\n return trimmed.slice(0, colon);\n }\n return trimmed;\n}\n\n/**\n * Extract tenant slug from a hostname.\n * Supports `{slug}.columnus.app`, `{slug}.columnus.dev` and local hosts.\n * Returns null for apex, reserved, invalid, or unrecognized hosts.\n */\nexport function tenantSlugFromHost(host: string): string | null {\n const hostname = hostnameFromHostHeader(host);\n const match = hostname.match(/^([a-z0-9-]+)\\.columnus\\.(app|dev|localhost)$/);\n if (!match) return null;\n const slug = match[1];\n if (!slug || !isValidTenantSlug(slug)) return null;\n return slug;\n}\n\nexport type TenantPublicHostEnv = \"prod\" | \"stage\" | \"local\";\n\n/** Public hostname for a tenant slug (no scheme). */\nexport function tenantPublicHost(slug: string, env: TenantPublicHostEnv = \"prod\"): string {\n const normalized = slug.trim().toLowerCase();\n const suffix = { prod: \"app\", stage: \"dev\", local: \"localhost\" }[env];\n return `${normalized}.columnus.${suffix}`;\n}\n\n/** Preview tenant slug: `preview-<label>` (e.g. `preview-12`). */\nexport const PREVIEW_TENANT_SLUG_RE = /^preview-[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nexport function isValidPreviewTenantSlug(slug: string): boolean {\n return PREVIEW_TENANT_SLUG_RE.test(slug.trim().toLowerCase());\n}\n\n/**\n * Extract preview tenant slug from hostname (`preview-12.columnus.app`).\n * Returns null for non-preview or invalid hosts.\n */\nexport function previewSlugFromHost(host: string): string | null {\n const hostname = hostnameFromHostHeader(host);\n const match = hostname.match(/^(preview-[a-z0-9-]+)\\.columnus\\.(app|dev|localhost)$/);\n if (!match) return null;\n const slug = match[1];\n if (!slug || !isValidPreviewTenantSlug(slug)) return null;\n return slug;\n}\n\n/**\n * Resolve either a standard tenant slug or a registered preview slug from a host.\n * Does not check Management DB — only host shape.\n */\nexport function anyTenantSlugFromHost(host: string): string | null {\n return tenantSlugFromHost(host) ?? previewSlugFromHost(host);\n}\n","/** Convex deployment cloud URL (.convex.cloud) → HTTP actions / auth issuer (.convex.site). */\nexport function convexCloudToSiteUrl(convexUrl: string): string {\n const trimmed = convexUrl.trim().replace(/\\/$/, \"\");\n if (trimmed.includes(\".convex.site\")) return trimmed;\n if (trimmed.includes(\".convex.cloud\")) {\n return trimmed.replace(/\\.convex\\.cloud\\b/, \".convex.site\");\n }\n return trimmed;\n}\n\nexport function convexSiteToCloudUrl(siteUrl: string): string {\n const trimmed = siteUrl.trim().replace(/\\/$/, \"\");\n if (trimmed.includes(\".convex.cloud\")) return trimmed;\n if (trimmed.includes(\".convex.site\")) {\n return trimmed.replace(/\\.convex\\.site\\b/, \".convex.cloud\");\n }\n return trimmed;\n}\n\ntype ProcessEnvLike = Record<string, string | undefined>;\n\nfunction trimOptional(value?: string): string | undefined {\n const trimmed = value?.trim();\n return trimmed || undefined;\n}\n\nfunction normalizeTrailingSlash(url: string): string {\n return url.trim().replace(/\\/$/, \"\");\n}\n\nfunction deriveSiteFromCloudUrl(cloud?: string): string | undefined {\n const trimmed = cloud?.trim();\n if (!trimmed) return undefined;\n if (trimmed.includes(\".convex.cloud\") || trimmed.includes(\".convex.site\")) {\n return convexCloudToSiteUrl(trimmed);\n }\n return undefined;\n}\n\n/** Docker runtime-base verify placeholder — must not be used for real BFF prepare. */\nexport function isPlaceholderManagementSiteUrl(url: string): boolean {\n try {\n const host = new URL(/:\\/\\//.test(url) ? url : `https://${url}`).hostname;\n return host === \"placeholder.convex.site\" || host.endsWith(\".placeholder.convex.site\");\n } catch {\n return /placeholder\\.convex\\.site/i.test(url);\n }\n}\n\n/**\n * Management `.convex.site` origin for BFF `prepare`.\n * Prefers explicit management site env vars, else derives from `VITE_MANAGEMENT_CONVEX_URL`.\n * Does not read `VITE_CONVEX_SITE_URL` — on web that is the tenant site, not Manage.\n */\nexport function resolveBffManagementSiteUrl(env: ProcessEnvLike = process.env): string | undefined {\n for (const key of [\"COLUMNUS_MANAGEMENT_SITE_URL\", \"MANAGEMENT_CONVEX_SITE_URL\"] as const) {\n const value = trimOptional(env[key]);\n if (value && !isPlaceholderManagementSiteUrl(value)) {\n return normalizeTrailingSlash(value);\n }\n }\n const managementCloud = trimOptional(env.VITE_MANAGEMENT_CONVEX_URL);\n if (managementCloud) return convexCloudToSiteUrl(managementCloud);\n return undefined;\n}\n\n/**\n * Runtime BFF management site: prefer the image-baked URL over runtime defaults.\n * A stale runtime `MANAGEMENT_CONVEX_SITE_URL` must not shadow the\n * ephemeral Manage preview bake and makes prepare return HTTP 404 (no routes on prod).\n */\nexport function resolveRuntimeManagementSiteUrl(options: {\n bakedSiteUrl?: string | null;\n env?: ProcessEnvLike;\n}): string | undefined {\n const bakedSite = trimOptional(options.bakedSiteUrl ?? undefined);\n if (bakedSite && !isPlaceholderManagementSiteUrl(bakedSite)) {\n return normalizeTrailingSlash(bakedSite);\n }\n return resolveBffManagementSiteUrl(options.env ?? process.env);\n}\n\nfunction convexDeploymentSlug(url: string): string | undefined {\n const match = url.match(/https:\\/\\/([^.]+)\\.convex\\.(?:cloud|site)/);\n return match?.[1];\n}\n\n/** Deployment name from a Convex cloud/site URL (`https://{name}.convex.cloud`). */\nexport function convexDeploymentSlugFromUrl(url: string): string | undefined {\n return convexDeploymentSlug(url);\n}\n\n/**\n * Best-effort `CONVEX_DEPLOYMENT` when only the cloud URL is known (e.g. after `columnus-api pull`).\n * Defaults to `dev:{slug}` — preview/prod deployments may need an explicit value in `.env.local`.\n */\nexport function deriveConvexDeploymentFromCloudUrl(cloudUrl: string): string | undefined {\n const slug = convexDeploymentSlug(cloudUrl);\n return slug ? `dev:${slug}` : undefined;\n}\n\nfunction explicitSiteMatchesCloud(explicit: string, cloud: string): boolean {\n const explicitSlug = convexDeploymentSlug(explicit);\n const cloudSlug = convexDeploymentSlug(cloud);\n if (!explicitSlug || !cloudSlug) return true;\n return explicitSlug === cloudSlug;\n}\n\nexport type ConvexSiteUrlEnv = {\n /** CONVEX_SITE_URL */\n convexSiteUrl?: string;\n /** VITE_CONVEX_SITE_URL */\n viteConvexSiteUrl?: string;\n /** CONVEX_URL */\n convexUrl?: string;\n /** VITE_CONVEX_URL */\n viteConvexUrl?: string;\n /** CONVEX_CLOUD_URL */\n convexCloudUrl?: string;\n};\n\n/** @deprecated Use `ConvexSiteUrlEnv` */\nexport type ResolveConvexSiteUrlOptions = ConvexSiteUrlEnv;\n\nexport function readConvexSiteUrlEnv(env: ProcessEnvLike): ConvexSiteUrlEnv {\n return {\n convexSiteUrl: env.CONVEX_SITE_URL,\n viteConvexSiteUrl: env.VITE_CONVEX_SITE_URL,\n convexUrl: env.CONVEX_URL,\n viteConvexUrl: env.VITE_CONVEX_URL,\n convexCloudUrl: env.CONVEX_CLOUD_URL,\n };\n}\n\n/**\n * Resolve .convex.site URL from env: explicit site URLs first, then derive from Convex cloud/site URLs.\n * Returns empty string when nothing is configured.\n */\nexport function resolveConvexSiteUrlFromEnv(env: ConvexSiteUrlEnv): string {\n const explicit = trimOptional(env.convexSiteUrl) ?? trimOptional(env.viteConvexSiteUrl);\n const cloud =\n trimOptional(env.convexUrl) ??\n trimOptional(env.viteConvexUrl) ??\n trimOptional(env.convexCloudUrl);\n\n if (explicit && cloud && !explicitSiteMatchesCloud(explicit, cloud)) {\n return deriveSiteFromCloudUrl(cloud) ?? normalizeTrailingSlash(explicit);\n }\n\n if (explicit) return normalizeTrailingSlash(explicit);\n\n return deriveSiteFromCloudUrl(cloud) ?? \"\";\n}\n\n/** Better Auth `baseURL` from standard Convex process env. */\nexport function convexSiteUrlFromProcessEnv(env: ProcessEnvLike = process.env): string {\n return resolveConvexSiteUrlFromEnv(readConvexSiteUrlEnv(env));\n}\n\n/** Resolve .convex.site URL for OTP sign-in and scripts (throws when unset). */\nexport function resolveConvexSiteUrl(env?: ConvexSiteUrlEnv): string {\n const url = resolveConvexSiteUrlFromEnv(env ?? {});\n if (!url) {\n throw new Error(\"Convex URL required to derive .convex.site URL for OTP sign-in\");\n }\n return url;\n}\n\nexport type AuthConfigIssuerEnv = ConvexSiteUrlEnv & {\n /** BAKED_AUTH_ISSUER patched at deploy time */\n bakedAuthIssuer?: string;\n};\n\n/**\n * Auth issuer for deploy scripts: baked deploy patch → convex site URL env chain.\n */\nexport function resolveAuthConfigIssuerFromEnv(env: AuthConfigIssuerEnv): string {\n const baked = trimOptional(env.bakedAuthIssuer);\n if (baked) return normalizeTrailingSlash(baked);\n\n return resolveConvexSiteUrlFromEnv(env);\n}\n\nexport type AuthConfigIssuerFromProcessEnvOptions = {\n bakedAuthIssuer?: string;\n /** Derive from CONVEX_URL / VITE_CONVEX_URL when issuer env vars are unset (deploy scripts). */\n deriveFromCloudUrl?: boolean;\n};\n\n/** JWT issuer from process env (auth.config.ts bundle vs deploy scripts). */\nexport function authConfigIssuerFromProcessEnv(\n env: ProcessEnvLike = process.env,\n options: AuthConfigIssuerFromProcessEnvOptions = {},\n): string {\n return resolveAuthConfigIssuerFromEnv({\n bakedAuthIssuer: options.bakedAuthIssuer,\n convexSiteUrl: env.CONVEX_SITE_URL,\n ...(options.deriveFromCloudUrl\n ? {\n convexUrl: env.CONVEX_URL,\n viteConvexUrl: env.VITE_CONVEX_URL,\n }\n : {}),\n });\n}\n\n/** Comma-separated browser origins (SITE_URL) for Better Auth trustedOrigins / crossDomain. */\nexport function parseTrustedSiteUrls(siteUrlRaw?: string): string[] {\n const raw = siteUrlRaw?.trim() ?? \"\";\n if (!raw) return [];\n return raw\n .split(\",\")\n .map((url) => url.trim())\n .filter(Boolean);\n}\n\nexport function primaryTrustedSiteUrl(siteUrlRaw?: string): string {\n const urls = parseTrustedSiteUrls(siteUrlRaw);\n return urls[0] ?? siteUrlRaw?.trim() ?? \"\";\n}\n\n/** Better Auth `trustedOrigins` from SITE_URL. */\nexport function betterAuthTrustedOrigins(siteUrlRaw?: string): string[] {\n const urls = parseTrustedSiteUrls(siteUrlRaw);\n if (urls.length > 0) return urls;\n const primary = primaryTrustedSiteUrl(siteUrlRaw);\n return primary ? [primary] : [];\n}\n","import { makeFunctionReference } from \"convex/server\";\n\n/** Public tenant record from Management's `tenants:resolveTenantByHost` (never secrets). */\nexport type PublicTenant = {\n slug: string;\n displayName: string;\n convexUrl: string;\n convexSiteUrl: string;\n status: \"active\" | \"disabled\";\n};\n\n/**\n * Management's public host → tenant query. It lives on the separately\n * configured Management deployment, so tenant apps cannot use their own\n * generated `api`; keep the cross-deployment name explicit here.\n */\nexport const resolveTenantByHostRef = makeFunctionReference<\n \"query\",\n { host: string },\n PublicTenant | null\n>(\"tenants:resolveTenantByHost\");\n\nimport { anyTenantSlugFromHost, hostnameFromHostHeader } from \"./tenantHosts.ts\";\nimport { resolveConvexSiteUrlFromEnv } from \"./convexSiteUrl.ts\";\n\nexport type StudioBootEnv = {\n host: string;\n managementConvexUrl?: string;\n viteConvexUrl?: string;\n viteConvexSiteUrl?: string;\n};\n\nexport type StudioBootPlan =\n | { mode: \"management\"; host: string; slug: string; managementConvexUrl: string }\n | {\n mode: \"baked\";\n convexUrl: string;\n convexSiteUrl: string;\n reason: \"preview\" | \"no-tenant-host\" | \"no-management-url\";\n }\n | { mode: \"error\"; code: \"missing_config\" | \"not_a_tenant_host\"; message: string };\n\n/** Preview Studio hosts (`preview-*.columnus.app`). */\nexport function isPreviewStudioHost(host: string): boolean {\n const hostname = hostnameFromHostHeader(host);\n return /^preview-[a-z0-9-]+\\.columnus\\.app$/i.test(hostname);\n}\n\nfunction normalizeUrl(url: string): string {\n return url.trim().replace(/\\/+$/, \"\");\n}\n\nfunction siteUrlFromCloudOrExplicit(cloudUrl: string, explicitSite?: string): string {\n return resolveConvexSiteUrlFromEnv({\n convexUrl: cloudUrl,\n convexSiteUrl: explicitSite,\n });\n}\n\n/**\n * Decide how Studio should obtain its tenant Convex URL.\n * Does not call the network — management mode still needs resolveTenantByHost.\n */\nexport function planStudioBoot(env: StudioBootEnv): StudioBootPlan {\n const management = env.managementConvexUrl?.trim();\n const baked = env.viteConvexUrl?.trim();\n const host = env.host;\n\n if (management) {\n const slug = anyTenantSlugFromHost(host);\n if (slug) {\n return {\n mode: \"management\",\n host: hostnameFromHostHeader(host),\n slug,\n managementConvexUrl: normalizeUrl(management),\n };\n }\n // Multi-tenant default: hard-gate apex / loopback / unknown hosts (D49).\n return {\n mode: \"error\",\n code: \"not_a_tenant_host\",\n message:\n \"This host is not a tenant subdomain (expected {slug}.columnus.app or {slug}.columnus.localhost). Open a registered tenant host such as https://app.columnus.localhost.\",\n };\n }\n\n // A baked single-deployment host does not need Management resolution.\n if (isPreviewStudioHost(host)) {\n if (!baked) {\n return {\n mode: \"error\",\n code: \"missing_config\",\n message:\n \"Preview hosts require VITE_MANAGEMENT_CONVEX_URL (Manage resolve) or VITE_CONVEX_URL (legacy bake).\",\n };\n }\n return {\n mode: \"baked\",\n convexUrl: normalizeUrl(baked),\n convexSiteUrl: siteUrlFromCloudOrExplicit(normalizeUrl(baked), env.viteConvexSiteUrl),\n reason: \"preview\",\n };\n }\n\n if (baked) {\n return {\n mode: \"baked\",\n convexUrl: normalizeUrl(baked),\n convexSiteUrl: siteUrlFromCloudOrExplicit(normalizeUrl(baked), env.viteConvexSiteUrl),\n reason: \"no-management-url\",\n };\n }\n\n return {\n mode: \"error\",\n code: \"missing_config\",\n message:\n \"Set VITE_MANAGEMENT_CONVEX_URL (multi-tenant) or VITE_CONVEX_URL (single deployment).\",\n };\n}\n\nexport type StudioHardGateReason = \"unknown\" | \"disabled\" | \"resolve_failed\" | \"config\";\n\nexport type StudioBootResult =\n | {\n ok: true;\n convexUrl: string;\n convexSiteUrl: string;\n source: \"management\" | \"baked\";\n slug?: string;\n displayName?: string;\n }\n | {\n ok: false;\n reason: StudioHardGateReason;\n message: string;\n slug?: string;\n displayName?: string;\n };\n\n/**\n * Guest share URLs (`/s/{token}`) must open the Convex WebSocket without a JWT.\n * `expectAuth: true` pauses the socket until `setAuth`, which never runs for\n * anonymous recipients — they stay stuck on \"Opening secure link…\".\n */\nexport function shouldExpectConvexAuth(pathname: string): boolean {\n return !pathname.startsWith(\"/s/\");\n}\n"]}
@@ -8,11 +8,11 @@ export declare function isValidTenantSlug(slug: string): boolean;
8
8
  export declare function hostnameFromHostHeader(host: string): string;
9
9
  /**
10
10
  * Extract tenant slug from a hostname.
11
- * Supports `{slug}.columnus.app` and `{slug}.columnus.localhost`.
11
+ * Supports `{slug}.columnus.app`, `{slug}.columnus.dev` and local hosts.
12
12
  * Returns null for apex, reserved, invalid, or unrecognized hosts.
13
13
  */
14
14
  export declare function tenantSlugFromHost(host: string): string | null;
15
- export type TenantPublicHostEnv = "prod" | "local";
15
+ export type TenantPublicHostEnv = "prod" | "stage" | "local";
16
16
  /** Public hostname for a tenant slug (no scheme). */
17
17
  export declare function tenantPublicHost(slug: string, env?: TenantPublicHostEnv): string;
18
18
  /** Preview tenant slug: `preview-<label>` (e.g. `preview-12`). */
@@ -29,7 +29,7 @@ function hostnameFromHostHeader(host) {
29
29
  }
30
30
  function tenantSlugFromHost(host) {
31
31
  const hostname = hostnameFromHostHeader(host);
32
- const match = hostname.match(/^([a-z0-9-]+)\.columnus\.(app|localhost)$/);
32
+ const match = hostname.match(/^([a-z0-9-]+)\.columnus\.(app|dev|localhost)$/);
33
33
  if (!match) return null;
34
34
  const slug = match[1];
35
35
  if (!slug || !isValidTenantSlug(slug)) return null;
@@ -37,7 +37,8 @@ function tenantSlugFromHost(host) {
37
37
  }
38
38
  function tenantPublicHost(slug, env = "prod") {
39
39
  const normalized = slug.trim().toLowerCase();
40
- return env === "local" ? `${normalized}.columnus.localhost` : `${normalized}.columnus.app`;
40
+ const suffix = { prod: "app", stage: "dev", local: "localhost" }[env];
41
+ return `${normalized}.columnus.${suffix}`;
41
42
  }
42
43
  var PREVIEW_TENANT_SLUG_RE = /^preview-[a-z0-9]+(?:-[a-z0-9]+)*$/;
43
44
  function isValidPreviewTenantSlug(slug) {
@@ -45,7 +46,7 @@ function isValidPreviewTenantSlug(slug) {
45
46
  }
46
47
  function previewSlugFromHost(host) {
47
48
  const hostname = hostnameFromHostHeader(host);
48
- const match = hostname.match(/^(preview-[a-z0-9-]+)\.columnus\.(app|localhost)$/);
49
+ const match = hostname.match(/^(preview-[a-z0-9-]+)\.columnus\.(app|dev|localhost)$/);
49
50
  if (!match) return null;
50
51
  const slug = match[1];
51
52
  if (!slug || !isValidPreviewTenantSlug(slug)) return null;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tenantHosts.ts"],"names":[],"mappings":";AACO,IAAM,qBAAA,GAAwB,CAAC,QAAA,EAAU,QAAA,EAAU,KAAK;AAE/D,IAAM,iBAAA,GAAoB,IAAI,GAAA,CAAY,qBAAqB,CAAA;AAGxD,IAAM,cAAA,GAAiB;AAEvB,SAAS,qBAAqB,IAAA,EAAuB;AAC1D,EAAA,MAAM,UAAA,GAAa,IAAA,CAAK,IAAA,EAAK,CAAE,WAAA,EAAY;AAC3C,EAAA,IAAI,CAAC,YAAY,OAAO,IAAA;AACxB,EAAA,IAAI,iBAAA,CAAkB,GAAA,CAAI,UAAU,CAAA,EAAG,OAAO,IAAA;AAC9C,EAAA,IAAI,UAAA,CAAW,UAAA,CAAW,UAAU,CAAA,EAAG,OAAO,IAAA;AAC9C,EAAA,OAAO,KAAA;AACT;AAEO,SAAS,kBAAkB,IAAA,EAAuB;AACvD,EAAA,MAAM,OAAA,GAAU,KAAK,IAAA,EAAK;AAE1B,EAAA,IAAI,CAAC,cAAA,CAAe,IAAA,CAAK,OAAO,GAAG,OAAO,KAAA;AAC1C,EAAA,OAAO,CAAC,qBAAqB,OAAO,CAAA;AACtC;AAGO,SAAS,uBAAuB,IAAA,EAAsB;AAC3D,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,IAAA,EAAK,CAAE,WAAA,EAAY;AACxC,EAAA,IAAI,CAAC,SAAS,OAAO,EAAA;AAErB,EAAA,IAAI,OAAA,CAAQ,UAAA,CAAW,GAAG,CAAA,EAAG;AAC3B,IAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA;AAC/B,IAAA,OAAO,QAAQ,EAAA,GAAK,OAAA,GAAU,OAAA,CAAQ,KAAA,CAAM,GAAG,GAAG,CAAA;AAAA,EACpD;AACA,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,WAAA,CAAY,GAAG,CAAA;AACrC,EAAA,IAAI,KAAA,GAAQ,MAAM,OAAA,CAAQ,IAAA,CAAK,QAAQ,KAAA,CAAM,KAAA,GAAQ,CAAC,CAAC,CAAA,EAAG;AACxD,IAAA,OAAO,OAAA,CAAQ,KAAA,CAAM,CAAA,EAAG,KAAK,CAAA;AAAA,EAC/B;AACA,EAAA,OAAO,OAAA;AACT;AAOO,SAAS,mBAAmB,IAAA,EAA6B;AAC9D,EAAA,MAAM,QAAA,GAAW,uBAAuB,IAAI,CAAA;AAC5C,EAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,KAAA,CAAM,2CAA2C,CAAA;AACxE,EAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AACnB,EAAA,MAAM,IAAA,GAAO,MAAM,CAAC,CAAA;AACpB,EAAA,IAAI,CAAC,IAAA,IAAQ,CAAC,iBAAA,CAAkB,IAAI,GAAG,OAAO,IAAA;AAC9C,EAAA,OAAO,IAAA;AACT;AAKO,SAAS,gBAAA,CAAiB,IAAA,EAAc,GAAA,GAA2B,MAAA,EAAgB;AACxF,EAAA,MAAM,UAAA,GAAa,IAAA,CAAK,IAAA,EAAK,CAAE,WAAA,EAAY;AAC3C,EAAA,OAAO,QAAQ,OAAA,GAAU,CAAA,EAAG,UAAU,CAAA,mBAAA,CAAA,GAAwB,GAAG,UAAU,CAAA,aAAA,CAAA;AAC7E;AAGO,IAAM,sBAAA,GAAyB;AAE/B,SAAS,yBAAyB,IAAA,EAAuB;AAC9D,EAAA,OAAO,uBAAuB,IAAA,CAAK,IAAA,CAAK,IAAA,EAAK,CAAE,aAAa,CAAA;AAC9D;AAMO,SAAS,oBAAoB,IAAA,EAA6B;AAC/D,EAAA,MAAM,QAAA,GAAW,uBAAuB,IAAI,CAAA;AAC5C,EAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,KAAA,CAAM,mDAAmD,CAAA;AAChF,EAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AACnB,EAAA,MAAM,IAAA,GAAO,MAAM,CAAC,CAAA;AACpB,EAAA,IAAI,CAAC,IAAA,IAAQ,CAAC,wBAAA,CAAyB,IAAI,GAAG,OAAO,IAAA;AACrD,EAAA,OAAO,IAAA;AACT;AAMO,SAAS,sBAAsB,IAAA,EAA6B;AACjE,EAAA,OAAO,kBAAA,CAAmB,IAAI,CAAA,IAAK,mBAAA,CAAoB,IAAI,CAAA;AAC7D","file":"tenantHosts.js","sourcesContent":["/** Reserved first labels under `*.columnus.app` / `*.columnus.localhost` — not tenants. */\nexport const RESERVED_TENANT_SLUGS = [\"manage\", \"deploy\", \"www\"] as const;\n\nconst RESERVED_SLUG_SET = new Set<string>(RESERVED_TENANT_SLUGS);\n\n/** Lowercase slug: letters/digits with optional hyphen segments (e.g. `app`, `acme-prod`). */\nexport const TENANT_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nexport function isReservedTenantSlug(slug: string): boolean {\n const normalized = slug.trim().toLowerCase();\n if (!normalized) return true;\n if (RESERVED_SLUG_SET.has(normalized)) return true;\n if (normalized.startsWith(\"preview-\")) return true;\n return false;\n}\n\nexport function isValidTenantSlug(slug: string): boolean {\n const trimmed = slug.trim();\n // Require already-lowercase slugs (hosts are normalized before parse).\n if (!TENANT_SLUG_RE.test(trimmed)) return false;\n return !isReservedTenantSlug(trimmed);\n}\n\n/** Strip optional port from a Host header or `window.location.host` value. */\nexport function hostnameFromHostHeader(host: string): string {\n const trimmed = host.trim().toLowerCase();\n if (!trimmed) return \"\";\n // IPv6 bracket form is not used for tenant hosts; strip :port for hostname:port.\n if (trimmed.startsWith(\"[\")) {\n const end = trimmed.indexOf(\"]\");\n return end === -1 ? trimmed : trimmed.slice(1, end);\n }\n const colon = trimmed.lastIndexOf(\":\");\n if (colon > -1 && /^\\d+$/.test(trimmed.slice(colon + 1))) {\n return trimmed.slice(0, colon);\n }\n return trimmed;\n}\n\n/**\n * Extract tenant slug from a hostname.\n * Supports `{slug}.columnus.app` and `{slug}.columnus.localhost`.\n * Returns null for apex, reserved, invalid, or unrecognized hosts.\n */\nexport function tenantSlugFromHost(host: string): string | null {\n const hostname = hostnameFromHostHeader(host);\n const match = hostname.match(/^([a-z0-9-]+)\\.columnus\\.(app|localhost)$/);\n if (!match) return null;\n const slug = match[1];\n if (!slug || !isValidTenantSlug(slug)) return null;\n return slug;\n}\n\nexport type TenantPublicHostEnv = \"prod\" | \"local\";\n\n/** Public hostname for a tenant slug (no scheme). */\nexport function tenantPublicHost(slug: string, env: TenantPublicHostEnv = \"prod\"): string {\n const normalized = slug.trim().toLowerCase();\n return env === \"local\" ? `${normalized}.columnus.localhost` : `${normalized}.columnus.app`;\n}\n\n/** Preview tenant slug: `preview-<label>` (e.g. `preview-12`). */\nexport const PREVIEW_TENANT_SLUG_RE = /^preview-[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nexport function isValidPreviewTenantSlug(slug: string): boolean {\n return PREVIEW_TENANT_SLUG_RE.test(slug.trim().toLowerCase());\n}\n\n/**\n * Extract preview tenant slug from hostname (`preview-12.columnus.app`).\n * Returns null for non-preview or invalid hosts.\n */\nexport function previewSlugFromHost(host: string): string | null {\n const hostname = hostnameFromHostHeader(host);\n const match = hostname.match(/^(preview-[a-z0-9-]+)\\.columnus\\.(app|localhost)$/);\n if (!match) return null;\n const slug = match[1];\n if (!slug || !isValidPreviewTenantSlug(slug)) return null;\n return slug;\n}\n\n/**\n * Resolve either a standard tenant slug or a registered preview slug from a host.\n * Does not check Management DB — only host shape.\n */\nexport function anyTenantSlugFromHost(host: string): string | null {\n return tenantSlugFromHost(host) ?? previewSlugFromHost(host);\n}\n"]}
1
+ {"version":3,"sources":["../src/tenantHosts.ts"],"names":[],"mappings":"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","file":"tenantHosts.js","sourcesContent":["/** Reserved first labels under `*.columnus.app` / `*.columnus.localhost` — not tenants. */\nexport const RESERVED_TENANT_SLUGS = [\"manage\", \"deploy\", \"www\"] as const;\n\nconst RESERVED_SLUG_SET = new Set<string>(RESERVED_TENANT_SLUGS);\n\n/** Lowercase slug: letters/digits with optional hyphen segments (e.g. `app`, `acme-prod`). */\nexport const TENANT_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nexport function isReservedTenantSlug(slug: string): boolean {\n const normalized = slug.trim().toLowerCase();\n if (!normalized) return true;\n if (RESERVED_SLUG_SET.has(normalized)) return true;\n if (normalized.startsWith(\"preview-\")) return true;\n return false;\n}\n\nexport function isValidTenantSlug(slug: string): boolean {\n const trimmed = slug.trim();\n // Require already-lowercase slugs (hosts are normalized before parse).\n if (!TENANT_SLUG_RE.test(trimmed)) return false;\n return !isReservedTenantSlug(trimmed);\n}\n\n/** Strip optional port from a Host header or `window.location.host` value. */\nexport function hostnameFromHostHeader(host: string): string {\n const trimmed = host.trim().toLowerCase();\n if (!trimmed) return \"\";\n // IPv6 bracket form is not used for tenant hosts; strip :port for hostname:port.\n if (trimmed.startsWith(\"[\")) {\n const end = trimmed.indexOf(\"]\");\n return end === -1 ? trimmed : trimmed.slice(1, end);\n }\n const colon = trimmed.lastIndexOf(\":\");\n if (colon > -1 && /^\\d+$/.test(trimmed.slice(colon + 1))) {\n return trimmed.slice(0, colon);\n }\n return trimmed;\n}\n\n/**\n * Extract tenant slug from a hostname.\n * Supports `{slug}.columnus.app`, `{slug}.columnus.dev` and local hosts.\n * Returns null for apex, reserved, invalid, or unrecognized hosts.\n */\nexport function tenantSlugFromHost(host: string): string | null {\n const hostname = hostnameFromHostHeader(host);\n const match = hostname.match(/^([a-z0-9-]+)\\.columnus\\.(app|dev|localhost)$/);\n if (!match) return null;\n const slug = match[1];\n if (!slug || !isValidTenantSlug(slug)) return null;\n return slug;\n}\n\nexport type TenantPublicHostEnv = \"prod\" | \"stage\" | \"local\";\n\n/** Public hostname for a tenant slug (no scheme). */\nexport function tenantPublicHost(slug: string, env: TenantPublicHostEnv = \"prod\"): string {\n const normalized = slug.trim().toLowerCase();\n const suffix = { prod: \"app\", stage: \"dev\", local: \"localhost\" }[env];\n return `${normalized}.columnus.${suffix}`;\n}\n\n/** Preview tenant slug: `preview-<label>` (e.g. `preview-12`). */\nexport const PREVIEW_TENANT_SLUG_RE = /^preview-[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nexport function isValidPreviewTenantSlug(slug: string): boolean {\n return PREVIEW_TENANT_SLUG_RE.test(slug.trim().toLowerCase());\n}\n\n/**\n * Extract preview tenant slug from hostname (`preview-12.columnus.app`).\n * Returns null for non-preview or invalid hosts.\n */\nexport function previewSlugFromHost(host: string): string | null {\n const hostname = hostnameFromHostHeader(host);\n const match = hostname.match(/^(preview-[a-z0-9-]+)\\.columnus\\.(app|dev|localhost)$/);\n if (!match) return null;\n const slug = match[1];\n if (!slug || !isValidPreviewTenantSlug(slug)) return null;\n return slug;\n}\n\n/**\n * Resolve either a standard tenant slug or a registered preview slug from a host.\n * Does not check Management DB — only host shape.\n */\nexport function anyTenantSlugFromHost(host: string): string | null {\n return tenantSlugFromHost(host) ?? previewSlugFromHost(host);\n}\n"]}
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@columnus/shared",
3
- "version": "0.1.63",
3
+ "version": "0.2.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Columnus shared package (CMS install family)",
7
7
  "license": "MIT",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "git+https://github.com/movement-overdrive/convex-studio.git",
10
+ "url": "git+https://github.com/columnus-dev/columnus.git",
11
11
  "directory": "packages/@columnus/shared"
12
12
  },
13
- "homepage": "https://github.com/movement-overdrive/convex-studio/tree/main/packages/@columnus/shared",
13
+ "homepage": "https://github.com/columnus-dev/columnus/tree/main/packages/@columnus/shared",
14
14
  "bugs": {
15
- "url": "https://github.com/movement-overdrive/convex-studio/issues"
15
+ "url": "https://github.com/columnus-dev/columnus/issues"
16
16
  },
17
17
  "files": [
18
18
  "dist"
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Movement Overdrive / Columnus
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.