Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers, document generators, FCC compliance tools, Canada CRTC formation, Ansible infrastructure, and deployment scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions
260
scripts/workers/services/porkbun.py
Normal file
260
scripts/workers/services/porkbun.py
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
"""
|
||||
Porkbun .ca domain registration service.
|
||||
|
||||
Production client for registering .ca domains via the Porkbun REST API.
|
||||
Called by the CRTC pipeline to register the client's .ca domain.
|
||||
|
||||
WHOIS Contact Setup (Porkbun account-level — set once in dashboard):
|
||||
Technical contact: Performance West Inc. (filings@performancewest.net)
|
||||
Billing contact: Performance West Inc. (admin@performancewest.net)
|
||||
Admin/Registrant: Set per-domain to the customer's order email
|
||||
|
||||
NOTE: Porkbun v3 API does not support per-domain contact customization.
|
||||
All domains inherit the account-level default contacts. The account
|
||||
should be configured in the Porkbun dashboard with PW as tech/billing
|
||||
and a generic admin contact. Post-registration, the customer email
|
||||
is set via the CIRA .ca registrant update process (manual for now).
|
||||
|
||||
WHOIS Privacy:
|
||||
Porkbun includes free WHOIS privacy for most TLDs. For .ca domains,
|
||||
CIRA (the .ca registry) requires the registrant organization name to
|
||||
be publicly visible regardless of privacy settings. Individual contact
|
||||
details (name, address, phone, email) can be hidden.
|
||||
|
||||
Environment variables:
|
||||
PORKBUN_API_KEY pk1_...
|
||||
PORKBUN_SECRET_KEY sk1_...
|
||||
|
||||
Usage:
|
||||
from scripts.workers.services.porkbun import register_ca_domain
|
||||
result = register_ca_domain("testcompany.ca", order_number="CA-2026-XXXXX")
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
|
||||
LOG = logging.getLogger("workers.porkbun")
|
||||
|
||||
API_BASE = "https://api.porkbun.com/api/json/v3"
|
||||
|
||||
_api_key = os.environ.get("PORKBUN_API_KEY", "")
|
||||
_secret_key = os.environ.get("PORKBUN_SECRET_KEY", "")
|
||||
|
||||
|
||||
def _auth(**extra) -> dict:
|
||||
return {"apikey": _api_key, "secretapikey": _secret_key, **extra}
|
||||
|
||||
|
||||
def _post(path: str, **extra) -> dict:
|
||||
r = requests.post(f"{API_BASE}/{path}", json=_auth(**extra), timeout=30)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def ping() -> bool:
|
||||
"""Verify API credentials."""
|
||||
try:
|
||||
data = _post("ping")
|
||||
ok = data.get("status") == "SUCCESS"
|
||||
LOG.info("Porkbun ping: %s (IP: %s)", "OK" if ok else "FAIL", data.get("yourIp"))
|
||||
return ok
|
||||
except Exception as e:
|
||||
LOG.error("Porkbun ping failed: %s", e)
|
||||
return False
|
||||
|
||||
|
||||
def check_availability_whois(domain: str) -> bool:
|
||||
"""Check .ca domain availability via CIRA WHOIS (Porkbun API doesn't support this)."""
|
||||
try:
|
||||
s = socket.create_connection(("whois.cira.ca", 43), timeout=10)
|
||||
s.sendall(f"{domain}\r\n".encode())
|
||||
response = b""
|
||||
while True:
|
||||
chunk = s.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
response += chunk
|
||||
s.close()
|
||||
text = response.decode("utf-8", errors="ignore")
|
||||
available = "Not found" in text
|
||||
LOG.info("WHOIS %s: %s", domain, "AVAILABLE" if available else "TAKEN")
|
||||
return available
|
||||
except Exception as e:
|
||||
LOG.error("WHOIS check failed for %s: %s", domain, e)
|
||||
return False
|
||||
|
||||
|
||||
def register_domain(
|
||||
domain: str,
|
||||
nameservers: Optional[list[str]] = None,
|
||||
years: int = 1,
|
||||
) -> dict:
|
||||
"""
|
||||
Register a .ca domain via Porkbun.
|
||||
|
||||
Args:
|
||||
domain: Full domain name (e.g. "mycompany.ca")
|
||||
nameservers: Custom NS records. Defaults to HestiaCP NS.
|
||||
years: Registration period (default 1 year)
|
||||
|
||||
Returns:
|
||||
{"success": bool, "domain": str, "error": str}
|
||||
"""
|
||||
# Default to HestiaCP nameservers
|
||||
if not nameservers:
|
||||
nameservers = [
|
||||
"ns0.cp.carrierone.com",
|
||||
"ns1.he.net",
|
||||
"ns2.he.net",
|
||||
"ns3.he.net",
|
||||
"ns4.he.net",
|
||||
"ns5.he.net",
|
||||
]
|
||||
|
||||
try:
|
||||
body = _auth(domain=domain, years=years)
|
||||
if nameservers:
|
||||
body["ns"] = nameservers
|
||||
|
||||
r = requests.post(
|
||||
f"{API_BASE}/domain/register/{domain}",
|
||||
json=body,
|
||||
timeout=60,
|
||||
)
|
||||
data = r.json()
|
||||
|
||||
if data.get("status") == "SUCCESS":
|
||||
LOG.info("Porkbun: registered %s for %d year(s)", domain, years)
|
||||
return {"success": True, "domain": domain}
|
||||
else:
|
||||
msg = data.get("message", str(data))
|
||||
LOG.error("Porkbun registration failed for %s: %s", domain, msg)
|
||||
return {"success": False, "domain": domain, "error": msg}
|
||||
|
||||
except Exception as e:
|
||||
LOG.error("Porkbun registration error for %s: %s", domain, e)
|
||||
return {"success": False, "domain": domain, "error": str(e)}
|
||||
|
||||
|
||||
def set_whois_privacy(domain: str, enabled: bool = True) -> dict:
|
||||
"""
|
||||
Enable or disable WHOIS privacy for a domain.
|
||||
|
||||
For .ca domains: CIRA requires the registrant organization name to remain
|
||||
publicly visible. WHOIS privacy only hides individual contact details
|
||||
(name, address, phone, email).
|
||||
|
||||
Porkbun v3 API doesn't have a dedicated WHOIS privacy endpoint, but
|
||||
domains registered under our account get free WHOIS privacy by default.
|
||||
This function is a placeholder for when the API supports it, or for
|
||||
toggling via the Porkbun dashboard.
|
||||
"""
|
||||
LOG.info("WHOIS privacy for %s: %s", domain, "enabled" if enabled else "disabled")
|
||||
# Porkbun includes free WHOIS privacy by default.
|
||||
# No API endpoint to toggle it — must be done in Porkbun dashboard if needed.
|
||||
# For .ca: CIRA publishes org name regardless.
|
||||
return {"success": True, "domain": domain, "privacy": enabled}
|
||||
|
||||
|
||||
def register_ca_domain(
|
||||
domain: str,
|
||||
order_number: str = "",
|
||||
skip_whois: bool = False,
|
||||
domain_privacy: bool = True,
|
||||
customer_email: str = "",
|
||||
) -> dict:
|
||||
"""
|
||||
Full .ca domain registration workflow:
|
||||
1. WHOIS availability check
|
||||
2. Porkbun registration with HestiaCP nameservers
|
||||
3. Set WHOIS privacy preference
|
||||
4. Return result
|
||||
|
||||
WHOIS contact roles:
|
||||
Technical + Billing: Performance West (set at Porkbun account level)
|
||||
Admin/Registrant: Customer email (set post-registration via CIRA if needed)
|
||||
|
||||
Args:
|
||||
domain: The .ca domain to register
|
||||
order_number: For logging context
|
||||
skip_whois: Skip availability check (if already verified)
|
||||
domain_privacy: Whether to enable WHOIS privacy (default True)
|
||||
customer_email: Customer's email for CIRA registrant contact
|
||||
|
||||
Returns:
|
||||
{"success": bool, "domain": str, "error": str}
|
||||
"""
|
||||
LOG.info("[%s] Registering .ca domain: %s (privacy=%s, customer=%s)",
|
||||
order_number, domain, domain_privacy, customer_email or "account default")
|
||||
|
||||
# Validate it's a .ca domain
|
||||
if not domain.endswith(".ca"):
|
||||
return {"success": False, "domain": domain, "error": "Not a .ca domain"}
|
||||
|
||||
# Clean the domain
|
||||
domain = domain.lower().strip()
|
||||
|
||||
# Step 1: Check availability
|
||||
if not skip_whois:
|
||||
if not check_availability_whois(domain):
|
||||
return {"success": False, "domain": domain, "error": "Domain is not available"}
|
||||
|
||||
# Step 2: Register
|
||||
result = register_domain(domain)
|
||||
|
||||
if result["success"]:
|
||||
# Step 3: Set WHOIS privacy preference
|
||||
set_whois_privacy(domain, enabled=domain_privacy)
|
||||
|
||||
# Step 4: Log customer email for CIRA registrant update
|
||||
# NOTE: Porkbun API doesn't support per-domain contact changes.
|
||||
# The registrant contact defaults to the Porkbun account owner (PW).
|
||||
# For .ca domains, the customer should be the admin contact — this
|
||||
# requires a manual CIRA registrant transfer or a support ticket to
|
||||
# Porkbun to update the admin contact to the customer's email.
|
||||
if customer_email:
|
||||
LOG.info(
|
||||
"[%s] Customer email for CIRA admin contact: %s "
|
||||
"(manual Porkbun support ticket needed to set per-domain admin)",
|
||||
order_number, customer_email,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def generate_domain_name(company_name: str, bc_number: str = "") -> str:
|
||||
"""
|
||||
Generate a .ca domain name from a company name.
|
||||
|
||||
For numbered companies: uses a slugified version of the trade name or
|
||||
falls back to "pw-{bc_number}.ca"
|
||||
|
||||
For named companies: slugifies the company name, removes legal endings.
|
||||
"""
|
||||
if not company_name or company_name.lower().startswith("numbered"):
|
||||
if bc_number:
|
||||
return f"pw-{bc_number.lower()}.ca"
|
||||
return ""
|
||||
|
||||
# Remove legal endings
|
||||
name = re.sub(
|
||||
r"\s+(ltd\.?|inc\.?|corp\.?|llc\.?|co\.?|limited|incorporated|corporation)\s*$",
|
||||
"",
|
||||
company_name,
|
||||
flags=re.IGNORECASE,
|
||||
).strip()
|
||||
|
||||
# Slugify
|
||||
slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
|
||||
|
||||
# Truncate to 63 chars (DNS label limit)
|
||||
if len(slug) > 59: # leave room for ".ca"
|
||||
slug = slug[:59].rstrip("-")
|
||||
|
||||
return f"{slug}.ca" if slug else ""
|
||||
Loading…
Add table
Add a link
Reference in a new issue