certmate-cli 0.1.5__tar.gz → 0.1.6__tar.gz

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.
@@ -119,3 +119,5 @@ UI_REDESIGN_PLAN.md
119
119
  # Local nginx reverse-proxy config: copy nginx.conf.example to nginx.conf and
120
120
  # fill in your domain. The copy is yours; the example is tracked (#576).
121
121
  nginx.conf
122
+ tls-erros.md
123
+ .mypy_cache/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: certmate-cli
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: CertMate command-line interface — the SSL certificate lifecycle from your terminal
5
5
  Project-URL: Homepage, https://github.com/fabriziosalmi/certmate
6
6
  Project-URL: Source, https://github.com/fabriziosalmi/certmate/tree/main/clients/certmate-cli
@@ -23,7 +23,7 @@ Classifier: Topic :: Security
23
23
  Classifier: Topic :: System :: Systems Administration
24
24
  Classifier: Topic :: Utilities
25
25
  Requires-Python: >=3.9
26
- Requires-Dist: certmate-sdk>=0.1.5
26
+ Requires-Dist: certmate-sdk>=0.1.6
27
27
  Requires-Dist: rich>=13
28
28
  Requires-Dist: typer>=0.9
29
29
  Description-Content-Type: text/markdown
@@ -34,7 +34,7 @@ The [CertMate](https://github.com/fabriziosalmi/certmate) SSL certificate
34
34
  lifecycle from your terminal — built on `certmate-sdk`.
35
35
 
36
36
  ```bash
37
- pip install certmate-cli
37
+ pip install certmate-cli # or, on macOS/Linux: brew install fabriziosalmi/certmate/certmate-cli
38
38
  export CERTMATE_URL=http://localhost:8000
39
39
  export CERTMATE_TOKEN=...
40
40
 
@@ -43,9 +43,16 @@ certmate cert ls
43
43
  certmate cert info app.example.com
44
44
  certmate cert renew app.example.com --force
45
45
  certmate cert create app.example.com --dns cloudflare --dry-run
46
+ certmate cert create app.example.com --name internal.example.com --dns cloudflare
46
47
  certmate audit verify
47
48
  ```
48
49
 
50
+ `--name` names a second certificate for a domain apart from it: the certificate
51
+ is issued for `app.example.com`, and every later command takes
52
+ `internal.example.com`. The server must be on API contract 2.61 or later; an
53
+ older one would ignore the name, so the CLI asks first and refuses, and
54
+ `--dry-run` says whether the server takes it.
55
+
49
56
  ## Pulling certificates onto a host
50
57
 
51
58
  `cert download` fetches one file at a time, so a target server can pull
@@ -4,7 +4,7 @@ The [CertMate](https://github.com/fabriziosalmi/certmate) SSL certificate
4
4
  lifecycle from your terminal — built on `certmate-sdk`.
5
5
 
6
6
  ```bash
7
- pip install certmate-cli
7
+ pip install certmate-cli # or, on macOS/Linux: brew install fabriziosalmi/certmate/certmate-cli
8
8
  export CERTMATE_URL=http://localhost:8000
9
9
  export CERTMATE_TOKEN=...
10
10
 
@@ -13,9 +13,16 @@ certmate cert ls
13
13
  certmate cert info app.example.com
14
14
  certmate cert renew app.example.com --force
15
15
  certmate cert create app.example.com --dns cloudflare --dry-run
16
+ certmate cert create app.example.com --name internal.example.com --dns cloudflare
16
17
  certmate audit verify
17
18
  ```
18
19
 
20
+ `--name` names a second certificate for a domain apart from it: the certificate
21
+ is issued for `app.example.com`, and every later command takes
22
+ `internal.example.com`. The server must be on API contract 2.61 or later; an
23
+ older one would ignore the name, so the CLI asks first and refuses, and
24
+ `--dry-run` says whether the server takes it.
25
+
19
26
  ## Pulling certificates onto a host
20
27
 
21
28
  `cert download` fetches one file at a time, so a target server can pull
@@ -1,2 +1,2 @@
1
1
  """certmate-cli — the CertMate SSL lifecycle from your terminal (built on certmate-sdk)."""
2
- __version__ = "0.1.5"
2
+ __version__ = "0.1.6"
@@ -21,7 +21,7 @@ import typer
21
21
  from rich.console import Console
22
22
  from rich.table import Table
23
23
 
24
- from certmate import Client, CertMateError, Job
24
+ from certmate import CertMateError, Client, Job, UnsupportedByServer
25
25
 
26
26
  app = typer.Typer(no_args_is_help=True, add_completion=False,
27
27
  help="CertMate — the SSL certificate lifecycle from your terminal.")
@@ -207,6 +207,10 @@ def cert_info(ctx: typer.Context, domain: str):
207
207
  """Show a certificate's details."""
208
208
  c = _run(lambda: _client(ctx).get_certificate(domain))
209
209
  out.print(f"[bold]{c.domain}[/]")
210
+ if c.host and c.host != c.domain:
211
+ # Where it is served, when that is not its name: an IP certificate's
212
+ # address, or the host a certificate named apart from it is for.
213
+ out.print(f" host: {c.host}")
210
214
  if c.has_expired() is True:
211
215
  remaining = "[red]expired[/]"
212
216
  elif c.days_until_expiry is None:
@@ -233,6 +237,12 @@ def cert_create(
233
237
  dns: Optional[str] = typer.Option(None, "--dns", help="DNS provider (e.g. cloudflare)."),
234
238
  ca: Optional[str] = typer.Option(None, "--ca", help="CA provider (e.g. letsencrypt)."),
235
239
  san: Optional[str] = typer.Option(None, "--san", help="Comma-separated SAN domains."),
240
+ # A str with an empty default, not Optional: typer reads the annotation at
241
+ # runtime, and `str | None` is not one on Python 3.9.
242
+ name: str = typer.Option(
243
+ "", "--name",
244
+ help="Name the certificate apart from its domain, for a second certificate for it "
245
+ "(server API contract 2.61 or later)."),
236
246
  wait: bool = typer.Option(True, "--wait/--no-wait", help="Wait for issuance to finish."),
237
247
  dry_run: bool = typer.Option(False, "--dry-run",
238
248
  help="Validate inputs and preflight the DNS provider WITHOUT issuing."),
@@ -242,6 +252,8 @@ def cert_create(
242
252
  # bogus "" SAN entry — mirrors cert_reissue.
243
253
  sans: List[str] = [s.strip() for s in san.split(",") if s.strip()] if san else []
244
254
  client = _client(ctx)
255
+ # What it is issued as: the domain, and the name it is given apart from it.
256
+ issued_as = f"{domain} as {name}" if name else domain
245
257
 
246
258
  if dry_run:
247
259
  problems = []
@@ -250,6 +262,16 @@ def cert_create(
250
262
  for s in sans:
251
263
  if not _DOMAIN_RE.match(s):
252
264
  problems.append(f"SAN {s!r} does not look valid")
265
+ if name:
266
+ # The server's rule (#1149): the shape of a DNS name, never a wildcard.
267
+ if not _DOMAIN_RE.match(name) or name.startswith("*."):
268
+ problems.append(f"name {name!r} is not a DNS name")
269
+ try:
270
+ client.require_contract("2.61", "name")
271
+ except UnsupportedByServer as e:
272
+ problems.append(str(e))
273
+ except CertMateError as e:
274
+ _die(str(e))
253
275
  if dns:
254
276
  res = _run(lambda: client.test_dns_provider(dns))
255
277
  ok = bool(res.get("success", res.get("ok", True)))
@@ -261,23 +283,23 @@ def cert_create(
261
283
  out.print("[dim]No --dns given; skipping provider preflight.[/]")
262
284
  if problems:
263
285
  _die("dry-run found issues:\n - " + "\n - ".join(problems))
264
- out.print(f"[green]dry-run OK[/] — would issue [bold]{domain}[/]"
286
+ out.print(f"[green]dry-run OK[/] — would issue [bold]{issued_as}[/]"
265
287
  f"{(' (+' + str(len(sans)) + ' SAN)') if sans else ''}"
266
288
  f"{(' via ' + dns) if dns else ''}{(' on ' + ca) if ca else ''}. Nothing issued.")
267
289
  return
268
290
 
269
291
  def _issue():
270
- with out.status(f"Issuing [bold]{domain}[/] …") as status:
292
+ with out.status(f"Issuing [bold]{issued_as}[/] …") as status:
271
293
  def _progress(j: Job):
272
294
  if j.status:
273
- status.update(f"Issuing [bold]{domain}[/] … [{j.status}]")
295
+ status.update(f"Issuing [bold]{issued_as}[/] … [{j.status}]")
274
296
  return client.create_certificate(
275
297
  domain, dns_provider=dns, ca_provider=ca, san_domains=sans or None,
276
- wait=wait, on_progress=_progress)
298
+ name=name or None, wait=wait, on_progress=_progress)
277
299
 
278
300
  job = _run(_issue)
279
301
  if wait:
280
- out.print(f"[green]issued[/] [bold]{domain}[/]"
302
+ out.print(f"[green]issued[/] [bold]{issued_as}[/]"
281
303
  f"{(' (' + job.status + ')') if job.status else ''}.")
282
304
  else:
283
305
  out.print(f"[yellow]accepted[/] — job [bold]{job.job_id}[/] "
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "certmate-cli"
7
- version = "0.1.5"
7
+ version = "0.1.6"
8
8
  description = "CertMate command-line interface — the SSL certificate lifecycle from your terminal"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -33,10 +33,10 @@ classifiers = [
33
33
  # The SDK floor tracks the CLI release, and now something enforces that:
34
34
  # tests/test_version_consistency.py fails if this floor falls behind the SDK
35
35
  # in the tree. It had already drifted once, sitting at 0.1.3 while both
36
- # packages were 0.1.4. 0.1.5 is needed for Certificate.has_expired(), which
37
- # `cert ls` and `cert info` call on every row.
36
+ # packages were 0.1.4. 0.1.6 is needed for Client.require_contract(), which
37
+ # `cert create --name --dry-run` calls, and create_certificate(name=...).
38
38
  dependencies = [
39
- "certmate-sdk>=0.1.5",
39
+ "certmate-sdk>=0.1.6",
40
40
  "typer>=0.9",
41
41
  "rich>=13",
42
42
  ]