Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,68 @@

Notable changes to `dodomain-sdk`. The import package is `dodomain`.

## 0.4.0

Parity with `@dodomain/node` 0.5.0 and 0.6.0, and with the `/v1` contract changes
those tracked. Additive apart from one rename that keeps a working alias, so
upgrading from 0.3.0 is a drop-in.

### Added

* **`TlsIssuanceAdvisory`** — why a certificate issuance for a *verified* name may
still fail. Every verify pass now reads the domain's own nameservers for a CAA
policy and a stale `_acme-challenge` record and reports what it found:
`caa_excludes_issuer`, `caa_restricts_issuance`, `stale_acme_challenge`, or
`tls_issuance_unchecked` when the check itself could not complete (unknown is
not the same answer as clean). Carries a `severity`, the `fqdn` it is about, the
`evidence_fqdn` it was read from — routinely a *parent*, since CAA is inherited
— the published `evidence` verbatim, and one human-readable `note`.

**An advisory never changes the verdict.** `verified` and `present` are computed
without it, so ignoring the field leaves you with exactly the 0.3.0 contract.
* **`VerifyResult.advisories`** — the advisories for this session's
TLS-terminating records, read on the same pass as the verify.
* **`IntegratorSession.tls_issuance_advisories`** — the same shape on
`sessions.get`, as a snapshot of what the LAST verify pass computed. Empty until
a verify has run; not a live read.
* **`VerifyRecord.authoritative_found` / `.public_found`** — what the domain's own
nameservers answered, and what a public recursive resolver sees. The first is
the set `present` is decided from, and the answer to "what did they put there
instead"; the second never gates anything, and trailing the first is the
ordinary, healthy meaning of `outcome == "propagating"`.
* **`App.tls_issuer_ca`** — the CA issuer-domain your end-user certificates are
issued with, or `None` until it is configured in the dashboard. It is what turns
a CAA policy into the actionable `caa_excludes_issuer` rather than the vaguer
`caa_restricts_issuance`.
* `connection.verified` and `session.completed` webhook payloads now carry
`tlsIssuanceAdvisories` **when there is at least one** — absent, not empty, when
there is nothing to say. There is still deliberately no typed event parser (see
`dodomain.webhooks`), so this is a documentation change on the SDK side.
* **`tests/fixtures/openapi_v1_shapes.json` + `tests/test_openapi_contract.py`** —
a mechanical parity guard, in the spirit of `webhook_vectors.json`. The fixture
is a verbatim extract of the published OpenAPI component schemas; the test fails
if a *required* wire field of `POST /v1/sessions`, `GET /v1/sessions/{token}`
(both arms), `POST /v1/sessions/{token}/verify` or `GET /v1/apps` has no field on
the matching model, and again if a mapped field does not survive a round trip.

### Changed

* **`Session` is now `ConnectSessionSummary`.** It is the summary of ONE connect
session, and "session" already meant two other things in the platform (the
dashboard login session, and the server-side `ConnectSession` row).
`@dodomain/node` 0.5.0 made the identical rename; this SDK follows so the two
keep answering to the same vocabulary. **`Session` still works** — it is an
alias bound to the same class object, so `isinstance`, equality and existing
imports are unaffected. It is deprecated and will be REMOVED in the next major;
switch your imports now.

### Notes

* All new response fields parse tolerantly: a body recorded before the field
existed still reads (as `()` / `None`), while a field that is *present* with the
wrong type still fails loudly. Same rule as `records` / `recordFqdns` /
`previousKeyExpiresAt` before them.

## 0.3.0

