@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.
package/dist/bffHost.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Trusted request host headers for Columnus BFF tenant resolution (D35/D41).
3
3
  * Prefer Forwarded / X-Forwarded-Host when present, else Host.
4
4
  * Always trust these headers in the web/BFF process — operators must not expose
5
- * the container publicly without Traefik/Coolify/tunnel in front.
5
+ * the container publicly without Traefik/tunnel in front.
6
6
  */
7
7
  export type RequestHostHeaders = {
8
8
  /** RFC 7239 `Forwarded` header value. */
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tenantHosts.ts","../src/bffHost.ts"],"names":[],"mappings":"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","file":"bffHost.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","import { hostnameFromHostHeader } from \"./tenantHosts.ts\";\n\n/**\n * Trusted request host headers for Columnus BFF tenant resolution (D35/D41).\n * Prefer Forwarded / X-Forwarded-Host when present, else Host.\n * Always trust these headers in the web/BFF process — operators must not expose\n * the container publicly without Traefik/Coolify/tunnel in front.\n */\nexport type RequestHostHeaders = {\n /** RFC 7239 `Forwarded` header value. */\n forwarded?: string | null;\n /** `X-Forwarded-Host` header value. */\n xForwardedHost?: string | null;\n /** `Host` header value. */\n host?: string | null;\n};\n\nexport type TrustedHostResult = { ok: true; hostname: string } | { ok: false; error: string };\n\n/**\n * Extract a single host value from a header that may be multi-valued.\n * Returns null when empty; rejects comma-separated lists as ambiguous.\n */\nexport function singleHeaderHostValue(raw: string | null | undefined): TrustedHostResult | null {\n if (raw == null) return null;\n const trimmed = raw.trim();\n if (!trimmed) return null;\n if (trimmed.includes(\",\")) {\n return { ok: false, error: \"ambiguous multi-value host header\" };\n }\n const hostname = hostnameFromHostHeader(trimmed);\n if (!hostname) return { ok: false, error: \"empty hostname\" };\n return { ok: true, hostname };\n}\n\n/**\n * Parse the `host=` parameter from an RFC 7239 Forwarded header entry.\n * Quoted values are unescaped; ports are stripped via hostnameFromHostHeader.\n */\nexport function hostFromForwardedHeader(forwarded: string): TrustedHostResult | null {\n const trimmed = forwarded.trim();\n if (!trimmed) return null;\n\n const entries = trimmed.split(\",\");\n const hosts: string[] = [];\n for (const entry of entries) {\n const host = hostParamFromForwardedEntry(entry);\n if (host) hosts.push(host);\n }\n if (hosts.length === 0) return null;\n const unique = new Set(hosts.map((h) => hostnameFromHostHeader(h)).filter(Boolean));\n if (unique.size > 1) {\n return { ok: false, error: \"ambiguous multi-value Forwarded host\" };\n }\n const [hostname] = unique;\n if (!hostname) return { ok: false, error: \"empty hostname\" };\n return { ok: true, hostname };\n}\n\nfunction hostParamFromForwardedEntry(entry: string): string | null {\n // Parameters are `;`-separated; values may be quoted.\n for (const part of entry.split(\";\")) {\n const eq = part.indexOf(\"=\");\n if (eq === -1) continue;\n const key = part.slice(0, eq).trim().toLowerCase();\n if (key !== \"host\") continue;\n let value = part.slice(eq + 1).trim();\n if (value.startsWith('\"') && value.endsWith('\"') && value.length >= 2) {\n value = value.slice(1, -1).replace(/\\\\(.)/g, \"$1\");\n }\n return value || null;\n }\n return null;\n}\n\n/**\n * Resolve the trusted request hostname for tenant identity (D3/D35/D41).\n * Never accepts a client-supplied tenant slug — only host headers.\n */\nexport function extractTrustedHostname(headers: RequestHostHeaders): TrustedHostResult {\n const fromForwarded =\n headers.forwarded != null && headers.forwarded.trim() !== \"\"\n ? hostFromForwardedHeader(headers.forwarded)\n : null;\n if (fromForwarded) return fromForwarded;\n\n const fromXfh = singleHeaderHostValue(headers.xForwardedHost);\n if (fromXfh) return fromXfh;\n\n const fromHost = singleHeaderHostValue(headers.host);\n if (fromHost) return fromHost;\n\n return { ok: false, error: \"missing Host header\" };\n}\n"]}
1
+ {"version":3,"sources":["../src/tenantHosts.ts","../src/bffHost.ts"],"names":[],"mappings":"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","file":"bffHost.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","import { hostnameFromHostHeader } from \"./tenantHosts.ts\";\n\n/**\n * Trusted request host headers for Columnus BFF tenant resolution (D35/D41).\n * Prefer Forwarded / X-Forwarded-Host when present, else Host.\n * Always trust these headers in the web/BFF process — operators must not expose\n * the container publicly without Traefik/tunnel in front.\n */\nexport type RequestHostHeaders = {\n /** RFC 7239 `Forwarded` header value. */\n forwarded?: string | null;\n /** `X-Forwarded-Host` header value. */\n xForwardedHost?: string | null;\n /** `Host` header value. */\n host?: string | null;\n};\n\nexport type TrustedHostResult = { ok: true; hostname: string } | { ok: false; error: string };\n\n/**\n * Extract a single host value from a header that may be multi-valued.\n * Returns null when empty; rejects comma-separated lists as ambiguous.\n */\nexport function singleHeaderHostValue(raw: string | null | undefined): TrustedHostResult | null {\n if (raw == null) return null;\n const trimmed = raw.trim();\n if (!trimmed) return null;\n if (trimmed.includes(\",\")) {\n return { ok: false, error: \"ambiguous multi-value host header\" };\n }\n const hostname = hostnameFromHostHeader(trimmed);\n if (!hostname) return { ok: false, error: \"empty hostname\" };\n return { ok: true, hostname };\n}\n\n/**\n * Parse the `host=` parameter from an RFC 7239 Forwarded header entry.\n * Quoted values are unescaped; ports are stripped via hostnameFromHostHeader.\n */\nexport function hostFromForwardedHeader(forwarded: string): TrustedHostResult | null {\n const trimmed = forwarded.trim();\n if (!trimmed) return null;\n\n const entries = trimmed.split(\",\");\n const hosts: string[] = [];\n for (const entry of entries) {\n const host = hostParamFromForwardedEntry(entry);\n if (host) hosts.push(host);\n }\n if (hosts.length === 0) return null;\n const unique = new Set(hosts.map((h) => hostnameFromHostHeader(h)).filter(Boolean));\n if (unique.size > 1) {\n return { ok: false, error: \"ambiguous multi-value Forwarded host\" };\n }\n const [hostname] = unique;\n if (!hostname) return { ok: false, error: \"empty hostname\" };\n return { ok: true, hostname };\n}\n\nfunction hostParamFromForwardedEntry(entry: string): string | null {\n // Parameters are `;`-separated; values may be quoted.\n for (const part of entry.split(\";\")) {\n const eq = part.indexOf(\"=\");\n if (eq === -1) continue;\n const key = part.slice(0, eq).trim().toLowerCase();\n if (key !== \"host\") continue;\n let value = part.slice(eq + 1).trim();\n if (value.startsWith('\"') && value.endsWith('\"') && value.length >= 2) {\n value = value.slice(1, -1).replace(/\\\\(.)/g, \"$1\");\n }\n return value || null;\n }\n return null;\n}\n\n/**\n * Resolve the trusted request hostname for tenant identity (D3/D35/D41).\n * Never accepts a client-supplied tenant slug — only host headers.\n */\nexport function extractTrustedHostname(headers: RequestHostHeaders): TrustedHostResult {\n const fromForwarded =\n headers.forwarded != null && headers.forwarded.trim() !== \"\"\n ? hostFromForwardedHeader(headers.forwarded)\n : null;\n if (fromForwarded) return fromForwarded;\n\n const fromXfh = singleHeaderHostValue(headers.xForwardedHost);\n if (fromXfh) return fromXfh;\n\n const fromHost = singleHeaderHostValue(headers.host);\n if (fromHost) return fromHost;\n\n return { ok: false, error: \"missing Host header\" };\n}\n"]}
@@ -430,6 +430,7 @@ export type BffPreparedBodyEnvelopeFor<T extends BffPrepareOperationType> = Extr
430
430
  operation: T;
