@sanoma-ai/connector-cloudflare 0.1.6 → 0.3.0

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @sanoma-ai/connector-cloudflare
2
2
 
3
- [Cloudflare Containers](https://developers.cloudflare.com/containers/) as a host that stops when quiet, and its [Durable Object](https://developers.cloudflare.com/durable-objects/)'s [alarm](https://developers.cloudflare.com/durable-objects/api/alarms/) as the wakeup that starts it again, for [`@sanoma-ai/workflows`](https://www.npmjs.com/package/@sanoma-ai/workflows). Both are one Worker per company, `sanoma-<name>`, on the account's workers.dev subdomain.
3
+ [Cloudflare Containers](https://developers.cloudflare.com/containers/) as a host that stops when quiet, and its [Durable Object](https://developers.cloudflare.com/durable-objects/)'s [alarm](https://developers.cloudflare.com/durable-objects/api/alarms/) as the wakeup that starts it again, for [`@sanoma-ai/workflows`](https://www.npmjs.com/package/@sanoma-ai/workflows). Both are one Worker per company, `sanoma-<name>`, on the account's workers.dev subdomain. And a set of [DNS records](#dns-records) in one of the account's zones, such as the ones a sending domain asks for.
4
4
 
5
5
  ## Usage
6
6
 
@@ -8,12 +8,13 @@
8
8
  npm install @sanoma-ai/connector-cloudflare
9
9
  ```
10
10
 
11
- Both types are writable: `read` and `import` (effect `read`), and the change operations only the apply workflow may call, `plan` (effect `read`), `create`, `update` and `delete` (effect `write`). The container is a host ([Hosts](https://github.com/Sanoma-AI/sanoma/blob/main/packages/workflows/README.md#hosts)) and the alarm a wakeup, with one more operation, `register`, which the worker calls:
11
+ The three types are writable: `read` and `import` (effect `read`), and the change operations only the apply workflow may call, `plan` (effect `read`), `create`, `update` and `delete` (effect `write`). The container is a host ([Hosts](https://github.com/Sanoma-AI/sanoma/blob/main/packages/workflows/README.md#hosts)) and the alarm a wakeup, with one more operation, `register`, which the worker calls:
12
12
 
13
- | Type | Operations | Import id |
14
- | ----------- | ----------------------------------------------------------------------------------------- | -------------- |
15
- | `container` | `cloudflare.container.read`, `.import`, `.plan`, `.create`, `.update`, `.delete` | its name |
16
- | `alarm` | `cloudflare.alarm.read`, `.import`, `.plan`, `.create`, `.update`, `.delete`, `.register` | its host's URL |
13
+ | Type | Operations | Import id |
14
+ | ----------- | ----------------------------------------------------------------------------------------- | --------------- |
15
+ | `container` | `cloudflare.container.read`, `.import`, `.plan`, `.create`, `.update`, `.delete` | its name |
16
+ | `alarm` | `cloudflare.alarm.read`, `.import`, `.plan`, `.create`, `.update`, `.delete`, `.register` | its host's URL |
17
+ | `records` | `cloudflare.records.read`, `.import`, `.plan`, `.create`, `.update`, `.delete` | `<zone>/<name>` |
17
18
 
18
19
  A data file declares them with the constructors in `@sanoma-ai/connector-cloudflare/resources`. Declaring the alarm is the whole switch for scaling to zero: without it, nothing starts a stopped container at a deadline, and a sleeping run waits for the next request.
19
20
 
@@ -45,12 +46,12 @@ export const wakeup = cloudflare.alarm({ host });
45
46
  | `ref` | The branch it runs, never a commit: apply resolves it and sets the hidden `revision`, so a merge is drift on the container |
46
47
  | `port` | The port the runtime listens on in the container (its `PORT`) |
47
48
  | `secrets` | Environment variables, each a `secret(...)`: the Worker's secret bindings, write-only, never in an output, the ledger or a checkpoint |
48
- | `domains` | Optional: a public site's domain, by site name, passed as `SANOMA_DOMAINS`; routing a domain to the Worker is not done here yet |
49
+ | `domains` | Optional: a public site's domain, by site name, passed as `SANOMA_DOMAINS` and attached to the Worker by apply as a custom domain ([Custom domains](#custom-domains)) |
49
50
  | `id`, `url`, `healthy` | Cloudflare's: the application's id, the Worker's workers.dev URL, and whether it answers |
50
51
 
51
52
  The host's outputs are `{ url, revision, healthy }`. Its capabilities: stops when quiet, a public URL, it runs an image rather than building one, no fixed egress. `healthy` is the application existing and the Worker answering `/__sanoma/health`, which does not start a stopped container: a stopped host is healthy, and a read or a drift check leaves it stopped.
52
53
 
53
- The alarm's only field is `host`, the container it wakes (its URL); its state is `{ host, at, rang, healthy }`, the wake it holds, when its alarm last rang the host's `/wake` for it, and whether the host's Worker took the company's token. `register` takes `{ at }`, an ISO 8601 time, which replaces the wake the Worker held: a wake is "resume if stopped", so one that rings late, twice or for a replaced registration starts a worker that finds nothing to do and stops again. It names no host: on the host, the worker reads the host's URL from `SANOMA_HOST_URL`, which the Worker gives the container; off it, from the credential `alarm.create` stored.
54
+ The alarm's field is `host`, the container it wakes (its URL); its state is `{ host, at, rang, healthy }`, the wake it holds, when its alarm last rang the host's `/wake` for it, and whether the host's Worker took the company's token. Its optional `token`, a `secret("NAME")`, names the secret that holds the host's wakeup token, by default `SANOMA_WAKEUP_TOKEN`: a workflow that makes a host and its alarm for each of several companies, outside any data file, gives each its own (`token: secret("CLOUD_ACME_WAKEUP_TOKEN")`, the secret the host's `secrets` give as `SANOMA_WAKEUP_TOKEN`). `create` mints the token there when none is stored, and its state then carries the name as `token`, so a `read`, `update` or `delete` given that state sends the same token; one given only an id uses the default. A data file leaves `token` out: apply reads and deletes an alarm by its id alone, so `plan` refuses another name. `register` takes `{ at }`, an ISO 8601 time, which replaces the wake the Worker held: a wake is "resume if stopped", so one that rings late, twice or for a replaced registration starts a worker that finds nothing to do and stops again. It names no host: on the host, the worker reads the host's URL from `SANOMA_HOST_URL`, which the Worker gives the container; off it, from the credential `alarm.create` stored. A runtime whose data files declare no wakeup, but whose environment has both `SANOMA_HOST_URL` and `SANOMA_WAKEUP_TOKEN` (a host Sanoma Cloud made), registers at the same route itself, with no connector call (`envWakeup` in `@sanoma-ai/workflows`).
54
55
 
55
56
  The logo is Cloudflare's cloud from [Simple Icons](https://simpleicons.org/?q=cloudflare) (CC0), there only to identify the service an operation calls.
56
57
 
@@ -64,7 +65,7 @@ The package ships the Worker it uploads, `worker/`: `index.js`, which exports th
64
65
  - holds the company's one wake at `/__sanoma/wake`, with `Authorization: Bearer <SANOMA_WAKEUP_TOKEN>`: `GET` shows `{ at, rang? }`, `PUT { at }` replaces it and sets the object's alarm, `DELETE` clears it. Without the token among the host's secrets it answers 404, with another one 401;
65
66
  - at the alarm, starts the container and calls its `/wake`, then notes when (`rang`); a failure throws, so Cloudflare rings the alarm again.
66
67
 
67
- Cloudflare stops the container after ten quiet minutes; the object, its storage and its alarm stay, and the next request or alarm starts the container again. The application is of the `durable_object` scheduling policy (public beta, the one Cloudflare documents for new applications; it refused `default` on the account the connector was first run on): the object starts its container, with the image and the instance type it names, so the application is only a name and the object's namespace, and has no image, versions or rollouts. A create, in Wrangler's order, copies and pins the image and waits until Cloudflare prepared it ([The image](#the-image)), uploads the Worker with the object's migration (`new_sqlite_classes`) and its class as a container's with that image (`containers: [{ name, class_name, images: { runtime } }]`, as Wrangler sends it), turns its workers.dev URL on, makes the application for the object's namespace (`{ name, scheduling_policy: "durable_object", durable_objects: { namespace_id } }`, the `namespace_id` the Worker's settings then show), and uploads the Worker once more: the object learns which application backs its namespace only from a deployment made after the application exists, and without that upload every start answers "There is no container application assigned to this Durable Object namespace", for good (found on the first real deploy, 2026-10-29); an update does the same, except that it leaves the application as it is: a new image is a new upload naming it, and the object destroys a container started at another and starts it again at the next request. Either waits for the Worker to answer at the new revision. The policy is fixed once an application is made, and the object cannot start a container under another: one of the `default` policy under that name (none can be made any more) is deleted by hand before the apply. A secret's new value goes up with the next update for another reason (a plan compares secret names, never values: the Worker's settings never show them), and a running container keeps the old one until it starts again. A delete deletes the application, then the Worker with its object's storage. A Worker of that name that is not Sanoma's (no `SANOMA_INSTANCE` binding) reads as gone, and a create or a delete refuses to touch it.
68
+ Cloudflare stops the container after ten quiet minutes; the object, its storage and its alarm stay, and the next request or alarm starts the container again. The application is of the `durable_object` scheduling policy (public beta, the one Cloudflare documents for new applications; it refused `default` on the account the connector was first run on): the object starts its container, with the image and the instance type it names, so the application is only a name and the object's namespace, and has no image, versions or rollouts. A create, in Wrangler's order, copies and pins the image and waits until Cloudflare prepared it ([The image](#the-image)), uploads the Worker with the object's migration (`new_sqlite_classes`) and its class as a container's with that image (`containers: [{ name, class_name, images: { runtime } }]`, as Wrangler sends it), turns its workers.dev URL on, makes the application for the object's namespace (`{ name, scheduling_policy: "durable_object", durable_objects: { namespace_id } }`, the `namespace_id` the Worker's settings then show), and uploads the Worker once more: the object learns which application backs its namespace only from a deployment made after the application exists, and without that upload every start answers "There is no container application assigned to this Durable Object namespace", for good (found on the first real deploy, 2026-10-29); an update does the same, except that it leaves the application as it is: a new image is a new upload naming it, and the object destroys a container started at another and starts it again at the next request. Either waits for the Worker to answer at the new revision. The policy is fixed once an application is made, and the object cannot start a container under another: one of the `default` policy under that name (none can be made any more) is deleted by hand before the apply. A secret's new value goes up with the next update for another reason (a plan compares secret names, never values: the Worker's settings never show them), and a running container keeps the old one until it starts again. A delete detaches the custom domains the Worker was declared to serve, deletes the application, then the Worker with its object's storage. A Worker of that name that is not Sanoma's (no `SANOMA_INSTANCE` binding) reads as gone, and a create or a delete refuses to touch it.
68
69
 
69
70
  ## The image
70
71
 
@@ -79,7 +80,39 @@ The Worker's `SANOMA_IMAGE` binding holds the declared image and its upload the
79
80
  3. **Go live.** `infra.ts` declares the host with `SANOMA_WAKEUP_TOKEN: secret("SANOMA_WAKEUP_TOKEN", { generate: true })` among its secrets (apply makes it), and, after it, `export const wakeup = cloudflare.alarm({ host })`. One apply makes the Worker, waits for it, then the alarm, which checks the Worker holds the token.
80
81
  4. **A sleeping run.** Start a workflow that sleeps past ten quiet minutes. Before the container stops the worker registers the sleep's end; at `at` the object's alarm starts the container and calls `/wake`, and the run continues.
81
82
 
82
- To stop scaling to zero, delete the `wakeup` line and apply: the alarm is deleted, and the next request still starts the container.
83
+ To stop scaling to zero, delete the `wakeup` line and `SANOMA_WAKEUP_TOKEN` from the host's secrets, and apply: the alarm is deleted, and the next request still starts the container. With the token left among the host's secrets, the runtime registers with the Worker's wake route itself (`envWakeup`), as a Sanoma Cloud company's does.
84
+
85
+ ## Custom domains
86
+
87
+ Each hostname in `domains` is attached to the Worker as a [Workers custom domain](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/) by a create or an update: Cloudflare then makes its DNS record and certificate and routes it to the Worker. The zone is the hostname's last two labels (`www.acme.example` is in `acme.example`), looked up in the account once per apex when a domain is to be attached (`GET /zones?name=&account.id=`); a hostname whose zone the account does not hold, or the token cannot read, is refused, naming the hostname and the zone, before anything changes. An update lists the Worker's attached domains first (`GET …/workers/domains?service=`), and only a missing one's zone is looked up; after the upload, each missing one is attached (`PUT …/workers/domains { zone_id, hostname, service }`), then the ones declared before and no longer are detached (`DELETE …/workers/domains/{id}`). A domain attached by hand and never declared is left alone. A read lists the Worker's attached domains too and shows only the declared ones attached, so a domain whose attach failed, or one detached by hand, is drift that the next apply attaches again. A delete detaches every declared one before the Worker goes. A container without `domains` makes none of these requests, unless it served some before, which are then detached. A zone such as `acme.co.uk`, whose apex is three labels, is not found yet.
88
+
89
+ ## DNS records
90
+
91
+ `cloudflare.records({ zone, name, records })` is a set of DNS records in one of the account's zones, compared as a set, in no order. A sending domain's records are the case it is made for: `records` names a declared `resend.domain`, whose `dns_records` it then takes ([a field taken from another resource's state](https://github.com/Sanoma-AI/sanoma/blob/main/packages/workflows/README.md#resources)), or is a list written out.
92
+
93
+ ```ts
94
+ // resources/infra.ts
95
+ import { cloudflare } from "@sanoma-ai/connector-cloudflare/resources";
96
+ import { resend } from "@sanoma-ai/connector-resend/resources";
97
+
98
+ export const mail = resend.domain({ name: "sanoma.work" });
99
+ export const mailDns = cloudflare.records({ zone: "sanoma.work", name: "resend", records: mail });
100
+ ```
101
+
102
+ | Field | What it is |
103
+ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
104
+ | `zone` | The zone's name, as the account holds it (`sanoma.work`); changing it replaces the set |
105
+ | `name` | The set's label (`resend`): lower case letters, digits, `-` and `_`. Its identity is `<zone>/<name>`; changing it replaces the set |
106
+ | `records` | The records, at least one: `{ type, name, content, priority?, ttl? }` each (`dnsRecord` from `@sanoma-ai/workflows`): TXT, MX, CNAME or CAA, the full name in lower case (the zone's name or one ending in it), a TXT record's text unquoted, a priority for MX, a ttl in seconds (1, automatic, when left out). One type, name and content declared twice with another ttl or priority, a name outside the zone, or a CNAME declared beside another record of its name, or two SPF policies (`v=spf1` TXT records) declared at one name, is refused, `invalid` |
107
+ | `zone_id`, `record_ids` | Cloudflare's: the zone's id, and each record's, in the order of `records` as a read gives them |
108
+
109
+ **Ownership.** Every record the set writes carries the comment `sanoma:<zone>/<name>` (`sanoma:sanoma.work/resend`), in that case exactly, which is how a read finds its records (`GET /zones/{id}/dns_records?comment.exact=`): the set is the records that carry it, and none is gone. A record's comment holds 100 characters on a Free zone, so a set whose mark is longer is refused, `invalid`. A record without that comment is never changed or deleted, whoever added it, with one exception: a record identical to a declared one in type, name and content, and an MX record's priority (one added by hand before the set was declared), is adopted, patched to carry the comment and the declared ttl; so is one a changed record would be patched to, whose marked one is then deleted. An adopted record is the set's from then on: it carries the mark, so it changes with the set and is **deleted with it**, and the comment it had is replaced by the mark (the set finds its records by the exact comment, so it keeps no other). A record in the way of a declared one is refused, `conflict`, naming it, before anything is written, whether the declared one is to be posted or a marked one patched to it: a CNAME at the name (or a declared CNAME beside any record), another SPF policy (a `v=spf1` TXT) at the name, an MX record to the same server at another priority, a record identical to a declared one that another set's comment marks, or one identical to it that is proxied (a set's records are DNS only, and adopting it would turn its proxy off). Every page of a lookup is read, so a busy name hides nothing.
110
+
111
+ **Writes.** A create or an update is one batch (`POST /zones/{id}/dns_records/batch`), which Cloudflare runs as one transaction, all or nothing: a post for each declared record no record holds, a patch for each marked record of a declared one's type and name whose content, priority or ttl changed, or whose proxy was turned on by hand (one of the same content first, so a ttl's change patches that record), and each one adopted, and a delete for each marked record no longer declared. Each record is written unproxied (`proxied: false`), with a ttl of 1 (automatic) unless declared; a CAA record's content, `0 issue "letsencrypt.org"` (its value quoted or not), is sent as its fields and compared with its value quoted, as Cloudflare shows it. A set already as declared writes nothing, an update retried after its batch was written too. A delete is one batch deleting the marked records, and only those. A read gives each marked record as `records` holds it: its name in lower case with no trailing dot, a TXT record's text with the quotes Cloudflare may show around it taken off (and a long one's quoted parts joined), the priority of an MX record, the ttl (1 when automatic); a plan and drift compare a declared ttl left out as 1.
112
+
113
+ **Records not known yet.** Where `records` is taken from a domain apply creates in the same run, `plan` is given `records: null`: a set not bound yet plans a create, one the zone holds an update, with `records` among `unknown`, and apply plans it again at its step with the domain's records. An `update` whose `desired` leaves `records` out (the domain is absent or unreadable, so drift and apply leave the field out) keeps what the zone holds and writes nothing. `records: null` at a create or an update is refused, `not_known`: apply fails such a step before it gets there.
114
+
115
+ **The token.** The API token needs **Zone → DNS → Edit** on the zone, beside Zone Read (the zone's lookup) and the Workers permissions the container needs. A token without it is refused, not retryable, `no_dns_permission` (status 403), naming the permission and the zone, whether Cloudflare refuses the read of the records or the batch. A zone the account does not hold, or the token cannot read, is refused at a create, an update or a delete, `no_zone` (Cloudflare lists neither, so a delete is not taken as done with the records perhaps left behind); a read there finds the set gone.
83
116
 
84
117
  ## Driver
85
118
 
@@ -93,16 +126,16 @@ A read is the Worker's settings (its bindings: what it runs is its plain-text bi
93
126
 
94
127
  It reads these on every call, never when the config loads:
95
128
 
96
- | Variable | What it is |
97
- | ----------------------- | ---------------------------------------------------------------------------------------------- |
98
- | `CLOUDFLARE_API_TOKEN` | An API token with Workers Scripts Edit and Workers Containers Edit (Write, in the API's terms) |
99
- | `CLOUDFLARE_ACCOUNT_ID` | The account's id, 32 hexadecimal digits, on its dashboard's overview |
129
+ | Variable | What it is |
130
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
131
+ | `CLOUDFLARE_API_TOKEN` | An API token with Workers Scripts Edit and Workers Containers Edit (Write, in the API's terms), Zone Read for `domains` (custom domains are attached and detached with Workers Scripts Edit) and DNS records, and Zone → DNS → Edit on the zones of [DNS records](#dns-records) |
132
+ | `CLOUDFLARE_ACCOUNT_ID` | The account's id, 32 hexadecimal digits, on its dashboard's overview |
100
133
 
101
134
  When either is unset, a call fails, not retryable, naming it. The driver declares both in its `env` ([Credentials](https://www.npmjs.com/package/@sanoma-ai/workflows#credentials)): the worker starts without them, and the app's connector page shows their status and lets you set them. The alarm's operations need neither: they call the Worker with `SANOMA_WAKEUP_TOKEN` (`alarm.create` mints it when none is stored), and `alarm.create` stores `SANOMA_HOST_URL` once the host answered, for a register made off the host; it keeps the first one stored, which the Worker's own binding overrides on the host. A read without the token reads the alarm as unhealthy, so a plan to delete it still goes on.
102
135
 
103
136
  ### Errors and retries
104
137
 
105
- A 429, a 5xx, a timeout or no reply is retryable, the image's copy's requests too; any other 4xx is not. `status` and `vendorCode` (Cloudflare's first error `code`) are kept. The driver's own refusals are not retryable: `exists` (a Worker of that name that is not Sanoma's, which is neither overwritten nor deleted), `plan_changed` (an update whose plan changed since it was approved, or is not an update: a new name is a delete and a create), `no_token` and `token` (a host without the company's `SANOMA_WAKEUP_TOKEN`, or with another one), and a Worker that did not answer at its revision in time, which says what it answered.
138
+ A 429, a 5xx, a timeout or no reply is retryable, the image's copy's requests too; any other 4xx is not. `status` and `vendorCode` (Cloudflare's first error `code`) are kept. The driver's own refusals are not retryable: `exists` (a Worker of that name that is not Sanoma's, which is neither overwritten nor deleted), `plan_changed` (an update whose plan changed since it was approved, or is not an update: a new name is a delete and a create), `no_token` and `token` (a host without the company's `SANOMA_WAKEUP_TOKEN`, or with another one), `no_zone` (a domain or a record set whose zone the account does not hold), `conflict` (a record in the way of a declared one), `no_dns_permission` (a token without Zone → DNS → Edit), `not_known` (records given as `null`), `invalid` (a record the shared shape refuses, an MX one without a priority, one outside the zone, a CNAME or a second SPF policy beside another declared record, or a set's mark longer than 100 characters), and a Worker that did not answer at its revision in time, which says what it answered.
106
139
 
107
140
  ### Plan
108
141
 
@@ -110,7 +143,7 @@ Containers need the Workers Paid plan. An instance is billed while it runs, by i
110
143
 
111
144
  ### Testing the driver
112
145
 
113
- `test/driver.test.ts` and the features replay Cloudflare's replies (`test/fixtures`) with [`@sanoma-ai/testing/replay`](https://github.com/Sanoma-AI/sanoma/blob/main/packages/testing/README.md#replaying-a-vendors-api): `pnpm vitest run connectors/cloudflare` needs no account. The fixtures are **written from Cloudflare's API reference, not recorded**: no account was at hand when the connector was written. A recording replaces them (`SANOMA_LIVE=1 SANOMA_RECORD=1` on the features test), losing the account's id and subdomain, ids and tokens; until then, a reply the reference gets wrong is wrong here too. The upload's `containers` field (its `name`, `class_name` and `images`) is Wrangler's and not in the reference or the SDK's types, and the object's `ctx.container.images` and `start({ image, instance })` are the policy's: the live run is what checks them. The image's copy is tested against two small registries in this process (`test/registry.test.ts`), and the driver's call for its credentials over replies written from the SDK's types and the OCI distribution spec; that Cloudflare's registry takes the copy as written is what the live run checks. The driver tests only replay.
146
+ `test/driver.test.ts`, `test/records.test.ts` and the features replay Cloudflare's replies (`test/fixtures`) with [`@sanoma-ai/testing/replay`](https://github.com/Sanoma-AI/sanoma/blob/main/packages/testing/README.md#replaying-a-vendors-api): `pnpm vitest run connectors/cloudflare` needs no account. The fixtures are **written from Cloudflare's API reference, not recorded**: no account was at hand when the connector was written. A recording replaces them (`SANOMA_LIVE=1 SANOMA_RECORD=1` on the features test), losing the account's id and subdomain, ids and tokens; until then, a reply the reference gets wrong is wrong here too. The upload's `containers` field (its `name`, `class_name` and `images`) is Wrangler's and not in the reference or the SDK's types, and the object's `ctx.container.images` and `start({ image, instance })` are the policy's: the live run is what checks them. The DNS records' fixtures (`records-*`) are written from the [DNS records reference](https://developers.cloudflare.com/api/resources/dns/subresources/records/) the same way, on `sanoma.work` with a set named `check` (`check-*.sanoma.work`), to be recorded on their first live run: Cloudflare's TXT content is quoted in them, which a read takes off. That Cloudflare shows a TXT record written unquoted with quotes is the reference's examples', not checked; a read compares it either way. The image's copy is tested against two small registries in this process (`test/registry.test.ts`), and the driver's call for its credentials over replies written from the SDK's types and the OCI distribution spec; that Cloudflare's registry takes the copy as written is what the live run checks. The driver tests only replay.
114
147
 
115
148
  ### Running it live
116
149
 
@@ -142,11 +175,11 @@ The connector carries steps for [scenarios](https://github.com/Sanoma-AI/sanoma/
142
175
  | ------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
143
176
  | Given | `a Cloudflare container for {string} at revision {string}` | Creates the company's container through `cloudflare.container.create`, its token generated, named `host` (`$host.state.url`) |
144
177
 
145
- `features/cloudflare.feature` creates, reads, plans and deletes a container, and creates an alarm and registers with it, on the fake and on the replies in `test/fixtures` (named after each scenario): `test/features.test.ts` runs them with [`describeFeatures`](https://github.com/Sanoma-AI/sanoma/blob/main/packages/testing/README.md#a-connectors-own-features). The package ships `features/`.
178
+ `features/cloudflare.feature` creates, reads, plans and deletes a container, and creates an alarm and registers with it, and `features/records.feature` creates a record set, reads it, plans it unchanged, updates it with a changed record and one dropped, and deletes it, on the fake and on the replies in `test/fixtures` (named after each scenario): `test/features.test.ts` runs them with [`describeFeatures`](https://github.com/Sanoma-AI/sanoma/blob/main/packages/testing/README.md#a-connectors-own-features). The package ships `features/`.
146
179
 
147
180
  ## Testing
148
181
 
149
- `@sanoma-ai/connector-cloudflare/fake` is an in-memory Cloudflare account, the fake the connector carries: each sandbox run makes one of its own. Its registry is in memory too: a copy records the image under its reference there and pulls nothing. A copy pins the image by a made-up digest, and a preparation answers ready for an image pinned in its registry. Its Workers answer on ports of this machine, one per Worker name, and run the package's own object code over a `ctx` in this process: storage in memory, an alarm on a timer (again after a throw), `ctx.container.images` from the upload's `containers[].images`, and a container that runs what `start` returns for the image (the one its pinned copy was copied from) and revision it was started at (by default a stub that answers `/healthz` and `/wake`); `start` refuses an image the upload did not name or an instance type that is not Cloudflare's. An upload restarts the object over its new bindings and images: a new image starts the container again at the next request, as the object finds its label changed.
182
+ `@sanoma-ai/connector-cloudflare/fake` is an in-memory Cloudflare account, the fake the connector carries: each sandbox run makes one of its own. Its registry is in memory too: a copy records the image under its reference there and pulls nothing. Its custom domains are in `state.domains` (hostname, Worker and zone), each zone's DNS records in `state.dns`, by the zone's name (a test adds one by hand there; a batch is all or nothing, and refuses a record identical to one the zone holds (its type, content and priority) or one beside a CNAME, as Cloudflare does), and it holds every zone asked for unless `fakeCloudflare({ zones })` names the ones it holds. A copy pins the image by a made-up digest, and a preparation answers ready for an image pinned in its registry. Its Workers answer on ports of this machine, one per Worker name, and run the package's own object code over a `ctx` in this process: storage in memory, an alarm on a timer (again after a throw), `ctx.container.images` from the upload's `containers[].images`, and a container that runs what `start` returns for the image (the one its pinned copy was copied from) and revision it was started at (by default a stub that answers `/healthz` and `/wake`); `start` refuses an image the upload did not name or an instance type that is not Cloudflare's. An upload restarts the object over its new bindings and images: a new image starts the container again at the next request, as the object finds its label changed.
150
183
 
151
184
  ```ts
152
185
  import { fakeCloudflare } from "@sanoma-ai/connector-cloudflare/fake";
@@ -166,6 +199,6 @@ await fake.close();
166
199
 
167
200
  ## Later
168
201
 
169
- Custom domains (routing `domains` to the Worker: `sanoma-ops`'s was attached by hand, `PUT /accounts/{id}/workers/domains`), snapshots, static egress, outbound Workers, and reading the logs (the Worker's and the container's are turned on at the upload and the application, and readable in the account's observability) are not done here yet.
202
+ Snapshots, static egress, outbound Workers, and reading the logs (the Worker's and the container's are turned on at the upload and the application, and readable in the account's observability) are not done here yet.
170
203
 
171
204
  License: FSL-1.1-ALv2.
@@ -1,4 +1,4 @@
1
- import { type Connector } from "@sanoma-ai/workflows";
1
+ import { type Connector, type DnsRecord } from "@sanoma-ai/workflows";
2
2
  import { z } from "zod";
3
3
  /**
4
4
  * `cloudflare.container`: the company's Worker, `sanoma-<name>`, on the account's workers.dev
@@ -32,19 +32,82 @@ export declare const container: import("@sanoma-ai/workflows").Resource<z.ZodObj
32
32
  * the container at the time the worker registered and calls its `/wake`. The alarm
33
33
  * is the host's object's, so it has no id of its own: its binding is the host's URL, and a new
34
34
  * host is a new alarm. The host's `secrets` must carry `SANOMA_WAKEUP_TOKEN`, which `create`
35
- * mints when none is stored.
35
+ * mints when none is stored, under the store name `token` gives (`secret("NAME")`), by default
36
+ * `SANOMA_WAKEUP_TOKEN`: a workflow that makes hosts for several companies names one per company.
37
+ * The state keeps that name, so a `read`, `update` or `delete` given the state sends the same token.
36
38
  */
37
39
  export declare const alarm: import("@sanoma-ai/workflows").Resource<z.ZodObject<{
38
40
  host: z.ZodString;
39
41
  at: z.ZodOptional<z.ZodNullable<z.ZodString>>;
40
42
  rang: z.ZodOptional<z.ZodNullable<z.ZodString>>;
41
43
  healthy: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
44
+ token: z.ZodOptional<z.ZodNullable<z.ZodString>>;
42
45
  }, z.core.$strip>, "cloudflare.alarm", {
43
46
  readonly host: {
44
47
  readonly type: readonly ["cloudflare.container"];
45
48
  readonly by: readonly ["url"];
46
49
  };
47
50
  }, true>;
51
+ /** A CAA record's content, `0 issue "letsencrypt.org"`, as the fields Cloudflare takes; undefined when it is not one. */
52
+ export declare function caaData(content: string): {
53
+ flags: number;
54
+ tag: string;
55
+ value: string;
56
+ } | undefined;
57
+ /** A CAA record's fields as its content, as Cloudflare shows them: `caaData`'s reverse. */
58
+ export declare const caaContent: ({ flags, tag, value }: {
59
+ flags: number;
60
+ tag: string;
61
+ value: string;
62
+ }) => string;
63
+ /** A record as a zone or a declaration gives it, as far as a set compares it. */
64
+ interface AnyRecord {
65
+ type: string;
66
+ name: string;
67
+ content: string;
68
+ priority?: number | null | undefined;
69
+ ttl?: number | null | undefined;
70
+ }
71
+ /** A canonical record's key: two records of one key are the same record. */
72
+ export declare const keyOf: ({ type, name, content, priority, ttl }: DnsRecord) => string;
73
+ /**
74
+ * One record in the one form a set compares: its name in lower case with no trailing dot, a TXT
75
+ * record's text unquoted, a CAA record's value quoted, a priority for an MX record only, and a
76
+ * ttl, 1 (automatic) unless given.
77
+ */
78
+ export declare const canonicalRecord: ({ type, name, content, priority, ttl }: AnyRecord) => DnsRecord;
79
+ /**
80
+ * `cloudflare.records({ zone, name, records })`: a set of DNS records in one of the account's
81
+ * zones, `<zone>/<name>`, compared as a set. Each record it writes carries the comment
82
+ * `sanoma:<zone>/<name>`, which is how a read finds them; a record without it is never changed or
83
+ * deleted, except one identical to a declared record in type, name, content (and an MX record's
84
+ * priority), which is adopted (its comment replaced by the mark), and is deleted with the set from then on.
85
+ * `records` may name a `resend.domain`, whose `dns_records` it then takes (`from`): a sending
86
+ * domain's records go to the zone that holds them.
87
+ */
88
+ export declare const records: import("@sanoma-ai/workflows").Resource<z.ZodObject<{
89
+ zone: z.ZodString;
90
+ name: z.ZodString;
91
+ records: z.ZodArray<z.ZodObject<{
92
+ type: z.ZodEnum<{
93
+ CAA: "CAA";
94
+ CNAME: "CNAME";
95
+ MX: "MX";
96
+ TXT: "TXT";
97
+ }>;
98
+ name: z.ZodString;
99
+ content: z.ZodString;
100
+ priority: z.ZodOptional<z.ZodNumber>;
101
+ ttl: z.ZodOptional<z.ZodNumber>;
102
+ }, z.core.$strip>>;
103
+ zone_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
104
+ record_ids: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
105
+ }, z.core.$strip>, "cloudflare.records", {
106
+ readonly records: {
107
+ readonly type: readonly ["resend.domain"];
108
+ readonly from: "dns_records";
109
+ };
110
+ }, true>;
48
111
  /** `alarm.register`, in the alarm type's own group, beside its resource operations. */
49
112
  declare const registerSpec: {
50
113
  register: {
@@ -57,9 +120,10 @@ declare const registerSpec: {
57
120
  output: z.ZodObject<{}, z.core.$strip>;
58
121
  };
59
122
  };
60
- /** The connector's specs: both types' operations, and `register` in the alarm's group. */
123
+ /** The connector's specs: each type's operations, and `register` in the alarm's group. */
61
124
  type CloudflareSpecs = {
62
125
  container: (typeof container)["ops"];
126
+ records: (typeof records)["ops"];
63
127
  alarm: (typeof alarm)["ops"] & typeof registerSpec;
64
128
  };
65
129
  /**
@@ -67,6 +131,6 @@ type CloudflareSpecs = {
67
131
  * `delete`) and `alarm.register` in the alarm's group (Supabase's pattern).
68
132
  */
69
133
  export declare const cloudflareOps: Connector<"cloudflare", CloudflareSpecs>;
70
- /** The connector's type: both types, with `alarm.register`. */
134
+ /** The connector's type: the three types, with `alarm.register`. */
71
135
  export type CloudflareOps = typeof cloudflareOps;
72
136
  export {};
package/dist/connector.js CHANGED
@@ -1,4 +1,4 @@
1
- import { defineConnector, defineResource, vendorInfo } from "@sanoma-ai/workflows";
1
+ import { compareDeclared, defineConnector, defineResource, dnsRecord, vendorInfo, } from "@sanoma-ai/workflows";
2
2
  import { DOMAIN_NAME, SITE_KEY } from "@sanoma-ai/workflows/shared";
3
3
  import { z } from "zod";
4
4
  /*
@@ -86,13 +86,19 @@ const alarmSchema = z.object({
86
86
  .nullish()
87
87
  .describe("When the alarm last rang the host's /wake for that registration, null until it has"),
88
88
  healthy: z.boolean().nullish().describe("Whether the host's Worker answers its wake route with this company's token"),
89
+ token: z
90
+ .string()
91
+ .nullish()
92
+ .describe("The secret that holds the host's wakeup token, a `secret(\"NAME\")`: the one the host's secrets give as SANOMA_WAKEUP_TOKEN, by default SANOMA_WAKEUP_TOKEN itself. A state gives its name when it is another"),
89
93
  });
90
94
  /**
91
95
  * `cloudflare.alarm({ host })`: the Durable Object alarm of the host's own Worker, which starts
92
96
  * the container at the time the worker registered and calls its `/wake`. The alarm
93
97
  * is the host's object's, so it has no id of its own: its binding is the host's URL, and a new
94
98
  * host is a new alarm. The host's `secrets` must carry `SANOMA_WAKEUP_TOKEN`, which `create`
95
- * mints when none is stored.
99
+ * mints when none is stored, under the store name `token` gives (`secret("NAME")`), by default
100
+ * `SANOMA_WAKEUP_TOKEN`: a workflow that makes hosts for several companies names one per company.
101
+ * The state keeps that name, so a `read`, `update` or `delete` given the state sends the same token.
96
102
  */
97
103
  export const alarm = defineResource({
98
104
  vendor: "cloudflare",
@@ -102,7 +108,7 @@ export const alarm = defineResource({
102
108
  fields: {
103
109
  immutable: ["host"],
104
110
  vendorOwned: ["at", "rang", "healthy"],
105
- writeOnly: [],
111
+ writeOnly: ["token"],
106
112
  references: { host: { type: ["cloudflare.container"], by: ["url"] } },
107
113
  },
108
114
  bindingId: (state) => String(state.host),
@@ -114,7 +120,106 @@ export const alarm = defineResource({
114
120
  outputs: (state) => ({ healthy: state.healthy === true }),
115
121
  },
116
122
  });
117
- const types = defineConnector("cloudflare", { container, alarm }, vendorInfo(import.meta.url));
123
+ /** A set's label: lower case letters, digits, `-` and `_`. */
124
+ const LABEL = /^[a-z0-9](?:[a-z0-9_-]{0,61}[a-z0-9])?$/;
125
+ const recordsSchema = z.object({
126
+ zone: z
127
+ .string()
128
+ .regex(DOMAIN_NAME, "a zone's name, lower case, such as `example.com`")
129
+ .describe("The zone's name, as the account holds it: `example.com`"),
130
+ name: z
131
+ .string()
132
+ .regex(LABEL, "lower case letters, digits, `-` and `_`")
133
+ .describe("The set's label, such as `resend`: its identity is `<zone>/<name>`, and each record it writes carries the comment `sanoma:<zone>/<name>`"),
134
+ records: z
135
+ .array(dnsRecord)
136
+ // Empty, it would read back as gone, a create on every apply.
137
+ .min(1, "a set holds at least one record: leave it out rather than empty")
138
+ .describe("The records, compared as a set: a list, or a declared `resend.domain`, whose `dns_records` they are then"),
139
+ zone_id: z.string().nullish().describe("The zone's id, which Cloudflare sets"),
140
+ record_ids: z
141
+ .array(z.string())
142
+ .nullish()
143
+ .describe("Each record's id, which Cloudflare sets, in the order of `records` as a read gives them"),
144
+ });
145
+ /** A TXT record's text as a zone may show it, quoted (`"v=spf1 …"`) or split in quoted strings (`"…" "…"`), unquoted. */
146
+ function unquoted(content) {
147
+ const text = content.trim();
148
+ if (!/^"(?:[^"\\]|\\.)*"(?:\s+"(?:[^"\\]|\\.)*")*$/s.test(text))
149
+ return content;
150
+ return [...text.matchAll(/"((?:[^"\\]|\\.)*)"/gs)].map(([, part = ""]) => part.replace(/\\(.)/gs, "$1")).join("");
151
+ }
152
+ /** A CAA record's content, `0 issue "letsencrypt.org"`, as the fields Cloudflare takes; undefined when it is not one. */
153
+ export function caaData(content) {
154
+ const parts = /^(\d+)\s+(\S+)\s+(.+)$/s.exec(content.trim());
155
+ if (!parts)
156
+ return undefined;
157
+ const [, flags, tag, value] = parts;
158
+ // A tag in any case is the same tag (RFC 8659); Cloudflare shows it in lower case.
159
+ return { flags: Number(flags), tag: tag.toLowerCase(), value: value.replace(/^"(.*)"$/s, "$1") };
160
+ }
161
+ /** A CAA record's fields as its content, as Cloudflare shows them: `caaData`'s reverse. */
162
+ export const caaContent = ({ flags, tag, value }) => `${flags} ${tag} "${value}"`;
163
+ /** A record's content in its compared form: a TXT record's text unquoted, a CAA record's value quoted. */
164
+ function contentOf(type, content) {
165
+ if (type === "TXT")
166
+ return unquoted(content);
167
+ const caa = type === "CAA" ? caaData(content) : undefined;
168
+ return caa ? caaContent(caa) : content;
169
+ }
170
+ /** A canonical record's key: two records of one key are the same record. */
171
+ export const keyOf = ({ type, name, content, priority, ttl }) => JSON.stringify([type, name, content, priority, ttl]);
172
+ /**
173
+ * One record in the one form a set compares: its name in lower case with no trailing dot, a TXT
174
+ * record's text unquoted, a CAA record's value quoted, a priority for an MX record only, and a
175
+ * ttl, 1 (automatic) unless given.
176
+ */
177
+ export const canonicalRecord = ({ type, name, content, priority, ttl }) => ({
178
+ type: type,
179
+ name: name.toLowerCase().replace(/\.$/, ""),
180
+ content: contentOf(type, content),
181
+ ...(type === "MX" && typeof priority === "number" && { priority }),
182
+ ttl: ttl ?? 1,
183
+ });
184
+ const recordsFields = {
185
+ immutable: ["zone", "name"],
186
+ vendorOwned: ["zone_id", "record_ids"],
187
+ writeOnly: [],
188
+ unordered: ["records"],
189
+ references: { records: { type: ["resend.domain"], from: "dns_records" } },
190
+ };
191
+ /** A state or a declaration with its records in their compared form (`canonicalRecord`), each once, as a plan has them. */
192
+ const canonicalSet = (state) => {
193
+ if (!Array.isArray(state.records))
194
+ return state;
195
+ const records = state.records.map((record) => canonicalRecord(record));
196
+ return { ...state, records: [...new Map(records.map((r) => [keyOf(r), r])).values()] };
197
+ };
198
+ /**
199
+ * `cloudflare.records({ zone, name, records })`: a set of DNS records in one of the account's
200
+ * zones, `<zone>/<name>`, compared as a set. Each record it writes carries the comment
201
+ * `sanoma:<zone>/<name>`, which is how a read finds them; a record without it is never changed or
202
+ * deleted, except one identical to a declared record in type, name, content (and an MX record's
203
+ * priority), which is adopted (its comment replaced by the mark), and is deleted with the set from then on.
204
+ * `records` may name a `resend.domain`, whose `dns_records` it then takes (`from`): a sending
205
+ * domain's records go to the zone that holds them.
206
+ */
207
+ export const records = defineResource({
208
+ vendor: "cloudflare",
209
+ type: "records",
210
+ title: "DNS records",
211
+ identity: "zone/name",
212
+ schema: recordsSchema,
213
+ fields: recordsFields,
214
+ find: ({ zone, name }) => `${zone}/${name}`,
215
+ bindingId: (state) => `${String(state.zone)}/${String(state.name)}`,
216
+ // Drift compares each record as `canonicalRecord` has it: a ttl left out is automatic, as read.
217
+ normalize: (state, desired) => compareDeclared(recordsFields, canonicalSet(state), canonicalSet(desired)),
218
+ writable: true,
219
+ // Records hold no data: a set deleted by hand is written again without asking.
220
+ stateless: true,
221
+ });
222
+ const types = defineConnector("cloudflare", { container, alarm, records }, vendorInfo(import.meta.url));
118
223
  /** `alarm.register`, in the alarm type's own group, beside its resource operations. */
119
224
  const registerSpec = {
120
225
  register: {
@@ -1 +1 @@
1
- {"version":3,"file":"connector.js","sourceRoot":"","sources":["../src/connector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,eAAe,EAAE,cAAc,EAAe,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAChH,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;GAKG;AAEH,8GAA8G;AAC9G,MAAM,IAAI,GAAG,wCAAwC,CAAC;AAEtD,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/B,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,CAAC,QAAQ,CAAC,uDAAuD,CAAC;IAC1F,IAAI,EAAE,CAAC;SACJ,MAAM,EAAE;SACR,KAAK,CAAC,IAAI,EAAE,mDAAmD,CAAC;SAChE,QAAQ,CACP,0GAA0G,CAC3G;IACH,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,KAAK,CAAC,oDAAoD,EAAE,oCAAoC,CAAC;SACjG,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,0CAA0C,CAAC;SACzF,QAAQ,CACP,8IAA8I,CAC/I;IACH,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,qEAAqE,CAAC;IACzG,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,oDAAoD,CAAC;IACrF,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,CAAC,QAAQ,CAAC,iDAAiD,CAAC;IAC1F,OAAO,EAAE,CAAC;SACP,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,oBAAoB,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;SAC1D,OAAO,EAAE;SACT,QAAQ,CACP,2HAA2H,CAC5H;IACH,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,+DAA+D,CAAC;IACnH,OAAO,EAAE,CAAC;SACP,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QAClE,6DAA6D;SAC5D,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,uCAAuC,CAAC;SAC7F,OAAO,EAAE;SACT,QAAQ,CAAC,sCAAsC,CAAC;IACnD,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,CAAC,QAAQ,CAAC,wDAAwD,CAAC;IAC5F,OAAO,EAAE,CAAC;SACP,OAAO,EAAE;SACT,OAAO,EAAE;SACT,QAAQ,CAAC,oGAAoG,CAAC;CAClH,CAAC,CAAC;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,cAAc,CAAC;IACtC,MAAM,EAAE,YAAY;IACpB,IAAI,EAAE,WAAW;IACjB,KAAK,EAAE,WAAW;IAClB,QAAQ,EAAE,MAAM;IAChB,MAAM,EAAE,eAAe;IACvB,MAAM,EAAE;QACN,SAAS,EAAE,CAAC,MAAM,CAAC;QACnB,WAAW,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,CAAC;QACrC,SAAS,EAAE,CAAC,SAAS,CAAC;QACtB,UAAU,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE,EAAE,EAAE,CAAC,gBAAgB,CAAC,EAAE,EAAE;KAC9E;IACD,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI;IACxB,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;IACxC,QAAQ,EAAE,IAAI;IACd,IAAI,EAAE;QACJ,YAAY,EAAE,EAAE,QAAQ,EAAE,kBAAkB,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE;QAC1G,MAAM,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE;QAC9F,QAAQ,EAAE,UAAU;QACpB,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YACnB,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,EAAE,CAAC;YAC5B,GAAG,CAAC,OAAO,KAAK,CAAC,QAAQ,KAAK,QAAQ,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC;YACvE,OAAO,EAAE,KAAK,CAAC,OAAO,KAAK,IAAI;SAChC,CAAC;KACH;CACF,CAAC,CAAC;AAEH,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3B,IAAI,EAAE,CAAC;SACJ,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,QAAQ,CAAC,wFAAwF,CAAC;IACrG,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,CAAC,QAAQ,CAAC,6DAA6D,CAAC;IAChG,IAAI,EAAE,CAAC;SACJ,MAAM,EAAE;SACR,OAAO,EAAE;SACT,QAAQ,CAAC,oFAAoF,CAAC;IACjG,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC,QAAQ,CAAC,4EAA4E,CAAC;CACtH,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,cAAc,CAAC;IAClC,MAAM,EAAE,YAAY;IACpB,IAAI,EAAE,OAAO;IACb,KAAK,EAAE,OAAO;IACd,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE;QACN,SAAS,EAAE,CAAC,MAAM,CAAC;QACnB,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC;QACtC,SAAS,EAAE,EAAE;QACb,UAAU,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,sBAAsB,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE;KACtE;IACD,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;IACxC,QAAQ,EAAE,IAAI;IACd,iGAAiG;IACjG,MAAM,EAAE;QACN,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;QACxB,QAAQ,EAAE,2BAA2B;QACrC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;KAC1D;CACF,CAAC,CAAC;AAEH,MAAM,KAAK,GAAG,eAAe,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,UAAU,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE/F,uFAAuF;AACvF,MAAM,YAAY,GAAG;IACnB,QAAQ,EAAE;QACR,MAAM,EAAE,OAAO;QACf,qEAAqE;QACrE,UAAU,EAAE,IAAI;QAChB,WAAW,EAAE,+EAA+E;QAC5F,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,iCAAiC,CAAC,EAAE,CAAC;QACrG,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;KACrB;CAC+B,CAAC;AAEnC,MAAM,QAAQ,GAAG,eAAe,CAAC,YAAY,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,CAAC;AAQxE;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,GAAG,KAAK;IACR,KAAK,EAAE,EAAE,GAAG,KAAK,CAAC,KAAK,EAAE,GAAG,QAAQ,CAAC,KAAK,EAAE;CACU,CAAC"}
1
+ {"version":3,"file":"connector.js","sourceRoot":"","sources":["../src/connector.ts"],"names":[],"mappings":"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"}
package/dist/driver.d.ts CHANGED
@@ -8,8 +8,8 @@ export interface CloudflareDriverOptions {
8
8
  }
9
9
  /**
10
10
  * The real Cloudflare account at `CLOUDFLARE_ACCOUNT_ID`, with `CLOUDFLARE_API_TOKEN`: the
11
- * company's Worker and its container application (`cloudflare.container`), and its object's alarm
12
- * over the Worker's wake route with the company's `SANOMA_WAKEUP_TOKEN` (`cloudflare.alarm`). See
13
- * the README.
11
+ * company's Worker and its container application (`cloudflare.container`), its object's alarm
12
+ * over the Worker's wake route with the company's `SANOMA_WAKEUP_TOKEN` (`cloudflare.alarm`), and
13
+ * sets of DNS records in the account's zones (`cloudflare.records`). See the README.
14
14
  */
15
15
  export declare function cloudflareDriver(options?: CloudflareDriverOptions): import("@sanoma-ai/workflows").Driver;
package/dist/driver.js CHANGED
@@ -2,10 +2,13 @@ import { defineDriver, DriverError, retryableStatus } from "@sanoma-ai/workflows
2
2
  import { z } from "zod";
3
3
  import { cloudflareOps } from "./connector.js";
4
4
  import { alarmOps, containerOps, refused } from "./ops.js";
5
+ import { recordsOps } from "./records.js";
5
6
  import { CLOUDFLARE_REGISTRY, copyImage } from "./registry.js";
6
7
  /** The environment variables the driver reads, on every call. */
7
8
  const env = z.object({
8
- CLOUDFLARE_API_TOKEN: z.string().describe("an API token with Workers Scripts Edit and Workers Containers Edit"),
9
+ CLOUDFLARE_API_TOKEN: z
10
+ .string()
11
+ .describe("an API token with Workers Scripts Edit and Workers Containers Edit, and Zone Read and Zone → DNS → Edit on the zones of custom domains and DNS records"),
9
12
  CLOUDFLARE_ACCOUNT_ID: z
10
13
  .string()
11
14
  .regex(/^[0-9a-f]{32}$/, "an account id is 32 hexadecimal digits")
@@ -151,14 +154,59 @@ function accountApi(timeoutMs, subdomains) {
151
154
  async deleteApplication(id) {
152
155
  await found(`DELETE containers/applications/${id}`, (cf) => cf.containers.applications.delete(id, { account_id }));
153
156
  },
157
+ zone: (name) => calling(`GET zones?name=${name}`, async (cf) => {
158
+ const page = await cf.zones.list({ name, account: { id: account_id } });
159
+ return page.result.find((zone) => zone.name === name)?.id;
160
+ }),
161
+ domains: (script) => calling(`GET workers/domains?service=${script}`, async (cf) => {
162
+ const page = await cf.workers.domains.list({ account_id, service: script });
163
+ return page.result.map(({ id, hostname }) => ({ id, hostname }));
164
+ }),
165
+ async attachDomain(script, hostname, zone_id) {
166
+ await calling(`PUT workers/domains ${hostname}`, (cf) => cf.workers.domains.update({ account_id, hostname, service: script, zone_id }));
167
+ },
168
+ async detachDomain(id) {
169
+ await found(`DELETE workers/domains/${id}`, (cf) => cf.workers.domains.delete(id, { account_id }));
170
+ },
171
+ dnsRecords: (zone_id, comment) => calling(`GET zones/${zone_id}/dns_records?comment.exact=${comment}`, (cf) => everyRecord(cf, { zone_id, comment: { exact: comment } })),
172
+ dnsRecordsAt: (zone_id, name) => calling(`GET zones/${zone_id}/dns_records?name.exact=${name}`, (cf) => everyRecord(cf, { zone_id, name: { exact: name } })),
173
+ writeDns: (zone_id, { posts, patches, deletes }) => calling(`POST zones/${zone_id}/dns_records/batch`, async (cf) => {
174
+ // The SDK's params are a union per record type; `RecordWrite` is the fields each shares.
175
+ const result = await cf.dns.records.batch({ zone_id, posts, patches, deletes });
176
+ return { patches: (result.patches ?? []).map(zoneRecord), posts: (result.posts ?? []).map(zoneRecord) };
177
+ }),
154
178
  send: sender(timeoutMs),
155
179
  };
156
180
  }
181
+ /**
182
+ * Every record a list finds, page after page as its first page counts them (the SDK's own
183
+ * iteration asks for one page more, which answers empty).
184
+ */
185
+ async function everyRecord(cf, query) {
186
+ const first = await cf.dns.records.list(query);
187
+ const found = [...first.result];
188
+ const pages = first.result_info.total_pages ?? 1;
189
+ for (let page = 2; page <= pages; page++) {
190
+ found.push(...(await cf.dns.records.list({ ...query, page })).result);
191
+ }
192
+ return found.map(zoneRecord);
193
+ }
194
+ /** A record of Cloudflare's reply, as the fields read here. */
195
+ const zoneRecord = (record) => ({
196
+ id: record.id,
197
+ type: record.type,
198
+ name: record.name,
199
+ content: record.content ?? "",
200
+ ttl: record.ttl,
201
+ ...(record.priority !== undefined && { priority: record.priority }),
202
+ ...(typeof record.comment === "string" && { comment: record.comment }),
203
+ ...(record.proxied && { proxied: true }),
204
+ });
157
205
  /**
158
206
  * The real Cloudflare account at `CLOUDFLARE_ACCOUNT_ID`, with `CLOUDFLARE_API_TOKEN`: the
159
- * company's Worker and its container application (`cloudflare.container`), and its object's alarm
160
- * over the Worker's wake route with the company's `SANOMA_WAKEUP_TOKEN` (`cloudflare.alarm`). See
161
- * the README.
207
+ * company's Worker and its container application (`cloudflare.container`), its object's alarm
208
+ * over the Worker's wake route with the company's `SANOMA_WAKEUP_TOKEN` (`cloudflare.alarm`), and
209
+ * sets of DNS records in the account's zones (`cloudflare.records`). See the README.
162
210
  */
163
211
  export function cloudflareDriver(options = {}) {
164
212
  const { timeoutMs = 30_000, startTimeoutMs = 10 * 60_000, pollMs = 2_000 } = options;
@@ -166,6 +214,10 @@ export function cloudflareDriver(options = {}) {
166
214
  const api = () => accountApi(timeoutMs, subdomains);
167
215
  // The alarm only calls the Worker, so it needs no account.
168
216
  const worker = { send: sender(timeoutMs) };
169
- return defineDriver(cloudflareOps, { container: containerOps(api, { startTimeoutMs, pollMs }), alarm: alarmOps(() => worker) }, { env });
217
+ return defineDriver(cloudflareOps, {
218
+ container: containerOps(api, { startTimeoutMs, pollMs }),
219
+ alarm: alarmOps(() => worker),
220
+ records: recordsOps(api),
221
+ }, { env });
170
222
  }
171
223
  //# sourceMappingURL=driver.js.map