domainkit 0.0.1 → 0.1.0-beta.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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +115 -1
  3. package/dist/auth-Df9BuC25.mjs +460 -0
  4. package/dist/auth-Df9BuC25.mjs.map +1 -0
  5. package/dist/client-DvXfJx_E.mjs +534 -0
  6. package/dist/client-DvXfJx_E.mjs.map +1 -0
  7. package/dist/cloudflare-C_ixZsJo.mjs +46 -0
  8. package/dist/cloudflare-C_ixZsJo.mjs.map +1 -0
  9. package/dist/cloudflare-DClWuAm8.d.mts +23 -0
  10. package/dist/cloudflare.d.mts +3 -0
  11. package/dist/cloudflare.mjs +2 -0
  12. package/dist/dns-record-Dzo4flM5.mjs +259 -0
  13. package/dist/dns-record-Dzo4flM5.mjs.map +1 -0
  14. package/dist/effect-cloudflare.d.mts +2 -0
  15. package/dist/effect-cloudflare.mjs +2 -0
  16. package/dist/effect-vercel.d.mts +2 -0
  17. package/dist/effect-vercel.mjs +2 -0
  18. package/dist/effect.d.mts +63 -0
  19. package/dist/effect.mjs +28 -0
  20. package/dist/effect.mjs.map +1 -0
  21. package/dist/index-D1KtyjuI.d.mts +71 -0
  22. package/dist/index-DQAjUrZu.d.mts +94 -0
  23. package/dist/index.d.mts +128 -0
  24. package/dist/index.mjs +198 -0
  25. package/dist/index.mjs.map +1 -0
  26. package/dist/manifest-DjH3BVB9.d.mts +579 -0
  27. package/dist/receipt-C7PjnibZ.mjs +28 -0
  28. package/dist/receipt-C7PjnibZ.mjs.map +1 -0
  29. package/dist/resolver-BjZwN2JE.d.mts +2327 -0
  30. package/dist/resolver-D5nVrR10.mjs +153 -0
  31. package/dist/resolver-D5nVrR10.mjs.map +1 -0
  32. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  33. package/dist/testing.d.mts +47 -0
  34. package/dist/testing.mjs +117 -0
  35. package/dist/testing.mjs.map +1 -0
  36. package/dist/vercel-BWz_9Y1v.mjs +38 -0
  37. package/dist/vercel-BWz_9Y1v.mjs.map +1 -0
  38. package/dist/vercel-Vc_K_87s.d.mts +22 -0
  39. package/dist/vercel.d.mts +3 -0
  40. package/dist/vercel.mjs +2 -0
  41. package/dist/verify-CeMW6XtK.mjs +1109 -0
  42. package/dist/verify-CeMW6XtK.mjs.map +1 -0
  43. package/dist/verify-DPB1OfZ5.d.mts +979 -0
  44. package/package.json +94 -6
  45. package/bun.lock +0 -66
  46. package/index.ts +0 -1
  47. package/tsconfig.json +0 -30
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arya Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1 +1,115 @@
1
- # domainkit
1
+ # DomainKit
2
+
3
+ DomainKit is a provider-independent TypeScript SDK for turning DNS requirements into reviewable,
4
+ authorized provisioning plans. It is intended for products that need to connect customer domains
5
+ without making registrar-specific APIs their product architecture.
6
+
7
+ ## Direction
8
+
9
+ DomainKit separates four concerns:
10
+
11
+ 1. a portable, versioned DNS plan and receipt format;
12
+ 2. explicit authorization for the exact operations being applied;
13
+ 3. provider adapters that reconcile and apply records;
14
+ 4. host-owned credentials, persistence, routes, and user interaction.
15
+
16
+ ## Package shape
17
+
18
+ - `domainkit` — Promise-based API for application code;
19
+ - `domainkit/effect` — canonical Effect-native services and programs;
20
+ - `domainkit/cloudflare` — Promise-based Cloudflare authoritative-DNS adapter;
21
+ - `domainkit/effect/cloudflare` — canonical Effect-native Cloudflare adapter;
22
+ - `domainkit/vercel` — Promise-based Vercel authoritative-DNS adapter;
23
+ - `domainkit/effect/vercel` — canonical Effect-native Vercel adapter;
24
+ - `domainkit/testing` — deterministic provider and store implementations for tests.
25
+
26
+ The package is portable ESM built on Fetch and Web APIs. Effect is a peer dependency.
27
+
28
+ Both entry points are organized by capability namespaces. The root API accepts ordinary async
29
+ providers and stores:
30
+
31
+ ```ts
32
+ import { Provisioning } from "domainkit";
33
+
34
+ const plan = await Provisioning.create({
35
+ provider: {
36
+ id: "my-provider",
37
+ listRecords: async (zone) => listDnsRecords(zone),
38
+ createRecord: async (zone, record) => createDnsRecord(zone, record),
39
+ },
40
+ requirements,
41
+ zone: "example.com",
42
+ });
43
+ ```
44
+
45
+ Effect applications provide the same capabilities as Layers and run the canonical program
46
+ directly:
47
+
48
+ ```ts
49
+ import { Effect, Layer } from "effect";
50
+ import { Digest, DnsProvider, Provisioning } from "domainkit/effect";
51
+
52
+ const program = Provisioning.create({ requirements, zone: "example.com" }).pipe(
53
+ Effect.provide(Layer.merge(DnsProvider.layerFromAsync(provider), Digest.webCryptoLayer)),
54
+ );
55
+ ```
56
+
57
+ The Promise namespaces delegate to the canonical Effect programs rather than maintaining a second
58
+ planning, authorization, or verification implementation. Hosts supply providers and stores
59
+ explicitly; DomainKit does not own a hidden runtime.
60
+
61
+ The Cloudflare adapter accepts a host-owned API or OAuth token and an explicitly selected account.
62
+ The host also declares the capabilities required when that credential was issued; Cloudflare's
63
+ non-mutating token-verification endpoint cannot infer DNS write permission. The adapter creates
64
+ DNS-only records and leaves credential storage and user interaction to the host:
65
+
66
+ ```ts
67
+ import { Cloudflare, Provisioning, Secret } from "domainkit";
68
+
69
+ const provider = Cloudflare.make({
70
+ accountId: "cloudflare-account-id",
71
+ capabilities: ["dns:read", "dns:write"],
72
+ token: Secret.make(apiToken),
73
+ });
74
+
75
+ const plan = await Provisioning.create({ provider, requirements, zone: "example.com" });
76
+ ```
77
+
78
+ OAuth helpers own Cloudflare's endpoints and client-auth variants while accepting the scope IDs
79
+ assigned during OAuth client registration. `Cloudflare.Auth.tokenMethod` describes the equivalent
80
+ API-token capability.
81
+
82
+ The Vercel adapter accepts either explicit personal or team context. It supports personal access
83
+ tokens and Vercel's integration-code exchange without treating that provider-specific installation
84
+ flow as generic OAuth:
85
+
86
+ ```ts
87
+ import { Secret, Vercel } from "domainkit";
88
+
89
+ const provider = Vercel.make({
90
+ capabilities: ["dns:read", "dns:write"],
91
+ context: { _tag: "team", teamId: "team-id" },
92
+ token: Secret.make(apiToken),
93
+ });
94
+ ```
95
+
96
+ `listAccounts()` returns the distinct accounts represented by zones visible to the credential.
97
+ Accounts with no visible zones are not returned because Cloudflare does not document bearer-token
98
+ authentication for its separate account-list endpoint.
99
+
100
+ Schemas own external parsing. For example, `DomainName.parse` and `DnsRecord.parse` decode and
101
+ canonicalize strings through codecs, while encoded protocol dates remain ISO strings and decoded
102
+ domain values use `Date`.
103
+
104
+ See [the architecture decisions](docs/adr/README.md) for the durable rationale and
105
+ [the provider contract](docs/providers.md) for adapter differences. Runnable TypeScript examples
106
+ cover [manual Promise provisioning](examples/promise/manual-provider.ts),
107
+ [token connections](examples/promise/cloudflare-token.ts),
108
+ [Cloudflare OAuth](examples/promise/cloudflare-oauth.ts),
109
+ [Vercel integration exchange](examples/promise/vercel-integration.ts), and
110
+ [Effect-native provisioning](examples/effect/provisioning.ts). See
111
+ [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow.
112
+
113
+ ## License
114
+
115
+ MIT
@@ -0,0 +1,460 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { T as decode$2, c as Opaque, h as decode$3, y as Error } from "./dns-record-Dzo4flM5.mjs";
3
+ import { Clock, Effect, Schema } from "effect";
4
+ //#region src/providers/cloudflare/protocol.ts
5
+ const ErrorDetail = Schema.Struct({
6
+ code: Schema.Number,
7
+ message: Schema.String
8
+ });
9
+ const ResultInfo = Schema.Struct({
10
+ count: Schema.optionalKey(Schema.Number),
11
+ page: Schema.optionalKey(Schema.Number),
12
+ per_page: Schema.optionalKey(Schema.Number),
13
+ total_count: Schema.optionalKey(Schema.Number),
14
+ total_pages: Schema.optionalKey(Schema.Number)
15
+ });
16
+ const Message = Schema.Struct({
17
+ code: Schema.optionalKey(Schema.Number),
18
+ message: Schema.String
19
+ });
20
+ const BaseEnvelope = Schema.Struct({
21
+ errors: Schema.Array(ErrorDetail),
22
+ messages: Schema.Array(Message),
23
+ success: Schema.Boolean
24
+ });
25
+ const Account = Schema.Struct({
26
+ id: Schema.String,
27
+ name: Schema.String
28
+ });
29
+ const Zone = Schema.Struct({
30
+ account: Account,
31
+ id: Schema.String,
32
+ name: Schema.String,
33
+ name_servers: Schema.Array(Schema.String),
34
+ status: Schema.optionalKey(Schema.String)
35
+ });
36
+ const Record = Schema.Struct({
37
+ content: Schema.optionalKey(Schema.String),
38
+ data: Schema.optionalKey(Schema.Unknown),
39
+ id: Schema.String,
40
+ name: Schema.String,
41
+ priority: Schema.optionalKey(Schema.Number),
42
+ proxied: Schema.optionalKey(Schema.Boolean),
43
+ ttl: Schema.Number,
44
+ type: Schema.String
45
+ });
46
+ const Token = Schema.Struct({
47
+ expires_on: Schema.optionalKey(Schema.String),
48
+ id: Schema.String,
49
+ not_before: Schema.optionalKey(Schema.String),
50
+ status: Schema.Literals([
51
+ "active",
52
+ "disabled",
53
+ "expired"
54
+ ])
55
+ });
56
+ const ZoneListEnvelope = Schema.Struct({
57
+ errors: Schema.Array(ErrorDetail),
58
+ messages: Schema.Array(Message),
59
+ result: Schema.Array(Zone),
60
+ result_info: Schema.optionalKey(ResultInfo),
61
+ success: Schema.Boolean
62
+ });
63
+ const RecordListEnvelope = Schema.Struct({
64
+ errors: Schema.Array(ErrorDetail),
65
+ messages: Schema.Array(Message),
66
+ result: Schema.Array(Record),
67
+ result_info: Schema.optionalKey(ResultInfo),
68
+ success: Schema.Boolean
69
+ });
70
+ const RecordEnvelope = Schema.Struct({
71
+ errors: Schema.Array(ErrorDetail),
72
+ messages: Schema.Array(Message),
73
+ result: Record,
74
+ success: Schema.Boolean
75
+ });
76
+ const TokenEnvelope = Schema.Struct({
77
+ errors: Schema.Array(ErrorDetail),
78
+ messages: Schema.Array(Message),
79
+ result: Token,
80
+ success: Schema.Boolean
81
+ });
82
+ //#endregion
83
+ //#region src/providers/cloudflare/records.ts
84
+ const Data = Schema.Struct({
85
+ flags: Schema.optionalKey(Schema.Number),
86
+ port: Schema.optionalKey(Schema.Number),
87
+ priority: Schema.optionalKey(Schema.Number),
88
+ tag: Schema.optionalKey(Schema.String),
89
+ target: Schema.optionalKey(Schema.String),
90
+ value: Schema.optionalKey(Schema.String),
91
+ weight: Schema.optionalKey(Schema.Number)
92
+ });
93
+ const decode$1 = Effect.fn("CloudflareRecords.decode")((record) => Effect.gen(function* () {
94
+ const name = yield* decode$2(record.name).pipe(Effect.match({
95
+ onFailure: () => null,
96
+ onSuccess: (value) => value
97
+ }));
98
+ if (name === null) return yield* decodeOpaque(record);
99
+ const common = {
100
+ metadata: {
101
+ ownership: "provider",
102
+ provenance: record.proxied === true ? "cloudflare:proxied" : "cloudflare",
103
+ purpose: "existing DNS record"
104
+ },
105
+ name,
106
+ policy: record.type === "CNAME" ? "exclusive" : "append",
107
+ ttl: record.ttl === 1 ? null : record.ttl
108
+ };
109
+ const content = () => record.content === void 0 ? Effect.fail(failure$1("decodeRecord", `Cloudflare ${record.type} record has no content`)) : Effect.succeed(record.content);
110
+ switch (record.type) {
111
+ case "A":
112
+ case "AAAA": return yield* decode$3({
113
+ ...common,
114
+ _tag: record.type,
115
+ address: yield* content()
116
+ }).pipe(Effect.mapError((cause) => failure$1("decodeRecord", cause.message)));
117
+ case "CNAME":
118
+ case "NS": return yield* decode$3({
119
+ ...common,
120
+ _tag: record.type,
121
+ target: yield* content()
122
+ }).pipe(Effect.mapError((cause) => failure$1("decodeRecord", cause.message)));
123
+ case "TXT": return yield* decode$3({
124
+ ...common,
125
+ _tag: "TXT",
126
+ value: yield* content()
127
+ }).pipe(Effect.mapError((cause) => failure$1("decodeRecord", cause.message)));
128
+ case "MX": return yield* decode$3({
129
+ ...common,
130
+ _tag: "MX",
131
+ exchange: yield* content(),
132
+ priority: record.priority
133
+ }).pipe(Effect.mapError((cause) => failure$1("decodeRecord", cause.message)));
134
+ case "CAA": {
135
+ const data = yield* decodeData(record);
136
+ return yield* decode$3({
137
+ ...common,
138
+ _tag: "CAA",
139
+ ...data
140
+ }).pipe(Effect.mapError((cause) => failure$1("decodeRecord", cause.message)));
141
+ }
142
+ case "SRV": {
143
+ const data = yield* decodeData(record);
144
+ return yield* decode$3({
145
+ ...common,
146
+ _tag: "SRV",
147
+ ...data
148
+ }).pipe(Effect.mapError((cause) => failure$1("decodeRecord", cause.message)));
149
+ }
150
+ default: return yield* decodeOpaque(record);
151
+ }
152
+ }));
153
+ const decodeOpaque = Effect.fn("CloudflareRecords.decodeOpaque")((record) => Schema.decodeUnknownEffect(Opaque)({
154
+ _tag: "Opaque",
155
+ name: record.name,
156
+ providerRecordId: record.id,
157
+ providerType: record.type
158
+ }).pipe(Effect.mapError((cause) => failure$1("decodeRecord", cause.message))));
159
+ function encode(record) {
160
+ const common = {
161
+ name: record.name,
162
+ proxied: false,
163
+ ttl: record.ttl ?? 1,
164
+ type: record._tag
165
+ };
166
+ switch (record._tag) {
167
+ case "A":
168
+ case "AAAA": return {
169
+ ...common,
170
+ content: record.address
171
+ };
172
+ case "CNAME":
173
+ case "NS": return {
174
+ ...common,
175
+ content: record.target
176
+ };
177
+ case "TXT": return {
178
+ ...common,
179
+ content: record.value
180
+ };
181
+ case "MX": return {
182
+ ...common,
183
+ content: record.exchange,
184
+ priority: record.priority
185
+ };
186
+ case "CAA": return {
187
+ ...common,
188
+ data: {
189
+ flags: record.flags,
190
+ tag: record.tag,
191
+ value: record.value
192
+ }
193
+ };
194
+ case "SRV": return {
195
+ ...common,
196
+ data: {
197
+ port: record.port,
198
+ priority: record.priority,
199
+ target: record.target,
200
+ weight: record.weight
201
+ }
202
+ };
203
+ }
204
+ }
205
+ const decodeData = Effect.fn("CloudflareRecords.decodeData")((record) => Schema.decodeUnknownEffect(Data)(record.data).pipe(Effect.mapError((cause) => failure$1("decodeRecord", cause.message))));
206
+ function failure$1(operation, message, fields = {}) {
207
+ return new Error({
208
+ message,
209
+ operation,
210
+ providerId: "cloudflare",
211
+ reason: fields.reason ?? "response"
212
+ });
213
+ }
214
+ //#endregion
215
+ //#region src/providers/cloudflare/client.ts
216
+ var client_exports = /* @__PURE__ */ __exportAll({ make: () => make });
217
+ /** Creates an Effect-native Cloudflare client without owning the credential lifecycle. */
218
+ function make(options) {
219
+ const fetch = options.fetch ?? globalThis.fetch;
220
+ const baseUrl = (options.baseUrl ?? "https://api.cloudflare.com/client/v4").replace(/\/$/, "");
221
+ const request = Effect.fn("CloudflareClient.request")((path, operation, init) => Effect.gen(function* () {
222
+ const response = yield* Effect.tryPromise({
223
+ try: () => fetch(`${baseUrl}${path}`, {
224
+ ...init,
225
+ headers: {
226
+ Accept: "application/json",
227
+ Authorization: `Bearer ${options.token.expose()}`,
228
+ ...init?.body === void 0 ? {} : { "Content-Type": "application/json" },
229
+ ...init?.headers
230
+ }
231
+ }),
232
+ catch: () => failure(operation, "Cloudflare request failed", { reason: "transport" })
233
+ });
234
+ return {
235
+ body: yield* Effect.tryPromise({
236
+ try: () => response.json(),
237
+ catch: () => {
238
+ const retryAfterMs = retryAfter(response.headers.get("retry-after"));
239
+ return failure(operation, response.ok ? "Cloudflare returned a non-JSON response" : `Cloudflare request failed with HTTP ${response.status}`, {
240
+ reason: response.ok ? "response" : reason(response.status),
241
+ ...retryAfterMs === void 0 ? {} : { retryAfterMs },
242
+ status: response.status
243
+ });
244
+ }
245
+ }),
246
+ response
247
+ };
248
+ }));
249
+ const listZonesPage = Effect.fn("CloudflareClient.listZonesPage")((accountId, name, page) => Effect.gen(function* () {
250
+ const query = new URLSearchParams({
251
+ page: String(page),
252
+ per_page: "50"
253
+ });
254
+ query.set("type", "full,partial,secondary,internal");
255
+ if (accountId !== void 0) query.set("account.id", accountId);
256
+ if (name !== void 0) query.set("name", name);
257
+ const { body, response } = yield* request(`/zones?${query}`, "listZones");
258
+ yield* ensureSuccess(body, "listZones", response);
259
+ return yield* decode(ZoneListEnvelope, body, "listZones", response);
260
+ }));
261
+ const allZones = Effect.fn("CloudflareClient.allZones")((accountId, name) => Effect.gen(function* () {
262
+ const zones = [];
263
+ let page = 1;
264
+ while (true) {
265
+ const envelope = yield* listZonesPage(accountId, name, page);
266
+ zones.push(...envelope.result);
267
+ const totalPages = envelope.result_info?.total_pages;
268
+ if (totalPages !== void 0 && page >= totalPages || totalPages === void 0 && envelope.result.length < 50) return zones;
269
+ page += 1;
270
+ }
271
+ }));
272
+ const resolveZone = Effect.fn("CloudflareClient.resolveZone")((name) => Effect.gen(function* () {
273
+ const matches = yield* allZones(options.accountId, name);
274
+ const match = matches[0];
275
+ if (matches.length === 1 && match !== void 0) return match;
276
+ return yield* Effect.fail(failure("resolveZone", matches.length === 0 ? `Cloudflare zone ${name} was not found in account ${options.accountId}` : `Cloudflare returned multiple zones named ${name} in account ${options.accountId}`, { reason: matches.length === 0 ? "not_found" : "response" }));
277
+ }));
278
+ const listRecords = Effect.fn("CloudflareClient.listRecords")((zoneName) => Effect.gen(function* () {
279
+ const zone = yield* resolveZone(zoneName);
280
+ const records = [];
281
+ let page = 1;
282
+ while (true) {
283
+ const query = new URLSearchParams({
284
+ page: String(page),
285
+ per_page: "100"
286
+ });
287
+ const { body, response } = yield* request(`/zones/${encodeURIComponent(zone.id)}/dns_records?${query}`, "listRecords");
288
+ yield* ensureSuccess(body, "listRecords", response);
289
+ const envelope = yield* decode(RecordListEnvelope, body, "listRecords", response);
290
+ records.push(...yield* Effect.forEach(envelope.result, decode$1));
291
+ const totalPages = envelope.result_info?.total_pages;
292
+ if (totalPages !== void 0 && page >= totalPages || totalPages === void 0 && envelope.result.length < 100) return records;
293
+ page += 1;
294
+ }
295
+ }));
296
+ return {
297
+ id: "cloudflare",
298
+ createRecord: Effect.fn("CloudflareClient.createRecord")((zoneName, record) => Effect.gen(function* () {
299
+ const zone = yield* resolveZone(zoneName);
300
+ const { body, response } = yield* request(`/zones/${encodeURIComponent(zone.id)}/dns_records`, "createRecord", {
301
+ body: JSON.stringify(encode(record)),
302
+ method: "POST"
303
+ });
304
+ yield* ensureSuccess(body, "createRecord", response);
305
+ return { providerRecordId: (yield* decode(RecordEnvelope, body, "createRecord", response)).result.id };
306
+ })),
307
+ listAccounts: Effect.fn("CloudflareClient.listAccounts")(() => allZones().pipe(Effect.map((zones) => {
308
+ const accounts = /* @__PURE__ */ new Map();
309
+ for (const zone of zones) accounts.set(zone.account.id, zone.account);
310
+ return [...accounts.values()].sort((left, right) => left.name.localeCompare(right.name));
311
+ }))),
312
+ listRecords,
313
+ listZones: Effect.fn("CloudflareClient.listZones")((input = {}) => allZones(input.accountId ?? options.accountId, input.name).pipe(Effect.flatMap((zones) => Effect.forEach(zones, projectZone)))),
314
+ validateToken: Effect.fn("CloudflareClient.validateToken")(() => Effect.gen(function* () {
315
+ const path = options.tokenKind === "account" ? `/accounts/${encodeURIComponent(options.accountId)}/tokens/verify` : "/user/tokens/verify";
316
+ const { body, response } = yield* request(path, "validateToken");
317
+ yield* ensureSuccess(body, "validateToken", response);
318
+ const envelope = yield* decode(TokenEnvelope, body, "validateToken", response);
319
+ if (envelope.result.status !== "active") return yield* Effect.fail(failure("validateToken", `Cloudflare token is ${envelope.result.status}`, {
320
+ reason: "authentication",
321
+ status: response.status
322
+ }));
323
+ yield* allZones(options.accountId);
324
+ const expiresAt = envelope.result.expires_on === void 0 ? null : new Date(envelope.result.expires_on);
325
+ if (expiresAt !== null && Number.isNaN(expiresAt.valueOf())) return yield* Effect.fail(failure("validateToken", "Cloudflare token expiry is not a valid timestamp", {
326
+ reason: "response",
327
+ status: response.status
328
+ }));
329
+ return {
330
+ accountId: options.accountId,
331
+ capabilities: options.capabilities,
332
+ expiresAt,
333
+ scopes: []
334
+ };
335
+ }))
336
+ };
337
+ }
338
+ const projectZone = Effect.fn("CloudflareClient.projectZone")((zone) => Effect.gen(function* () {
339
+ const name = yield* decode$2(zone.name).pipe(Effect.mapError((cause) => failure("listZones", cause.message, { reason: "response" })));
340
+ const nameservers = yield* Effect.forEach(zone.name_servers, (nameserver) => decode$2(nameserver).pipe(Effect.mapError((cause) => failure("listZones", cause.message, { reason: "response" }))));
341
+ return {
342
+ accountId: zone.account.id,
343
+ id: zone.id,
344
+ name,
345
+ nameservers,
346
+ status: zone.status ?? null
347
+ };
348
+ }));
349
+ function decode(schema, input, operation, response) {
350
+ return Schema.decodeUnknownEffect(schema)(input).pipe(Effect.mapError((cause) => failure(operation, `Cloudflare response did not match its API contract: ${cause.message}`, {
351
+ reason: "response",
352
+ status: response.status
353
+ })));
354
+ }
355
+ function ensureSuccess(input, operation, response) {
356
+ return Schema.decodeUnknownEffect(BaseEnvelope)(input).pipe(Effect.mapError(() => failure(operation, "Cloudflare response did not contain a valid API envelope", {
357
+ reason: "response",
358
+ status: response.status
359
+ })), Effect.flatMap((envelope) => {
360
+ if (response.ok && envelope.success) return Effect.void;
361
+ const detail = envelope.errors[0];
362
+ const retryAfterMs = retryAfter(response.headers.get("retry-after"));
363
+ return Effect.fail(failure(operation, detail?.message ?? `Cloudflare request failed with HTTP ${response.status}`, {
364
+ ...detail === void 0 ? {} : { code: detail.code },
365
+ reason: reason(response.status, detail?.code),
366
+ ...retryAfterMs === void 0 ? {} : { retryAfterMs },
367
+ status: response.status
368
+ }));
369
+ }));
370
+ }
371
+ function reason(status, code) {
372
+ if (status === 401) return "authentication";
373
+ if (status === 403) return "authorization";
374
+ if (status === 404) return "not_found";
375
+ if (status === 409 || code === 81056 || code === 81057 || code === 81058) return "conflict";
376
+ if (status === 429) return "rate_limit";
377
+ if (status >= 500) return "response";
378
+ return "request";
379
+ }
380
+ function retryAfter(value) {
381
+ if (value === null) return void 0;
382
+ const seconds = Number(value);
383
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
384
+ const date = Date.parse(value);
385
+ return Number.isNaN(date) ? void 0 : Math.max(0, date - Date.now());
386
+ }
387
+ function failure(operation, message, fields = {}) {
388
+ return new Error({
389
+ ...fields,
390
+ message,
391
+ operation,
392
+ providerId: "cloudflare"
393
+ });
394
+ }
395
+ //#endregion
396
+ //#region src/providers/cloudflare/auth.ts
397
+ var auth_exports = /* @__PURE__ */ __exportAll({
398
+ manifest: () => manifest,
399
+ oauthMethod: () => oauthMethod,
400
+ subjectResolver: () => subjectResolver,
401
+ tokenMethod: () => tokenMethod,
402
+ tokenValidator: () => tokenValidator
403
+ });
404
+ const authorizationServer = {
405
+ authorization_endpoint: "https://dash.cloudflare.com/oauth2/auth",
406
+ issuer: "https://dash.cloudflare.com",
407
+ revocation_endpoint: "https://dash.cloudflare.com/oauth2/revoke",
408
+ token_endpoint: "https://dash.cloudflare.com/oauth2/token"
409
+ };
410
+ /** Describes Cloudflare's caller-created API-token authorization method. */
411
+ function tokenMethod(capabilities) {
412
+ return {
413
+ _tag: "token",
414
+ capabilities: [...capabilities],
415
+ instructionsUrl: "https://developers.cloudflare.com/fundamentals/api/get-started/create-token/"
416
+ };
417
+ }
418
+ /** Describes a registered Cloudflare OAuth client and its assigned scope IDs. */
419
+ function oauthMethod(options) {
420
+ return {
421
+ _tag: "oauth2",
422
+ authorizationServer,
423
+ capabilities: [...options.capabilities],
424
+ clientAuth: options.clientAuth,
425
+ scopes: [...options.scopes]
426
+ };
427
+ }
428
+ /** Creates the Cloudflare token and OAuth authorization manifest. */
429
+ function manifest(options) {
430
+ return {
431
+ methods: [tokenMethod(options.capabilities), oauthMethod(options)],
432
+ providerId: "cloudflare"
433
+ };
434
+ }
435
+ /** Resolves the Cloudflare account selected by a completed OAuth grant. */
436
+ function subjectResolver(options) {
437
+ return (tokens, accessToken) => Effect.gen(function* () {
438
+ yield* make({
439
+ ...options,
440
+ token: accessToken,
441
+ tokenKind: "user"
442
+ }).listZones();
443
+ const now = yield* Clock.currentTimeMillis;
444
+ return {
445
+ accountId: options.accountId,
446
+ expiresAt: typeof tokens.expires_in === "number" ? new Date(now + tokens.expires_in * 1e3) : null
447
+ };
448
+ });
449
+ }
450
+ /** Creates a host callback that validates a Cloudflare API token. */
451
+ function tokenValidator(options) {
452
+ return (token) => make({
453
+ ...options,
454
+ token
455
+ }).validateToken();
456
+ }
457
+ //#endregion
458
+ export { tokenMethod as a, subjectResolver as i, manifest as n, client_exports as o, oauthMethod as r, make as s, auth_exports as t };
459
+
460
+ //# sourceMappingURL=auth-Df9BuC25.mjs.map