shiply-cli 0.14.4 → 0.14.5

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/domain.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { api, resolveBase } from './publish.js';
2
+ import { PSL_RULES } from './psl.data.js';
2
3
  const headers = (apiKey) => ({ 'content-type': 'application/json', authorization: `Bearer ${apiKey}` });
3
4
  const enc = (d) => encodeURIComponent(d);
4
5
  export async function domainLs(ctx) {
@@ -24,40 +25,72 @@ export async function domainConnect(ctx, domain) {
24
25
  const r = await api(`${base}/api/v1/custom-domains/${enc(domain)}/connect`, { method: 'POST', headers: headers(ctx.apiKey), body: '{}' });
25
26
  console.log(`Open this link in your browser to connect ${domain}:\n ${r.url}`);
26
27
  }
27
- /** Common 2-label public suffixes. Not exhaustive but covers the registrar
28
- * ccTLDs we've seen in the wild (au, uk, nz, za, br, in, jp, kr, mx, co...).
29
- * For anything else we fall back to the last-two-labels heuristic, which is
30
- * correct for the vast majority of TLDs. A future improvement is to ship the
31
- * Public Suffix List as data; this keeps the CLI bundle tiny. */
32
- const TWO_LABEL_SUFFIXES = new Set([
33
- 'com.au', 'net.au', 'org.au', 'edu.au', 'gov.au', 'id.au',
34
- 'co.uk', 'org.uk', 'me.uk', 'gov.uk', 'ac.uk', 'net.uk',
35
- 'co.nz', 'net.nz', 'org.nz', 'govt.nz',
36
- 'co.za', 'org.za', 'web.za',
37
- 'com.br', 'net.br', 'org.br', 'gov.br',
38
- 'co.in', 'net.in', 'org.in', 'gov.in',
39
- 'co.jp', 'or.jp', 'ne.jp', 'go.jp', 'ac.jp',
40
- 'co.kr', 'or.kr', 'ne.kr', 'go.kr',
41
- 'com.mx', 'org.mx', 'gob.mx',
42
- 'co.id', 'or.id', 'ac.id', 'go.id',
43
- 'com.sg', 'org.sg', 'edu.sg', 'gov.sg',
44
- 'com.hk', 'org.hk', 'edu.hk', 'gov.hk',
45
- 'com.tw', 'org.tw', 'gov.tw',
46
- ]);
47
- /** Split hostname into (parent, subdomain) using a small Public Suffix List.
48
- * parts.slice(-2).join('.') is wrong for `paidsooner.com.au` (treats `com.au`
49
- * as the parent — that bug shipped phantom state to prod). */
28
+ /** Index the PSL into three buckets for O(1) lookup. Exception rules ('!foo')
29
+ * win over wildcard rules ('*.foo') which win over plain rules ('foo'). */
30
+ const PSL_EXACT = new Set();
31
+ const PSL_WILDCARD = new Set();
32
+ const PSL_EXCEPTION = new Set();
33
+ for (const rule of PSL_RULES) {
34
+ if (rule.startsWith('!'))
35
+ PSL_EXCEPTION.add(rule.slice(1));
36
+ else if (rule.startsWith('*.'))
37
+ PSL_WILDCARD.add(rule.slice(2));
38
+ else
39
+ PSL_EXACT.add(rule);
40
+ }
41
+ /** Find the longest matching public suffix for a hostname, applying the full
42
+ * PSL algorithm: exception rules win, then wildcards, then longest exact
43
+ * match. Returns the suffix as a dotted string (e.g. 'com.au', 'co.uk').
44
+ * Returns null if no rule matches (hostname has only a single label, or it's
45
+ * IDN/punycode outside the ICANN list). */
46
+ export function findPublicSuffix(hostname) {
47
+ const labels = hostname.toLowerCase().split('.');
48
+ // Walk from most-specific (full hostname) to least-specific (TLD).
49
+ for (let i = 0; i < labels.length; i++) {
50
+ const candidate = labels.slice(i).join('.');
51
+ // 1. Exception rule wins → suffix is candidate minus its first label.
52
+ if (PSL_EXCEPTION.has(candidate)) {
53
+ return labels.slice(i + 1).join('.') || null;
54
+ }
55
+ }
56
+ for (let i = 0; i < labels.length; i++) {
57
+ const candidate = labels.slice(i).join('.');
58
+ // 2. Exact match.
59
+ if (PSL_EXACT.has(candidate))
60
+ return candidate;
61
+ // 3. Wildcard: parent of candidate (everything after candidate's first
62
+ // label) is in the wildcard set.
63
+ if (i + 1 < labels.length) {
64
+ const parent = labels.slice(i + 1).join('.');
65
+ if (PSL_WILDCARD.has(parent))
66
+ return candidate;
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+ /** Split hostname into (registrable domain, subdomain) using the full PSL.
72
+ * The registrable domain is the public suffix plus one extra label.
73
+ * E.g. `www.example.co.uk` → suffix `co.uk`, domain `example.co.uk`, sub `www`.
74
+ * `foo.city.kawasaki.jp` → exception `!city.kawasaki.jp` makes the suffix
75
+ * `kawasaki.jp`, so domain is `city.kawasaki.jp` and sub is `foo`.
76
+ * Falls back to the last-two-labels heuristic if the hostname is below the
77
+ * PSL coverage (no matching rule). */
50
78
  export function splitRegistrable(hostname) {
51
- const parts = hostname.toLowerCase().split('.');
52
- // Try a 2-label public suffix first (e.g. com.au, co.uk).
53
- if (parts.length >= 3) {
54
- const twoLabel = parts.slice(-2).join('.');
55
- if (TWO_LABEL_SUFFIXES.has(twoLabel)) {
56
- const domain = parts.slice(-3).join('.');
57
- const subdomain = parts.slice(0, -3).join('.') || '@';
58
- return { domain, subdomain };
79
+ const host = hostname.toLowerCase().replace(/\.$/, '');
80
+ const parts = host.split('.');
81
+ const suffix = findPublicSuffix(host);
82
+ if (suffix) {
83
+ const suffixLabels = suffix.split('.').length;
84
+ // Registrable domain = public suffix + one more label.
85
+ if (parts.length <= suffixLabels) {
86
+ // Hostname IS the public suffix — not registrable on its own.
87
+ return { domain: host, subdomain: '@' };
59
88
  }
89
+ const domain = parts.slice(-(suffixLabels + 1)).join('.');
90
+ const subdomain = parts.slice(0, -(suffixLabels + 1)).join('.') || '@';
91
+ return { domain, subdomain };
60
92
  }
93
+ // No PSL hit — fall back to last-two-labels.
61
94
  const domain = parts.slice(-2).join('.');
62
95
  const subdomain = parts.slice(0, -2).join('.') || '@';
63
96
  return { domain, subdomain };