domainkit 0.7.0 → 0.9.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.
Files changed (165) hide show
  1. package/README.md +214 -39
  2. package/dist/Approval.mjs +34 -0
  3. package/dist/Approval.mjs.map +1 -0
  4. package/dist/Cleanup.mjs +97 -0
  5. package/dist/Cleanup.mjs.map +1 -0
  6. package/dist/Cloudflare.mjs +225 -0
  7. package/dist/Cloudflare.mjs.map +1 -0
  8. package/dist/Connect.mjs +463 -0
  9. package/dist/Connect.mjs.map +1 -0
  10. package/dist/Custody.mjs +64 -0
  11. package/dist/Custody.mjs.map +1 -0
  12. package/dist/DnsRecord.mjs +213 -0
  13. package/dist/DnsRecord.mjs.map +1 -0
  14. package/dist/DomainKit.mjs +34 -0
  15. package/dist/DomainKit.mjs.map +1 -0
  16. package/dist/DomainName.mjs +100 -0
  17. package/dist/DomainName.mjs.map +1 -0
  18. package/dist/Plan.mjs +102 -0
  19. package/dist/Plan.mjs.map +1 -0
  20. package/dist/Principal.mjs +23 -0
  21. package/dist/Principal.mjs.map +1 -0
  22. package/dist/Provider.mjs +130 -0
  23. package/dist/Provider.mjs.map +1 -0
  24. package/dist/Providers.mjs +40 -0
  25. package/dist/Providers.mjs.map +1 -0
  26. package/dist/Provision.mjs +93 -0
  27. package/dist/Provision.mjs.map +1 -0
  28. package/dist/Reason.mjs +162 -0
  29. package/dist/Reason.mjs.map +1 -0
  30. package/dist/Receipt.mjs +71 -0
  31. package/dist/Receipt.mjs.map +1 -0
  32. package/dist/Resolver.mjs +75 -0
  33. package/dist/Resolver.mjs.map +1 -0
  34. package/dist/Server.mjs +516 -0
  35. package/dist/Server.mjs.map +1 -0
  36. package/dist/Storage.mjs +236 -0
  37. package/dist/Storage.mjs.map +1 -0
  38. package/dist/Testing.mjs +226 -0
  39. package/dist/Testing.mjs.map +1 -0
  40. package/dist/Transport.mjs +187 -0
  41. package/dist/Transport.mjs.map +1 -0
  42. package/dist/Vercel.mjs +116 -0
  43. package/dist/Vercel.mjs.map +1 -0
  44. package/dist/Verify.mjs +292 -0
  45. package/dist/Verify.mjs.map +1 -0
  46. package/dist/{rolldown-runtime-D7D4PA-g.mjs → _virtual/_rolldown/runtime.mjs} +1 -1
  47. package/dist/entry/client.mjs +2 -0
  48. package/dist/entry/server.mjs +2 -0
  49. package/dist/entry/testing.mjs +3 -0
  50. package/dist/index.mjs +33 -356
  51. package/dist/index.mjs.map +1 -1
  52. package/dist/internal/aes.mjs +61 -0
  53. package/dist/internal/aes.mjs.map +1 -0
  54. package/dist/internal/attempts.mjs +284 -0
  55. package/dist/internal/attempts.mjs.map +1 -0
  56. package/dist/internal/cloudflare/client.mjs +100 -0
  57. package/dist/internal/cloudflare/client.mjs.map +1 -0
  58. package/dist/internal/cloudflare/protocol.mjs +59 -0
  59. package/dist/internal/cloudflare/protocol.mjs.map +1 -0
  60. package/dist/internal/cloudflare/records.mjs +156 -0
  61. package/dist/internal/cloudflare/records.mjs.map +1 -0
  62. package/dist/internal/config.mjs +13 -0
  63. package/dist/internal/config.mjs.map +1 -0
  64. package/dist/internal/conformance/provider.mjs +207 -0
  65. package/dist/internal/conformance/provider.mjs.map +1 -0
  66. package/dist/internal/conformance/storage.mjs +428 -0
  67. package/dist/internal/conformance/storage.mjs.map +1 -0
  68. package/dist/internal/digest.mjs +25 -0
  69. package/dist/internal/digest.mjs.map +1 -0
  70. package/dist/internal/doh.mjs +152 -0
  71. package/dist/internal/doh.mjs.map +1 -0
  72. package/dist/internal/error.mjs +69 -0
  73. package/dist/internal/error.mjs.map +1 -0
  74. package/dist/internal/http.mjs +89 -0
  75. package/dist/internal/http.mjs.map +1 -0
  76. package/dist/internal/ids.mjs +8 -0
  77. package/dist/internal/ids.mjs.map +1 -0
  78. package/dist/internal/oauth.mjs +113 -0
  79. package/dist/internal/oauth.mjs.map +1 -0
  80. package/dist/internal/planner.mjs +88 -0
  81. package/dist/internal/planner.mjs.map +1 -0
  82. package/dist/internal/provider-async.mjs +83 -0
  83. package/dist/internal/provider-async.mjs.map +1 -0
  84. package/dist/internal/storage-memory.mjs +302 -0
  85. package/dist/internal/storage-memory.mjs.map +1 -0
  86. package/dist/internal/vercel/client.mjs +122 -0
  87. package/dist/internal/vercel/client.mjs.map +1 -0
  88. package/dist/internal/vercel/protocol.mjs +80 -0
  89. package/dist/internal/vercel/protocol.mjs.map +1 -0
  90. package/dist/internal/vercel/records.mjs +151 -0
  91. package/dist/internal/vercel/records.mjs.map +1 -0
  92. package/dist/package.mjs +6 -0
  93. package/dist/package.mjs.map +1 -0
  94. package/dist/types/Approval.d.ts +26 -0
  95. package/dist/types/Cleanup.d.ts +41 -0
  96. package/dist/types/Cloudflare.d.ts +36 -0
  97. package/dist/types/Connect.d.ts +506 -0
  98. package/dist/types/Custody.d.ts +38 -0
  99. package/dist/types/DnsRecord.d.ts +158 -0
  100. package/dist/types/DomainKit.d.ts +31 -0
  101. package/dist/types/DomainName.d.ts +27 -0
  102. package/dist/types/Plan.d.ts +76 -0
  103. package/dist/types/Principal.d.ts +18 -0
  104. package/dist/types/Provider.d.ts +248 -0
  105. package/dist/types/Providers.d.ts +16 -0
  106. package/dist/types/Provision.d.ts +75 -0
  107. package/dist/types/Reason.d.ts +118 -0
  108. package/dist/types/Receipt.d.ts +55 -0
  109. package/dist/types/Resolver.d.ts +135 -0
  110. package/dist/types/Server.d.ts +6523 -0
  111. package/dist/types/Storage.d.ts +314 -0
  112. package/dist/types/Testing.d.ts +84 -0
  113. package/dist/types/Transport.d.ts +124 -0
  114. package/dist/types/Vercel.d.ts +25 -0
  115. package/dist/types/Verify.d.ts +109 -0
  116. package/dist/types/entry/client.d.ts +2 -0
  117. package/dist/types/entry/server.d.ts +2 -0
  118. package/dist/types/entry/testing.d.ts +3 -0
  119. package/dist/types/index.d.ts +21 -0
  120. package/dist/types/internal/aes.d.ts +8 -0
  121. package/dist/types/internal/attempts.d.ts +40 -0
  122. package/dist/types/internal/cloudflare/client.d.ts +23 -0
  123. package/dist/types/internal/cloudflare/protocol.d.ts +71 -0
  124. package/dist/types/internal/cloudflare/records.d.ts +8 -0
  125. package/dist/types/internal/config.d.ts +4 -0
  126. package/dist/types/internal/conformance/provider.d.ts +12 -0
  127. package/dist/types/internal/conformance/storage.d.ts +14 -0
  128. package/dist/types/internal/digest.d.ts +10 -0
  129. package/dist/types/internal/doh.d.ts +19 -0
  130. package/dist/types/internal/error.d.ts +24 -0
  131. package/dist/types/internal/http.d.ts +27 -0
  132. package/dist/types/internal/ids.d.ts +3 -0
  133. package/dist/types/internal/oauth.d.ts +58 -0
  134. package/dist/types/internal/planner.d.ts +22 -0
  135. package/dist/types/internal/provider-async.d.ts +78 -0
  136. package/dist/types/internal/storage-memory.d.ts +2 -0
  137. package/dist/types/internal/vercel/client.d.ts +29 -0
  138. package/dist/types/internal/vercel/protocol.d.ts +124 -0
  139. package/dist/types/internal/vercel/records.d.ts +7 -0
  140. package/package.json +88 -12
  141. package/dist/canonical-json-CQYKsZw7.mjs +0 -310
  142. package/dist/canonical-json-CQYKsZw7.mjs.map +0 -1
  143. package/dist/connect-10vHuShV.d.mts +0 -1401
  144. package/dist/connect-oI8hcMxc.mjs +0 -185
  145. package/dist/connect-oI8hcMxc.mjs.map +0 -1
  146. package/dist/connection-C0xsU41C.d.mts +0 -186
  147. package/dist/connection-api-DW7xUJdj.d.mts +0 -26
  148. package/dist/index.d.mts +0 -882
  149. package/dist/managed-dns-connections-C3A_GBaz.mjs +0 -99
  150. package/dist/managed-dns-connections-C3A_GBaz.mjs.map +0 -1
  151. package/dist/promise.d.mts +0 -378
  152. package/dist/promise.mjs +0 -598
  153. package/dist/promise.mjs.map +0 -1
  154. package/dist/resolver-DPJs8h0a.mjs +0 -859
  155. package/dist/resolver-DPJs8h0a.mjs.map +0 -1
  156. package/dist/resolver-DRrLm_hB.d.mts +0 -3734
  157. package/dist/server.d.mts +0 -128
  158. package/dist/server.mjs +0 -319
  159. package/dist/server.mjs.map +0 -1
  160. package/dist/testing.d.mts +0 -62
  161. package/dist/testing.mjs +0 -507
  162. package/dist/testing.mjs.map +0 -1
  163. package/dist/verify-C3V_GtzL.d.mts +0 -1268
  164. package/dist/verify-DDnK8U7C.mjs +0 -2396
  165. package/dist/verify-DDnK8U7C.mjs.map +0 -1
