certmate-cli 0.1.4__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.
@@ -79,6 +79,7 @@ htmlcov/
79
79
  .pytest_cache/
80
80
  .coverage
81
81
  coverage.xml
82
+ coverage.json
82
83
  test_results.json
83
84
 
84
85
  # Local dev / debug scripts (kept on developer machines only)
@@ -114,3 +115,9 @@ theme-baseline/
114
115
  UI_DRACONIAN_ANALYSIS.md
115
116
  UI_UX_Opinioni_CertMate.pdf
116
117
  UI_REDESIGN_PLAN.md
118
+
119
+ # Local nginx reverse-proxy config: copy nginx.conf.example to nginx.conf and
120
+ # fill in your domain. The copy is yours; the example is tracked (#576).
121
+ nginx.conf
122
+ tls-erros.md
123
+ .mypy_cache/
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: certmate-cli
3
- Version: 0.1.4
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.3
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.4"
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.")
@@ -166,11 +166,31 @@ def cert_ls(ctx: typer.Context):
166
166
  table.add_column("DAYS", justify="right")
167
167
  table.add_column("CA")
168
168
  table.add_column("AUTO-RENEW")
169
- for c in sorted(certs, key=lambda x: (x.days_until_expiry if x.days_until_expiry is not None else 1 << 30)):
169
+ # Sort by seconds where the server sends them: a certificate with 23 hours
170
+ # left and one that lapsed an hour ago are both 0 or -1 in whole days, and
171
+ # would otherwise land next to each other in either order.
172
+ def _remaining(cert):
173
+ if cert.seconds_left is not None:
174
+ return cert.seconds_left
175
+ if cert.days_until_expiry is not None:
176
+ return cert.days_until_expiry * 86400
177
+ return 1 << 40
178
+
179
+ for c in sorted(certs, key=_remaining):
170
180
  days = c.days_until_expiry
171
- days_str = "-" if days is None else str(days)
172
- colour = "green"
173
- if days is not None:
181
+ expired = c.has_expired()
182
+ # "0" for a certificate with 23 hours left was read as expired by
183
+ # everything that saw it, including CertMate's own dashboard until
184
+ # server 2.32.2. Say which of the two it is instead of printing the
185
+ # number that cannot tell them apart.
186
+ if expired is True:
187
+ days_str, colour = "expired", "red"
188
+ elif days is None:
189
+ days_str, colour = "-", "green"
190
+ elif days == 0:
191
+ days_str, colour = ("<1" if expired is False else "?"), "red"
192
+ else:
193
+ days_str = str(days)
174
194
  colour = "red" if days < 7 else ("yellow" if days < 30 else "green")
175
195
  table.add_row(
176
196
  c.domain,
@@ -187,8 +207,20 @@ def cert_info(ctx: typer.Context, domain: str):
187
207
  """Show a certificate's details."""
188
208
  c = _run(lambda: _client(ctx).get_certificate(domain))
189
209
  out.print(f"[bold]{c.domain}[/]")
190
- out.print(f" expires: {c.expiry_date or '-'} "
191
- f"({c.days_until_expiry if c.days_until_expiry is not None else '?'} days)")
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}")
214
+ if c.has_expired() is True:
215
+ remaining = "[red]expired[/]"
216
+ elif c.days_until_expiry is None:
217
+ remaining = "? days"
218
+ elif c.days_until_expiry == 0:
219
+ # Under a day, and not expired, or the server is too old to say.
220
+ remaining = "less than a day" if c.has_expired() is False else "? days"
221
+ else:
222
+ remaining = f"{c.days_until_expiry} days"
223
+ out.print(f" expires: {c.expiry_date or '-'} ({remaining})")
192
224
  out.print(f" CA: {c.ca_provider or '-'}")
193
225
  out.print(f" DNS: {c.dns_provider or '-'}")
194
226
  out.print(f" auto-renew: {c.auto_renew}")
@@ -205,6 +237,12 @@ def cert_create(
205
237
  dns: Optional[str] = typer.Option(None, "--dns", help="DNS provider (e.g. cloudflare)."),
206
238
  ca: Optional[str] = typer.Option(None, "--ca", help="CA provider (e.g. letsencrypt)."),
207
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)."),
208
246
  wait: bool = typer.Option(True, "--wait/--no-wait", help="Wait for issuance to finish."),
209
247
  dry_run: bool = typer.Option(False, "--dry-run",
210
248
  help="Validate inputs and preflight the DNS provider WITHOUT issuing."),
@@ -214,6 +252,8 @@ def cert_create(
214
252
  # bogus "" SAN entry — mirrors cert_reissue.
215
253
  sans: List[str] = [s.strip() for s in san.split(",") if s.strip()] if san else []
216
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
217
257
 
218
258
  if dry_run:
219
259
  problems = []
@@ -222,6 +262,16 @@ def cert_create(
222
262
  for s in sans:
223
263
  if not _DOMAIN_RE.match(s):
224
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))
225
275
  if dns:
226
276
  res = _run(lambda: client.test_dns_provider(dns))
227
277
  ok = bool(res.get("success", res.get("ok", True)))
@@ -233,23 +283,23 @@ def cert_create(
233
283
  out.print("[dim]No --dns given; skipping provider preflight.[/]")
234
284
  if problems:
235
285
  _die("dry-run found issues:\n - " + "\n - ".join(problems))
236
- out.print(f"[green]dry-run OK[/] — would issue [bold]{domain}[/]"
286
+ out.print(f"[green]dry-run OK[/] — would issue [bold]{issued_as}[/]"
237
287
  f"{(' (+' + str(len(sans)) + ' SAN)') if sans else ''}"
238
288
  f"{(' via ' + dns) if dns else ''}{(' on ' + ca) if ca else ''}. Nothing issued.")
239
289
  return
240
290
 
241
291
  def _issue():
242
- with out.status(f"Issuing [bold]{domain}[/] …") as status:
292
+ with out.status(f"Issuing [bold]{issued_as}[/] …") as status:
243
293
  def _progress(j: Job):
244
294
  if j.status:
245
- status.update(f"Issuing [bold]{domain}[/] … [{j.status}]")
295
+ status.update(f"Issuing [bold]{issued_as}[/] … [{j.status}]")
246
296
  return client.create_certificate(
247
297
  domain, dns_provider=dns, ca_provider=ca, san_domains=sans or None,
248
- wait=wait, on_progress=_progress)
298
+ name=name or None, wait=wait, on_progress=_progress)
249
299
 
250
300
  job = _run(_issue)
251
301
  if wait:
252
- out.print(f"[green]issued[/] [bold]{domain}[/]"
302
+ out.print(f"[green]issued[/] [bold]{issued_as}[/]"
253
303
  f"{(' (' + job.status + ')') if job.status else ''}.")
254
304
  else:
255
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.4"
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"
@@ -30,10 +30,13 @@ classifiers = [
30
30
  ]
31
31
  # Built ON the SDK (never the reverse). typer for the command surface, rich
32
32
  # for tables/spinners. Still light — no server deps.
33
- # The SDK floor tracks the CLI release: 0.1.2 needs TransportError and the
34
- # renew/audit semantics introduced in certmate-sdk 0.1.2.
33
+ # The SDK floor tracks the CLI release, and now something enforces that:
34
+ # tests/test_version_consistency.py fails if this floor falls behind the SDK
35
+ # in the tree. It had already drifted once, sitting at 0.1.3 while both
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=...).
35
38
  dependencies = [
36
- "certmate-sdk>=0.1.3",
39
+ "certmate-sdk>=0.1.6",
37
40
  "typer>=0.9",
38
41
  "rich>=13",
39
42
  ]