Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-4210Implemented

(motir-core) The Fly certificates adapter — request, check and remove a hostname's certificate on the motir-marketing app, behind a port beside flyMachines

The one module in motir-core that asks Fly for a customer's certificate — behind a port, with a dependency guard, reading its token at call time. The shape is lib/orchestrator/adapters/fly/flyMachines.ts's, copied deliberately: that file is "the only module in the repository that speaks Fly" for the fleet, and this is its twin for certificates on a DIFFERENT app with a DIFFERENT token.

What ships

  • The portlib/publicAddresses/certificateProvider.ts: interface CertificateProvider { request(hostname): Promise<CertificateState>; check(hostname): Promise<CertificateState>; remove(hostname): Promise<void> } and the normalised CertificateState{ hostname, configured: boolean, issued: boolean, dnsRequirements: { type: 'A' | 'AAAA' | 'CNAME' | 'TXT', name, value }[], checkedAt }. The lifecycle service depends on the port only.
  • The adapterlib/publicAddresses/adapters/fly/flyCertificates.ts: fetch against Fly's certificates resource on the app named by config — POST /v1/apps/{app}/certificates/acme with { hostname } to request, POST /v1/apps/{app}/certificates/{hostname}/check to read state and dns_requirements, and the documented remove call. Verify each path and response field against https://fly.io/docs/networking/custom-domain-api/ at build time and quote the doc's field names in the mapping's comments — the planner read the resource's names (dns_requirements, configured), not its full schema, so the mapping is written from the doc, not from this card.
  • Config at CALL time, never module load (the appAuth.ts contract flyMachines.ts cites): FLY_CERTS_TOKEN and FLY_CERTS_APP (the ADR names them), a typed CertificateProviderNotConfiguredError when either is unset — a self-hosted build never reaches this path and must not crash on boot. No fallback to any other Fly token: the token is scoped to the motir-marketing app and to nothing else, for the same reason FLY_FLEET_API_TOKEN is its own variable.
  • Typed errors, one per outcome the service must branch on: not configured · Fly refused (4xx with the body's message) · Fly unavailable (5xx / network / ORCHESTRATOR_REQUEST_TIMEOUT_MS-style timeout) · hostname unknown on remove.
  • The dependency guard — a test in the shape of tests/ciFleet/orchestratorPortBoundary.test.ts asserting that api.machines.dev and the Fly paths appear in this adapter file and nowhere else outside lib/orchestrator/adapters/fly/.
  • Egress: confirm lib/legal/egress-manifest.json already records api.machines.dev as an egress host (it lists it for the fleet); add this caller to the entry's evidence if the manifest is per-caller — read the file before asserting either.
  • Unit tests over a fake fetch: every port method's happy path, every typed error, the not-configured branch, and that no method is called at import.

Boundary

No persistence and no scheduling — the store holds state, the lifecycle decides when to call, the status job polls. The wildcard for the base domain is NOT requested here — a human ran fly certs add for it (the domain card); this adapter is per-customer-hostname only.

Acceptance criteria

  • CertificateProvider and CertificateState are exported from lib/publicAddresses/certificateProvider.ts, and flyCertificates.ts is the only implementation; no file under lib/services/ imports the adapter directly.
  • Requesting, checking and removing a hostname each issue exactly the documented Fly call, with the token as Authorization: Bearer and the app from FLY_CERTS_APP; the mapping's comments quote the doc's field names beside each mapped field.
  • With FLY_CERTS_TOKEN or FLY_CERTS_APP unset, every method throws CertificateProviderNotConfiguredError and importing the module throws nothing.
  • Each of the four typed errors has a unit test, and a request that exceeds the timeout resolves to the unavailable error rather than hanging.
  • A dependency-guard test fails when a Fly host or path string appears in any file outside the two Fly adapter directories.
  • No file outside motir-core is touched.

Context refs

  • the decision — Q5 (Fly per hostname, on the marketing app) and the variable names
  • motir-core/lib/orchestrator/adapters/fly/flyMachines.ts — the boundary shape, the call-time config accessor, the token rule; lib/orchestrator/errors.ts — the typed-error vocabulary to mirror
  • motir-core/tests/ciFleet/orchestratorPortBoundary.test.ts — the dependency guard to copy
  • motir-core/lib/legal/egress-manifest.json — the api.machines.dev entry
  • Fly — https://fly.io/docs/networking/custom-domain-api/ (the certificates resource) · https://fly.io/docs/networking/custom-domain/