package/README.md CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  Add custom domains to a TypeScript SaaS application.
4
4
 
5
- DomainKit turns DNS requirements into plans you can review, requires approval for the exact plan
6
- before a write, records applied changes in receipts, and uses those receipts to plan cleanup. It
7
- supports Cloudflare and Vercel without making either provider's API your product architecture.
5
+ DomainKit turns DNS requirements into plans a customer can review, applies only the plan digest
6
+ they approved, keeps a receipt of every write, and plans cleanup from that receipt. Cloudflare and
7
+ Vercel are built in; a provider is one declarative value, so tokens-only providers are the same
8
+ shape minus the OAuth case.
8
9
 
9
10
  ## Install
10
11
 
@@ -14,59 +15,233 @@ npm install domainkit effect@rc
14
15
 
15
16
  Node.js 24.10 or newer and Effect 4 are required.
16
17
 
17
- ## Plan, approve, and apply
18
+ ## Plan, approve, apply
19
+
20
+ ```ts
21
+ import { Effect, Match } from "effect";
22
+ import { DnsRecord, DomainKit, Principal, Provision, Verify } from "domainkit";
23
+ import { Testing } from "domainkit/testing";
24
+
25
+ const requirements = [
26
+ DnsRecord.cname({ name: "app.example.com", target: "edge.acme.dev", purpose: "Serve your site" }),
27
+ DnsRecord.txt({
28
+ name: "_acme.app.example.com",
29
+ value: "acme-verify=7f3a",
30
+ purpose: "Prove ownership",
31
+ }),
32
+ ];
33
+
34
+ const program = Effect.gen(function* () {
35
+ const plan = yield* Provision.plan({ domain: "app.example.com", requirements });
36
+ // ^ operations: [Create CNAME, Noop TXT] with a digest the customer approves
37
+
38
+ const approval = yield* Provision.approve(plan);
39
+ const receipt = yield* Provision.apply(approval);
40
+ // ^ status: "complete" | "partial", one outcome per operation, safe to retry
41
+
42
+ const readiness = yield* Verify.observe({ domain: "app.example.com" });
43
+ return { receipt, ready: readiness.overall === "ready", nextCheckAt: readiness.nextCheckAt };
44
+ }).pipe(
45
+ Effect.catchTag("DomainKitError", (error) =>
46
+ Match.value(error.reason).pipe(
47
+ Match.tag("Conflict", ({ operations }) =>
48
+ Effect.fail(`Fix ${operations.length} conflicting record(s) first`),
49
+ ),
50
+ Match.tag("Stale", () => Effect.fail("Provider changed under us; plan again")),
51
+ Match.orElse(() => Effect.fail(error.message)),
52
+ ),
53
+ ),
54
+ );
55
+
56
+ export const main = program.pipe(
57
+ Effect.provideService(Principal.Service, { ownerId: "org_42", actorId: "user_7" }),
58
+ Effect.provide(
59
+ DomainKit.layerMemory({ providers: [Testing.provider({ zones: ["example.com"] })] }),
60
+ ),
61
+ );
62
+ ```
63
+
64
+ Plans are additive and fail closed: exact records are `Noop`, missing records are `Create`, and
65
+ incompatible state is `Conflict`. DomainKit never updates or deletes a record it did not create,
66
+ and cleanup is its own plan, approval, and receipt built from the apply receipt.
67
+
68
+ Every step is a stored attempt, so a host can render the plan in one request, collect consent in
69
+ another, and apply in a third; retrying any step replays its result. A customer who declines calls
70
+ `Provision.reject`, which closes the attempt for good and leaves the domain free for a new plan. Every failure is one
71
+ `DomainKit.Error` whose `reason` you match on; `category`, `isRetryable`, and `httpStatus` derive
72
+ from it.
73
+
74
+ ## Wire it into your app
75
+
76
+ ```ts
77
+ import { Config, Layer } from "effect";
78
+ import { Cloudflare, Custody, DomainKit, Vercel } from "domainkit";
79
+
80
+ export const DomainKitLive = DomainKit.layer({
81
+ providers: [
82
+ Cloudflare.provider({
83
+ oauth: {
84
+ clientId: Config.string("CF_CLIENT_ID"),
85
+ clientSecret: Config.redacted("CF_CLIENT_SECRET"),
86
+ },
87
+ }),
88
+ Vercel.provider(), // tokens only
89
+ ],
90
+ }).pipe(
91
+ // `provideMerge`, not `provide`: `domainkit/server`'s handlers read attempts and receipts
92
+ // straight from Storage, so the layer they are given has to still carry it.
93
+ Layer.provideMerge(Layer.mergeAll(YourStorage, Custody.layerConfig())),
94
+ );
95
+ ```
96
+
97
+ The host provides two services beneath the layer and one per request:
98
+
99
+ - `Storage` — every durable row (authorizations with sealed credentials, connections, attachments,
100
+ continuations, attempts, readiness). `Storage.layerMemory` is for tests; the Postgres
101
+ implementation lives in `@domainkit/capsuledb`; `Storage.layerFromAsync` wraps a Promise-shaped
102
+ implementation of your own. `Testing.conformance.storage` checks any implementation.
103
+ - `Custody` — seals credentials before Storage sees them. `Custody.layerConfig()` reads a 32-byte
104
+ key from `DOMAINKIT_CUSTODY_KEY`; `Custody.layerFromAsync` wraps a KMS.
105
+ - `Principal` — `{ ownerId, actorId }` per request. Every Storage read and write is scoped by it, so
106
+ cross-tenant access is a type error, not a runtime check.
107
+
108
+ `Connect.start` connects a provider (a token in one call, OAuth or a marketplace integration via a
109
+ redirect and `Connect.complete`), attaches domains, refreshes credentials before they expire, and
110
+ revokes them on disconnect. `Verify.observe` reads the provider and public DNS, stores readiness
111
+ per requirement, and tells you when to look again; pass `requirements` to observe records a
112
+ customer applies by hand on a domain with no attachment.
113
+
114
+ ## Mount the routes
18
115
 