431
431
  }>;
432
432
  export declare const bffPreviewRegisterRequestSchema: z.ZodObject<{
433
+ previewName: z.ZodOptional<z.ZodString>;
433
434
  hostname: z.ZodString;
434
435
  convexUrl: z.ZodString;
435
436
  deployKey: z.ZodString;
@@ -19661,6 +19661,9 @@ function date4(params) {
19661
19661
  return _coercedDate(ZodDate, params);
19662
19662
  }
19663
19663
 
19664
+ // packages/@columnus/shared/src/previewDeployName.ts
19665
+ var PREVIEW_DEPLOY_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
19666
+
19664
19667
  // packages/@columnus/shared/src/dataTransfer.ts
19665
19668
  var COLUMNUS_DATA_TRANSFER_VERSION = 1;
19666
19669
  var MAX_TRANSFER_BYTES = 50 * 1024 * 1024;
@@ -19937,6 +19940,7 @@ var bffPreparedBodyEnvelopeSchema = external_exports.discriminatedUnion("operati
19937
19940
  })
19938
19941
  ]);
19939
19942
  var bffPreviewRegisterRequestSchema = external_exports.object({
19943
+ previewName: external_exports.string().regex(PREVIEW_DEPLOY_NAME_RE).optional(),
19940
19944
  hostname: external_exports.string().min(1),
19941
19945
  convexUrl: external_exports.string().url(),
19942
19946
  deployKey: external_exports.string().min(1),
@@ -19983,7 +19987,7 @@ var deployInternalSchemaDeployResponseSchema = external_exports.object({
19983
19987
  var deployHealthResponseSchema = external_exports.object({
19984
19988
  ok: external_exports.boolean(),
19985
19989
  commit: external_exports.string().optional(),
19986
- /** ISO-8601 Coolify/image bake time (BRU-76 preview supersede). */
19990
+ /** ISO-8601 deployment/image bake time (BRU-76 preview supersede). */
19987
19991
  deployedAt: external_exports.string().optional()
19988
19992
  });
19989
19993
  var BFF_JSON_BODY_LIMIT_BYTES = 2 * 1024 * 1024;