// Standalone JS port of deriveTenantName for offline verification. // Mirror lib/tenant-naming.ts byte-for-byte logic. const MAX_NAMESPACE_LEN = 63; const NAMESPACE_PREFIX = "tenant-"; const MAX_TENANT_NAME_LEN = MAX_NAMESPACE_LEN - NAMESPACE_PREFIX.length; const SUFFIX_HEX_LEN = 8; const SUFFIX_TOTAL_LEN = SUFFIX_HEX_LEN + 1; const MAX_SLUG_LEN = MAX_TENANT_NAME_LEN - SUFFIX_TOTAL_LEN; function slugify(input) { return input .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); } function requestIdSuffix(requestId) { const hex = requestId.replace(/-/g, "").toLowerCase(); if (!/^[0-9a-f]{8}/.test(hex)) { throw new Error(`Invalid request id: ${requestId}`); } return hex.slice(0, SUFFIX_HEX_LEN); } function deriveTenantName(kind, companyName, requestId) { const suffix = requestIdSuffix(requestId); if (kind === "personal") return `p-${suffix}`; const rawSlug = slugify(companyName); const slug = rawSlug.slice(0, MAX_SLUG_LEN).replace(/-+$/, ""); if (!slug) return `t-${suffix}`; return `${slug}-${suffix}`; } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- const cases = [ // [kind, companyName, requestId, expected, note] ["company", "Acme GmbH", "abc12345-1234-1234-1234-123456789abc", "acme-gmbh-abc12345", "basic company"], ["company", "Müller AG", "abc12345-aaaa", "m-ller-ag-abc12345", "umlaut → '-'"], ["company", "!!!", "abc12345-aaaa", "t-abc12345", "no alnum → 't-' fallback"], ["personal", "irrelevant", "abc12345-aaaa", "p-abc12345", "personal ignores companyName"], ["personal", "", "abc12345-aaaa", "p-abc12345", "personal with empty companyName"], ["company", " Trim Me ", "abc12345-aaaa", "trim-me-abc12345", "leading/trailing whitespace"], ["company", "Foo---Bar", "abc12345-aaaa", "foo-bar-abc12345", "consecutive hyphens collapse"], ["company", "A very long company name that absolutely will exceed the slug limit easily", "abc12345-aaaa", null, "must be <= 56 chars"], ["company", "----", "abc12345-aaaa", "t-abc12345", "all-hyphen → fallback"], ["company", "ACME", "ABCDEF12-...", "acme-abcdef12", "uppercase UUID is lowercased"], ]; let pass = 0, fail = 0; for (const [kind, name, id, expected, note] of cases) { let got; let err = null; try { got = deriveTenantName(kind, name, id); } catch (e) { err = e.message; } // Special length-only cases if (expected === null) { const ok = got && got.length <= 56; console.log(`${ok ? "PASS" : "FAIL"} len(${got}) = ${got?.length} [${note}]`); if (ok) pass++; else fail++; continue; } if (err) { console.log(`THROW ${err} [${note}]`); if (expected === "throw") pass++; else fail++; continue; } const ok = got === expected; console.log(`${ok ? "PASS" : "FAIL"} got=${got} want=${expected} [${note}]`); if (ok) pass++; else fail++; } // Should-throw cases console.log("\nThrow cases:"); const throwCases = [ ["company", "Acme", "", "empty requestId"], ["company", "Acme", "xyz", "non-hex requestId"], ["company", "Acme", "1234567", "too short (7 chars)"], ]; for (const [kind, name, id, note] of throwCases) { let threw = false; try { deriveTenantName(kind, name, id); } catch { threw = true; } console.log(`${threw ? "PASS" : "FAIL"} threw=${threw} [${note}]`); if (threw) pass++; else fail++; } console.log(`\n${pass} pass, ${fail} fail`); process.exit(fail === 0 ? 0 : 1);