19
116
  ```ts
20
- import { Digest, DnsRecord, DomainName, Provisioning } from "domainkit";
21
- import { InMemoryDnsProvider } from "domainkit/testing";
22
117
  import { Effect, Layer } from "effect";
118
+ import { HttpApi, HttpApiBuilder } from "effect/unstable/httpapi";
119
+ import { DomainKit, Reason } from "domainkit";
120
+ import { Server } from "domainkit/server";
121
+
122
+ // Verify a credential you issued and look the tenant up yourself. A request never names its own
123
+ // `ownerId`, and one you cannot attribute fails closed. Read it from a cookie: `/callback/:provider`
124
+ // is a browser navigation, so only what the browser sends by itself arrives with it.
125
+ const IdentityLive = Layer.succeed(Server.Identity)({
126
+ principal: (request) =>
127
+ Effect.flatMap(yourSessions.verify(request.cookies.session), (session) =>
128
+ session === null
129
+ ? Effect.fail(
130
+ new DomainKit.Error({ reason: new Reason.Unauthenticated({ message: "No session" }) }),
131
+ )
132
+ : Effect.succeed({ ownerId: session.orgId, actorId: session.userId }),
133
+ ),
134
+ // Optional: which routes this principal may reach. Members read, administrators write.
135
+ authorize: (principal, endpoint) =>
136
+ principal.actorId === "admin" || !writeRoutes.has(endpoint)
137
+ ? Effect.void
138
+ : Effect.fail(
139
+ new DomainKit.Error({
140
+ reason: new Reason.Forbidden({ message: `${endpoint} needs an administrator` }),
141
+ }),
142
+ ),
143
+ });
144
+
145
+ export const Api = HttpApi.make("app").add(Server.group);
146
+
147
+ export const ApiLive = HttpApiBuilder.layer(Api).pipe(
148
+ Layer.provide(Server.layer(Api, { defaultReturnTo: "/settings/domains" })),
149
+ Layer.provide([DomainKitLive, IdentityLive]),
150
+ );
151
+ ```
152
+
153
+ `Server.group` is one `HttpApiGroup` with fifteen typed endpoints covering the whole lifecycle:
154
+ inspect, discover, connect, callback, attach, detach, disconnect, plan, approve, reject, apply, read
155
+ a plan or a receipt, observe, and build a cleanup plan. `Identity` is the only service you write, and
156
+ every handler derives the `Principal` for the request it is serving. `Server.group.prefix("/internal/dns")` moves
157
+ every route, and the OAuth callback URL follows the mount. `OpenApi.fromApi(Server.api)` documents
158
+ the group.
159
+
160
+ `/callback/:provider` is the one route the provider drives the browser to, so `Identity` has to
161
+ recognise a credential the browser sends on a top-level navigation.
162
+
163
+ `authorize` is optional and runs after `principal` on every request with the route's name, one of
164
+ `Server.EndpointName`. Fail it with reason `Forbidden` for the 403 a UI expects. Omit it and every
165
+ authenticated principal reaches every route, which is right when your own middleware already gates
166
+ the mount.
167
+
168
+ After an interactive connection completes, the callback redirects to the `returnTo` the flow was
169
+ started with, or to `defaultReturnTo`. The destination is resolved against the callback's own base
170
+ and must land on its origin, so neither the provider nor a crafted `returnTo` can steer the customer
171
+ off the application. Behind a proxy that rewrites `Host`, set `callbackBaseUrl` to the public base:
172
+ the request origin is one the browser never sees, and both the provider's callback URL and the
173
+ redirect follow the configured one.
23
174
 