Parity with the rotation-overlap contract the API shipped on 2026-08-20 (and
Expand Down
52 changes: 51 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ as a mysteriously failing verify. `session.warnings` carries advisories about a
request that was accepted anyway (`duplicate_host_label` is the one that exists
today); a warning never changes the status code.

> The type `sessions.create` returns is `ConnectSessionSummary`. It was called
> `Session` through 0.3.0, and `from dodomain import Session` still works — the
> alias is the same class object, is deprecated, and goes away in the next major.
> `@dodomain/node` 0.5.0 made the identical rename, for the identical reason:
> "session" already meant two other things in the platform.

### Reading a session back

Two different reads, and the difference matters. **From your server, use
Expand All @@ -89,6 +95,7 @@ state.status # "verified"
state.expired # True once the 24h TTL passed — even before the reaper catches up
state.connection_id # the DomainConnection id, or None if it never finalized
state.records # composed names (type/host/fqdn) — no `value` on this arm
state.tls_issuance_advisories # what the last verify pass found about certificates
```

The **token-public** routes are the other read: they take the session token in the
Expand All @@ -102,6 +109,8 @@ result = client.sessions.verify(session.token) # check live DNS now

for record in result.records:
print(record.fqdn, record.type, record.outcome)
print(record.authoritative_found) # what their OWN nameservers answered
print(record.public_found) # what a public resolver sees — trails, when propagating
```

`retrieve` raises `ExpiredError` forever once the TTL passes — correct for a
Expand All @@ -116,6 +125,42 @@ session.cloudflare_start_url # tier-1 Cloudflare OAuth flow
session.domain_connect_start_url # tier-2 Domain Connect one-click
```

### Will the certificate issue?

A verified record means DNS points at you. It does not mean a CA will hand you a
certificate for that name. Every verify pass reads the domain's own nameservers
for the two things that usually stop issuance and reports them as **advisories**:

```python
result = client.sessions.verify(session.token)

for advisory in result.advisories:
print(advisory.severity, advisory.code, advisory.fqdn)
print(advisory.note) # one human-readable sentence
print(advisory.evidence_fqdn, advisory.evidence) # where we read it, and what it said
```

| `code` | What it means |
| ------------------------- | -------------------------------------------------------------------- |
| `caa_excludes_issuer` | A CAA policy leaves out the CA your app is configured with |
| `caa_restricts_issuance` | A CAA policy exists and no CA is configured to judge it against |
| `stale_acme_challenge` | `_acme-challenge.<fqdn>` already holds a TXT or CNAME |
| `tls_issuance_unchecked` | The check itself could not complete — unknown, which is not clean |

**An advisory never changes the verdict.** `verified` and `present` are computed
without it, so a consumer that ignores the field sees exactly the contract that
shipped before advisories existed. `severity` is `"warning"` when it plausibly
breaks issuance for you and `"info"` when it is a fact we could not turn into a
verdict. An empty list means we looked and found nothing; a check that could not
run is an entry, not silence.

The same shape rides on `sessions.get(...).tls_issuance_advisories` (a snapshot
from the last verify pass, empty until one has run) and, when non-empty, on the
`connection.verified` and `session.completed` webhook payloads as
`tlsIssuanceAdvisories`. Configure which CA you issue with in the dashboard — it
is what turns the vague `caa_restricts_issuance` into the actionable
`caa_excludes_issuer`, and it reads back as `app.tls_issuer_ca`.

### Async

`AsyncDoDomain` has the identical resource tree, arguments and return types —
Expand Down Expand Up @@ -293,6 +338,7 @@ check.guide.steps # copy-ready manual instructions
```python
for app in client.apps.list():
print(app.id, app.name, app.public_key, app.sandbox)
print(app.tls_issuer_ca) # the CA your certificates are issued with, or None
```

A secret key sees exactly its own app — listing siblings would widen a single
Expand Down Expand Up @@ -358,7 +404,11 @@ parsing; do not write new code against it.

`data` always carries `sessionId` as your correlation handle, and every payload
that announces a connection also carries `connectionId` — the id
`connections.get` / `reverify` / `disconnect` are keyed by.
`connections.get` / `reverify` / `disconnect` are keyed by. `connection.verified`
and `session.completed` additionally carry `tlsIssuanceAdvisories` **when there is
at least one**, in the shape described under
[Will the certificate issue?](#will-the-certificate-issue) — absent, not empty,
when there is nothing to say.

Event types: `connection.verified`, `connection.failed`,
`connection.disconnected`, `session.completed`, `session.abandoned`. A receiver
Expand Down
10 changes: 9 additions & 1 deletion src/dodomain/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

from __future__ import annotations

__version__ = "0.3.0"
__version__ = "0.4.0"

from ._client import AsyncDoDomain, DoDomain
from ._transport import DEFAULT_BASE_URL, RateLimitSnapshot
Expand Down Expand Up @@ -49,6 +49,7 @@
Connection,
ConnectionPage,
ConnectionStatus,
ConnectSessionSummary,
DeletedWebhookEndpoint,
DetectResult,
DisconnectResult,
Expand All @@ -66,6 +67,9 @@
Session,
SessionWarning,
Tier,
TlsIssuanceAdvisory,
TlsIssuanceAdvisoryCode,
TlsIssuanceAdvisorySeverity,
VerifyOutcome,
VerifyRecord,
VerifyResult,
Expand All @@ -88,6 +92,7 @@
"Connection",
"ConnectionPage",
"ConnectionStatus",
"ConnectSessionSummary",
"DeletedWebhookEndpoint",
"DetectResult",
"DisconnectResult",
Expand Down Expand Up @@ -120,6 +125,9 @@
"Session",
"SessionWarning",
"Tier",
"TlsIssuanceAdvisory",
"TlsIssuanceAdvisoryCode",
"TlsIssuanceAdvisorySeverity",
"VerifyOutcome",
"VerifyRecord",
"VerifyResult",
Expand Down
Loading
Loading