24
- const requirement = DnsRecord.parse({
25
- _tag: "CNAME",
26
- metadata: {
27
- ownership: "customer",
28
- provenance: "product-onboarding",
29
- purpose: "tracking",
30
- },
31
- name: "track.example.com",
32
- policy: "exclusive",
33
- target: "tracking.example.net",
34
- ttl: 300,
175
+ Failures cross the wire as the `DomainKit.Error` value with the status its `reason` derives, so a
176
+ `Conflict` is a 409 carrying the conflicting operations and a `Reconnect` is a 403 naming the
177
+ connection.
178
+
179
+ Hosts that are not on Effect's HTTP stack use the Promise edge:
180
+
181
+ ```ts
182
+ const { handler, dispose } = Server.toWebHandler(Layer.mergeAll(DomainKitLive, IdentityLive), {
183
+ prefix: "/api/domainkit",
35
184
  });
185
+ ```
36
186
 
37
- const program = Effect.gen(function* () {
38
- const { plan } = yield* Provisioning.create({
39
- requirements: [requirement],
40
- target: Provisioning.Target.ExactZone({
41
- zone: DomainName.parse("example.com"),
42
- }),
187
+ ## Talk to them from the browser
188
+
189
+ ```ts
190
+ import { Transport } from "domainkit/client";
191
+
192
+ const transport = Transport.fromFetch("/api/domainkit");
193
+
194
+ // Effect at the call site, or `Transport.toAsync(transport)` for Promises.
195
+ const started =
196
+ yield *
197
+ transport.connection!.start({
198
+ domain: "app.example.com",
199
+ provider: "cloudflare",
200
+ method: Transport.Method.oauth({ returnTo: "/settings/domains" }),
43
201
  });
44
- const authorization = yield* Provisioning.authorize(plan);
45
- return yield* Provisioning.apply({ authorization, plan });
46
- }).pipe(Effect.provide(Layer.merge(InMemoryDnsProvider.layer(), Digest.webCryptoLayer)));
47
202
  ```
48
203
 
49
- Planning is additive and fail-closed: exact records become no-ops, missing records become creates,
50
- and incompatible state becomes a conflict. DomainKit never silently overwrites DNS.
204
+ `connection.discover(domain)` answers which of the customer's existing connections already reaches
205
+ the domain, so a second domain on a connected provider skips the connect step entirely.
206
+ `Snapshot.providers[].methods[]` carries each method's label, docs URL, and token fields, so a
207
+ connect form renders from the response instead of hard-coding provider names.
208
+
209
+ Capability groups are optional. A host that mounts only the connection routes declares
210
+ `Transport.fromFetch(url, { capabilities: ["connection"] })`, `Transport.capabilities(transport)`
211
+ reports what is there, and the parts of `@domainkit/react` that plan or clean up do not render.
212
+ `Transport.fromAsync` adapts a Promise-shaped transport of your own.
213
+
214
+ Failures arrive as the same `DomainKit.Error` the lifecycle raised, reason intact, so a `Conflict`
215
+ still carries its conflicting operations. A response the transport cannot read as a `DomainKit.Error`
216
+ becomes a retryable `ProviderUnavailable` naming the base URL.
217
+
218
+ ## Test against the seam
219
+
220
+ `domainkit/testing` ships `Testing.provider` (a token and OAuth provider over in-memory zones),
221
+ `Testing.resolver`, `Testing.storage`, `Testing.transport` (the whole lifecycle over an in-memory
222
+ server, recording every call), and the conformance runners, so host tests never stub global
223
+ `fetch`. Provider authors run `Testing.conformance.provider(definition, credential, zone)` against
224
+ a real account before shipping.
51
225
 
52
226
  ## Public entry points
53
227
 
54
- - `domainkit` — Effect services, schemas, plans, providers, and verification;
55
- - `domainkit/promise` — Promise API for apps that use async/await;
56
- - `domainkit/server` — provider connection routes and an async Web handler factory;
57
- - `domainkit/testing` — in-memory services and provider tests.
228
+ - `domainkit` — the Effect-native root: lifecycle services, host seams, providers, and values;
229
+ - `domainkit/server` — the mountable route group, its layers, and the wire schemas;
230
+ - `domainkit/client` — the capability-gated fetch transport and its Promise adapters;
231
+ - `domainkit/testing` — fakes and conformance runners;
232
+ - `domainkit/<Module>` — every root namespace as its own subpath (`domainkit/Principal`,
233
+ `domainkit/DnsRecord`, ...), the same module instances the root re-exports, for declaration
234
+ emit and bundlers that want the declaring module.
58
235
 
59
- Your app owns provider credentials, OAuth state, saved connections, plans, receipts, authorization,
60
- and audit history. DomainKit supplies the domain model and operations, not a hosted control plane.
61
- Cloudflare's provider namespace includes credential refresh; the host owns refresh locking,
62
- encrypted rotation, retry policy, and reconnect UX.
236
+ Your app owns identity, tenancy, persistence, keys, routes, and consent. DomainKit supplies the
237
+ lifecycle, not a hosted control plane.
63
238
 
64
239
  ## Learn more
65
240
 
66
241
  - [Executable quickstart](https://github.com/AryaLabsHQ/domainkit/blob/main/packages/domainkit/examples/effect/quickstart.ts)
67
- - [Architecture decisions](https://github.com/AryaLabsHQ/domainkit/tree/main/docs/adr)
68
- - [Provider behavior](https://domain-kit.dev/docs/reference/providers)
69
- - [Examples](https://github.com/AryaLabsHQ/domainkit/tree/main/packages/domainkit/examples)
242
+ - [Mounting the routes](https://github.com/AryaLabsHQ/domainkit/blob/main/packages/domainkit/examples/effect/server.ts)
243
+ - [Writing a provider](https://github.com/AryaLabsHQ/domainkit/blob/main/packages/domainkit/examples/effect/provider.ts)
244
+ - [Architecture decisions](https://github.com/AryaLabsHQ/domainkit/tree/main/packages/domainkit/docs/adr)
70
245
  - [Issues](https://github.com/AryaLabsHQ/domainkit/issues)
71
246
 
72
247
  ## License
@@ -0,0 +1,34 @@
1
+ import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
2
+ import { Digest, Kind, OperationId, PlanId } from "./Plan.mjs";
3
+ import { decode as decode$1 } from "./internal/error.mjs";
4
+ import { Schema } from "effect";
5
+ //#region src/Approval.ts
6
+ /**
7
+ * A principal's consent to apply a specific plan digest. `Provision.apply` and `Cleanup.apply`
8
+ * accept only an Approval. The word "authorization" is reserved for provider OAuth.
9
+ */
10
+ var Approval_exports = /* @__PURE__ */ __exportAll({
11
+ ApprovalId: () => ApprovalId,
12
+ Model: () => Model,
13
+ decode: () => decode,
14
+ encode: () => encode
15
+ });
16
+ const ApprovalId = Schema.String.pipe(Schema.brand("@domainkit/ApprovalId"));
17
+ var Model = class extends Schema.Class("@domainkit/Approval")({
18
+ id: ApprovalId,
19
+ version: Schema.Literal("domainkit.approval.v2"),
20
+ kind: Kind,
21
+ planId: PlanId,
22
+ digest: Digest,
23
+ /** Subset of the plan's write operations; the full set unless partial approval was requested. */
24
+ operationIds: Schema.Array(OperationId),
25
+ approvedBy: Schema.String,
26
+ approvedAt: Schema.DateTimeUtcFromString,
27
+ expiresAt: Schema.DateTimeUtcFromString
28
+ }) {};
29
+ const decode = (input) => decode$1(Model, input);
30
+ const encode = Schema.encodeSync(Model);
31
+ //#endregion
32
+ export { ApprovalId, Approval_exports, Model, decode, encode };
33
+
34
+ //# sourceMappingURL=Approval.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Approval.mjs","names":["Plan.Kind","Plan.PlanId","Plan.Digest","Plan.OperationId","Errors.decode"],"sources":["../src/Approval.ts"],"sourcesContent":["/**\n * A principal's consent to apply a specific plan digest. `Provision.apply` and `Cleanup.apply`\n * accept only an Approval. The word \"authorization\" is reserved for provider OAuth.\n */\nimport { Effect, Schema } from \"effect\";\n\nimport * as Errors from \"./internal/error.ts\";\nimport * as Plan from \"./Plan.ts\";\n\nexport const ApprovalId = Schema.String.pipe(Schema.brand(\"@domainkit/ApprovalId\"));\nexport type ApprovalId = typeof ApprovalId.Type;\n\nexport class Model extends Schema.Class<Model>(\"@domainkit/Approval\")({\n id: ApprovalId,\n version: Schema.Literal(\"domainkit.approval.v2\"),\n kind: Plan.Kind,\n planId: Plan.PlanId,\n digest: Plan.Digest,\n /** Subset of the plan's write operations; the full set unless partial approval was requested. */\n operationIds: Schema.Array(Plan.OperationId),\n approvedBy: Schema.String,\n approvedAt: Schema.DateTimeUtcFromString,\n expiresAt: Schema.DateTimeUtcFromString,\n}) {}\nexport type Encoded = typeof Model.Encoded;\n\nexport const decode = (input: unknown): Effect.Effect<Model, Errors.DomainKitError> =>\n Errors.decode(Model, input);\nexport const encode: (approval: Model) => Encoded = Schema.encodeSync(Model);\n"],"mappings":";;;;;;;;;;;;;;;AASA,MAAa,aAAa,OAAO,OAAO,KAAK,OAAO,MAAM,uBAAuB,CAAC;AAGlF,IAAa,QAAb,cAA2B,OAAO,MAAa,qBAAqB,CAAC,CAAC;CACpE,IAAI;CACJ,SAAS,OAAO,QAAQ,uBAAuB;CAC/C,MAAMA;CACN,QAAQC;CACR,QAAQC;;CAER,cAAc,OAAO,MAAMC,WAAgB;CAC3C,YAAY,OAAO;CACnB,YAAY,OAAO;CACnB,WAAW,OAAO;AACpB,CAAC,CAAC,CAAC,CAAC;AAGJ,MAAa,UAAU,UACrBC,SAAc,OAAO,KAAK;AAC5B,MAAa,SAAuC,OAAO,WAAW,KAAK"}
@@ -0,0 +1,97 @@
1
+ import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
2
+ import { decode } from "./DomainName.mjs";
3
+ import { InvalidInput, NotFound } from "./Reason.mjs";
4
+ import { fail } from "./internal/error.mjs";
5
+ import { applied } from "./Receipt.mjs";
6
+ import { Service as Service$1 } from "./Storage.mjs";
7
+ import { Service as Service$2 } from "./Connect.mjs";
8
+ import { assertWithin, cleanupOperation, make as make$1 } from "./internal/attempts.mjs";
9
+ import { Policy } from "./Provision.mjs";
10
+ import { Context, Effect, Layer, Option } from "effect";
11
+ //#region src/Cleanup.ts
12
+ /**
13
+ * Remove only what a receipt proves DomainKit created, with its own plan, approval, and receipt.
14
+ * Same three verbs as Provision; the plan is built from a receipt instead of requirements.
15
+ */
16
+ var Cleanup_exports = /* @__PURE__ */ __exportAll({
17
+ Service: () => Service,
18
+ apply: () => apply,
19
+ approve: () => approve,
20
+ layer: () => layer,
21
+ make: () => make,
22
+ plan: () => plan,
23
+ reject: () => reject
24
+ });
25
+ var Service = class extends Context.Service()("@domainkit/Cleanup") {};
26
+ const make = Effect.gen(function* () {
27
+ const storage = yield* Service$1;
28
+ const connect = yield* Service$2;
29
+ const attempts = make$1(storage, connect, "cleanup");
30
+ const sourceAttempt = (input) => Effect.gen(function* () {
31
+ if ("receiptId" in input) {
32
+ const attempt = yield* storage.attempts.byReceipt(input.receiptId);
33
+ if (attempt.kind !== "provisioning" || attempt.receipt === null) return yield* fail(new NotFound({
34
+ entity: "receipt",
35
+ id: input.receiptId
36
+ }));
37
+ return attempt;
38
+ }
39
+ const domain = yield* decode(input.domain);
40
+ const attachment = yield* storage.attachments.byDomain(domain);
41
+ const latest = Option.isSome(attachment) ? yield* storage.attempts.latest(attachment.value.id, "provisioning") : Option.none();
42
+ if (Option.isNone(latest) || latest.value.receipt === null) return yield* fail(new NotFound({
43
+ entity: "receipt",
44
+ id: domain
45
+ }));
46
+ return latest.value;
47
+ });
48
+ return {
49
+ plan: (input) => Effect.gen(function* () {
50
+ const policy = yield* Policy;
51
+ const source = yield* sourceAttempt(input);
52
+ const receipt = source.receipt;
53
+ if (receipt === null) return yield* fail(new NotFound({
54
+ entity: "receipt",
55
+ id: source.id
56
+ }));
57
+ const attachment = yield* storage.attachments.get(source.attachmentId);
58
+ yield* assertWithin(attachment, source.plan.operations.map((operation) => operation.record));
59
+ const { session, target } = yield* connect.session(attachment.id);
60
+ const dns = session.dns(target);
61
+ const operations = yield* Effect.forEach(applied(receipt), (applied) => Effect.gen(function* () {
62
+ const created = source.plan.operations.find((operation) => operation._tag === "Create" && operation.id === applied.operationId);
63
+ if (created === void 0) return yield* fail(new InvalidInput({
64
+ message: `Receipt ${receipt.id} proves no created record for ${applied.operationId}`,
65
+ field: "receiptId"
66
+ }));
67
+ return yield* cleanupOperation(dns, target.zone, created.id, created.record, applied.providerRecordId);
68
+ }));
69
+ return yield* attempts.record({
70
+ attachment,
71
+ target,
72
+ operations,
73
+ ttlMs: policy.planTtlMs,
74
+ sourceReceiptId: receipt.id
75
+ });
76
+ }),
77
+ approve: (plan, options) => attempts.approve(plan, options),
78
+ reject: (plan, options) => Effect.map(attempts.reject(plan, options), (attempt) => ({
79
+ plan: attempt.plan,
80
+ status: attempt.status,
81
+ approval: attempt.approval,
82
+ receipt: attempt.receipt,
83
+ rejection: attempt.rejection
84
+ })),
85
+ apply: (approval) => Effect.flatMap(Policy, (policy) => attempts.apply(approval, policy))
86
+ };
87
+ });
88
+ const layer = Layer.effect(Service)(make);
89
+ const accessor = (pick) => (...args) => Effect.flatMap(Service, (service) => pick(service)(...args));
90
+ const plan = accessor((service) => service.plan);
91
+ const approve = accessor((service) => service.approve);
92
+ const reject = accessor((service) => service.reject);
93
+ const apply = accessor((service) => service.apply);
94
+ //#endregion
95
+ export { Cleanup_exports, Service, apply, approve, layer, make, plan, reject };
96
+
97
+ //# sourceMappingURL=Cleanup.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Cleanup.mjs","names":["Storage.Service","Connect.Service","Attempts.make","Errors.fail","Reason.NotFound","DomainName.decode","Provision.Policy","Attempts.assertWithin","Receipt.applied","Reason.InvalidInput","Attempts.cleanupOperation"],"sources":["../src/Cleanup.ts"],"sourcesContent":["/**\n * Remove only what a receipt proves DomainKit created, with its own plan, approval, and receipt.\n * Same three verbs as Provision; the plan is built from a receipt instead of requirements.\n */\nimport { Context, Effect, Layer, Option } from \"effect\";\n\nimport type * as Approval from \"./Approval.ts\";\nimport * as Connect from \"./Connect.ts\";\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as DomainName from \"./DomainName.ts\";\nimport * as Attempts from \"./internal/attempts.ts\";\nimport type * as Plan from \"./Plan.ts\";\nimport type * as Principal from \"./Principal.ts\";\nimport * as Provision from \"./Provision.ts\";\nimport * as Receipt from \"./Receipt.ts\";\nimport * as Storage from \"./Storage.ts\";\n\ntype Fx<A> = Effect.Effect<A, Errors.DomainKitError, Principal.Service>;\n\nexport interface Interface {\n /** Read back every receipt record; records that no longer match exactly become `Conflict`. */\n readonly plan: (\n input: { readonly receiptId: Receipt.ReceiptId } | { readonly domain: string },\n ) => Fx<Plan.Model>;\n readonly approve: (\n plan: Plan.Model | Plan.PlanId,\n options?: Attempts.ApproveOptions,\n ) => Fx<Approval.Model>;\n /** Decline the cleanup plan; terminal, same rules as `Provision.reject`. */\n readonly reject: (\n plan: Plan.Model | Plan.PlanId,\n options?: Provision.RejectOptions,\n ) => Fx<Provision.Attempt>;\n readonly apply: (approval: Approval.Model | Approval.ApprovalId) => Fx<Receipt.Model>;\n}\n\nexport class Service extends Context.Service<Service, Interface>()(\"@domainkit/Cleanup\") {}\n\nexport const make: Effect.Effect<Interface, never, Storage.Service | Connect.Service> = Effect.gen(\n function* () {\n const storage = yield* Storage.Service;\n const connect = yield* Connect.Service;\n const attempts = Attempts.make(storage, connect, \"cleanup\");\n\n const sourceAttempt = (\n input: { readonly receiptId: Receipt.ReceiptId } | { readonly domain: string },\n ) =>\n Effect.gen(function* () {\n if (\"receiptId\" in input) {\n const attempt = yield* storage.attempts.byReceipt(input.receiptId);\n if (attempt.kind !== \"provisioning\" || attempt.receipt === null) {\n return yield* Errors.fail(\n new Reason.NotFound({ entity: \"receipt\", id: input.receiptId }),\n );\n }\n return attempt;\n }\n const domain = yield* DomainName.decode(input.domain);\n const attachment = yield* storage.attachments.byDomain(domain);\n const latest = Option.isSome(attachment)\n ? yield* storage.attempts.latest(attachment.value.id, \"provisioning\")\n : Option.none<Storage.Attempt>();\n if (Option.isNone(latest) || latest.value.receipt === null) {\n return yield* Errors.fail(new Reason.NotFound({ entity: \"receipt\", id: domain }));\n }\n return latest.value;\n });\n\n return {\n plan: (input) =>\n Effect.gen(function* () {\n const policy = yield* Provision.Policy;\n const source = yield* sourceAttempt(input);\n const receipt = source.receipt;\n if (receipt === null) {\n return yield* Errors.fail(new Reason.NotFound({ entity: \"receipt\", id: source.id }));\n }\n const attachment = yield* storage.attachments.get(source.attachmentId);\n yield* Attempts.assertWithin(\n attachment,\n source.plan.operations.map((operation) => operation.record),\n );\n const { session, target } = yield* connect.session(attachment.id);\n const dns = session.dns(target);\n const operations = yield* Effect.forEach(Receipt.applied(receipt), (applied) =>\n Effect.gen(function* () {\n const created = source.plan.operations.find(\n (operation) => operation._tag === \"Create\" && operation.id === applied.operationId,\n );\n if (created === undefined) {\n return yield* Errors.fail(\n new Reason.InvalidInput({\n message: `Receipt ${receipt.id} proves no created record for ${applied.operationId}`,\n field: \"receiptId\",\n }),\n );\n }\n return yield* Attempts.cleanupOperation(\n dns,\n target.zone,\n created.id,\n created.record,\n applied.providerRecordId,\n );\n }),\n );\n return yield* attempts.record({\n attachment,\n target,\n operations,\n ttlMs: policy.planTtlMs,\n sourceReceiptId: receipt.id,\n });\n }),\n approve: (plan, options) => attempts.approve(plan, options),\n reject: (plan, options) =>\n Effect.map(attempts.reject(plan, options), (attempt) => ({\n plan: attempt.plan,\n status: attempt.status,\n approval: attempt.approval,\n receipt: attempt.receipt,\n rejection: attempt.rejection,\n })),\n apply: (approval) =>\n Effect.flatMap(Provision.Policy, (policy) => attempts.apply(approval, policy)),\n };\n },\n);\n\nexport const layer: Layer.Layer<Service, never, Storage.Service | Connect.Service> =\n Layer.effect(Service)(make);\n\nconst accessor =\n <Args extends ReadonlyArray<unknown>, A>(\n pick: (service: Interface) => (...args: Args) => Fx<A>,\n ): ((...args: Args) => Effect.Effect<A, Errors.DomainKitError, Principal.Service | Service>) =>\n (...args) =>\n Effect.flatMap(Service, (service) => pick(service)(...args));\n\nexport const plan = accessor((service) => service.plan);\nexport const approve = accessor((service) => service.approve);\nexport const reject = accessor((service) => service.reject);\nexport const apply = accessor((service) => service.apply);\n"],"mappings":"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"}
@@ -0,0 +1,225 @@
1
+ import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
2
+ import { decode } from "./DomainName.mjs";
3
+ import { Unauthenticated, Unsupported } from "./Reason.mjs";
4
+ import { fail } from "./internal/error.mjs";
5
+ import { authorizationUrl, exchangeCode, refresh, revoke } from "./internal/oauth.mjs";
6
+ import { make, resolveAmong, tokenAuth } from "./Provider.mjs";
7
+ import { dns, listZones, provider as provider$1, tokenExpiry, verifyToken } from "./internal/cloudflare/client.mjs";
8
+ import { resolve } from "./internal/config.mjs";
9
+ import { Effect, Redacted, Schema } from "effect";
10
+ //#region src/Cloudflare.ts
11
+ /** Cloudflare: personal API tokens and OAuth. */
12
+ var Cloudflare_exports = /* @__PURE__ */ __exportAll({
13
+ AccountContext: () => AccountContext,
14
+ TokenFields: () => TokenFields,
15
+ provider: () => provider,
16
+ server: () => server
17
+ });
18
+ /**
19
+ * Authorization context: `accountId` is `null` when the credential spans several accounts or
20
+ * none yet. Target context: the zone's account and zone id.
21
+ */
22
+ const AccountContext = Schema.Struct({
23
+ accountId: Schema.NullOr(Schema.String),
24
+ /** How the token was verified: user tokens span accounts, account tokens are pinned. */
25
+ tokenKind: Schema.optionalKey(Schema.Literals(["user", "account"])),
26
+ zoneId: Schema.optionalKey(Schema.String)
27
+ });
28
+ /** Token-method input: the API token, and the account id for account-owned tokens. */
29
+ const TokenFields = Schema.Struct({
30
+ token: Schema.RedactedFromValue(Schema.String),
31
+ accountId: Schema.optionalKey(Schema.String)
32
+ });
33
+ const server = {
34
+ authorization_endpoint: "https://dash.cloudflare.com/oauth2/auth",
35
+ issuer: "https://dash.cloudflare.com",
36
+ revocation_endpoint: "https://dash.cloudflare.com/oauth2/revoke",
37
+ token_endpoint: "https://dash.cloudflare.com/oauth2/token"
38
+ };
39
+ const defaultScopes = [
40
+ "zone:read",
41
+ "dns_records:edit",
42
+ "offline_access"
43
+ ];
44
+ const capabilities = ["dns:read", "dns:write"];
45
+ /** OAuth credentials pack both tokens into `secret`; token credentials are the token itself. */
46
+ const Secret = Schema.fromJsonString(Schema.Struct({
47
+ accessToken: Schema.String,
48
+ refreshToken: Schema.NullOr(Schema.String)
49
+ }));
50
+ const parseSecret = (secret) => {
51
+ const raw = Redacted.value(secret);
52
+ const parsed = raw.startsWith("{") ? Schema.decodeUnknownOption(Secret)(raw) : void 0;
53
+ return parsed !== void 0 && parsed._tag === "Some" ? parsed.value : {
54
+ accessToken: raw,
55
+ refreshToken: null
56
+ };
57
+ };
58
+ const packSecret = (tokens) => Redacted.make(Schema.encodeSync(Secret)({
59
+ accessToken: tokens.accessToken,
60
+ refreshToken: tokens.refreshToken
61
+ }));
62
+ const provider = (options = {}) => {
63
+ const fetch = options.fetch ?? globalThis.fetch;
64
+ const baseUrl = (options.baseUrl ?? "https://api.cloudflare.com/client/v4").replace(/\/$/, "");
65
+ const client = (token) => ({
66
+ token,
67
+ fetch,
68
+ baseUrl
69
+ });
70
+ const discoverAccount = (token) => listZones(client(token)).pipe(Effect.map((zones) => {
71
+ const accounts = new Set(zones.map((zone) => zone.account.id));
72
+ const [only] = accounts;
73
+ return accounts.size === 1 && only !== void 0 ? only : null;
74
+ }));
75
+ const targetOf = (zone) => ({
76
+ zone: zone.name,
77
+ context: {
78
+ accountId: zone.account.id,
79
+ zoneId: zone.id
80
+ },
81
+ label: `${zone.name} (${zone.account.name})`,
82
+ nameservers: zone.name_servers ?? []
83
+ });
84
+ const oauthClient = (oauth) => Effect.gen(function* () {
85
+ return {
86
+ clientId: yield* resolve(oauth.clientId, "oauth.clientId"),
87
+ clientSecret: yield* resolve(oauth.clientSecret, "oauth.clientSecret"),
88
+ clientAuth: oauth.clientAuth ?? "client_secret_basic"
89
+ };
90
+ });
91
+ const issued = (tokens) => discoverAccount(tokens.accessToken).pipe(Effect.map((accountId) => ({
92
+ secret: packSecret(tokens),
93
+ context: { accountId },
94
+ expiresAt: tokens.expiresAt
95
+ })));
96
+ const oauth = options.oauth === void 0 ? void 0 : oauthAuth(options.oauth);
97
+ function oauthAuth(settings) {
98
+ const scopes = settings.scopes ?? defaultScopes;
99
+ return {
100
+ label: "Sign in with Cloudflare",
101
+ scopes,
102
+ start: (input) => oauthClient(settings).pipe(Effect.map(({ clientId }) => ({ authorizationUrl: authorizationUrl({
103
+ server,
104
+ clientId,
105
+ scopes,
106
+ state: input.state,
107
+ callbackUrl: input.callbackUrl,
108
+ codeChallenge: input.codeChallenge
109
+ }) }))),
110
+ complete: (input) => oauthClient(settings).pipe(Effect.flatMap((oauthClientValue) => exchangeCode({
111
+ provider: provider$1,
112
+ server,
113
+ client: oauthClientValue,
114
+ code: input.code,
115
+ state: input.params.state ?? "",
116
+ callbackUrl: input.callbackUrl,
117
+ codeVerifier: input.codeVerifier,
118
+ fetch
119
+ })), Effect.flatMap(issued)),
120
+ refresh: (credential) => Effect.gen(function* () {
121
+ const { refreshToken } = parseSecret(credential.secret);
122
+ if (refreshToken === null) return yield* fail(new Unauthenticated({ message: "Cloudflare credential has no refresh token" }));
123
+ const tokens = yield* refresh({
124
+ provider: provider$1,
125
+ server,
126
+ client: yield* oauthClient(settings),
127
+ refreshToken,
128
+ fetch
129
+ });
130
+ return {
131
+ secret: packSecret(tokens),
132
+ context: credential.context,
133
+ expiresAt: tokens.expiresAt
134
+ };
135
+ }),
136
+ revoke: (credential) => Effect.gen(function* () {
137
+ const { accessToken } = parseSecret(credential.secret);
138
+ yield* revoke({
139
+ provider: provider$1,
140
+ server,
141
+ client: yield* oauthClient(settings),
142
+ token: accessToken,
143
+ fetch
144
+ });
145
+ })
146
+ };
147
+ }
148
+ return make({
149
+ id: "cloudflare",
150
+ name: "Cloudflare",
151
+ context: AccountContext,
152
+ contextVersion: "cloudflare.v1",
153
+ auth: {
154
+ token: tokenAuth({
155
+ label: "API token",
156
+ docsUrl: "https://developers.cloudflare.com/fundamentals/api/get-started/create-token/",
157
+ requiredCapabilities: capabilities,
158
+ fields: TokenFields,
159
+ authenticate: ({ token, accountId }) => Effect.gen(function* () {
160
+ const raw = Redacted.value(token);
161
+ if (accountId !== void 0) {
162
+ const expiresAt = yield* verifyToken(client(raw), `/accounts/${encodeURIComponent(accountId)}/tokens/verify`);
163
+ return {
164
+ secret: token,
165
+ context: {
166
+ accountId,
167
+ tokenKind: "account"
168
+ },
169
+ expiresAt
170
+ };
171
+ }
172
+ const discovered = yield* discoverAccount(raw);
173
+ const verified = yield* tokenExpiry(client(raw), discovered);
174
+ return {
175
+ secret: token,
176
+ context: {
177
+ accountId: discovered,
178
+ tokenKind: verified.kind
179
+ },
180
+ expiresAt: verified.expiresAt
181
+ };
182
+ })
183
+ }),
184
+ ...oauth === void 0 ? {} : { oauth }
185
+ },
186
+ session: (credential) => {
187
+ const { accessToken } = parseSecret(credential.secret);
188
+ const clientOptions = client(accessToken);
189
+ const context = Schema.decodeUnknownOption(AccountContext)(credential.context);
190
+ const accountId = context._tag === "Some" ? context.value.accountId ?? void 0 : void 0;
191
+ const listTargets = () => listZones(clientOptions, accountId).pipe(Effect.map((zones) => zones.map(targetOf)));
192
+ return {
193
+ capabilities: () => Effect.succeed(capabilities),
194
+ listTargets,
195
+ resolveTarget: (domain) => Effect.gen(function* () {
196
+ const name = yield* decode(domain);
197
+ const targets = yield* listTargets();
198
+ return resolveAmong(name, targets);
199
+ }),
200
+ dns: (target) => {
201
+ const zone = Schema.decodeUnknownOption(AccountContext)(target.context);
202
+ const zoneId = zone._tag === "Some" ? zone.value.zoneId : void 0;
203
+ if (zoneId === void 0) {
204
+ const failure = fail(new Unsupported({
205
+ provider: "cloudflare",
206
+ operation: "dns",
207
+ message: "Cloudflare target has no zone id; only zone targets host records"
208
+ }));
209
+ return {
210
+ list: () => failure,
211
+ create: () => failure,
212
+ get: () => failure,
213
+ delete: () => failure
214
+ };
215
+ }
216
+ return dns(clientOptions, zoneId);
217
+ }
218
+ };
219
+ }
220
+ });
221
+ };
222
+ //#endregion
223
+ export { AccountContext, Cloudflare_exports, TokenFields, provider, server };
224
+
225
+ //# sourceMappingURL=Cloudflare.mjs.map