domainkit 0.1.0 → 0.2.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 (68) hide show
  1. package/README.md +91 -71
  2. package/dist/{auth-Vi-hhXbV.mjs → auth-CZ1pZHMZ.mjs} +301 -38
  3. package/dist/auth-CZ1pZHMZ.mjs.map +1 -0
  4. package/dist/{client-BnD7QLRF.mjs → auth-CzhZ3Cuc.mjs} +235 -131
  5. package/dist/auth-CzhZ3Cuc.mjs.map +1 -0
  6. package/dist/authorization-DOg2Re1D.mjs +247 -0
  7. package/dist/authorization-DOg2Re1D.mjs.map +1 -0
  8. package/dist/canonical-json-CpvqGMcr.mjs +57 -0
  9. package/dist/canonical-json-CpvqGMcr.mjs.map +1 -0
  10. package/dist/{cloudflare-C6ryPlCZ.mjs → cloudflare-BedFw4cX.mjs} +28 -7
  11. package/dist/cloudflare-BedFw4cX.mjs.map +1 -0
  12. package/dist/cloudflare-FDl5q7Dm.d.mts +32 -0
  13. package/dist/cloudflare.d.mts +3 -3
  14. package/dist/cloudflare.mjs +2 -2
  15. package/dist/connection-BWrBwqjk.mjs +58 -0
  16. package/dist/connection-BWrBwqjk.mjs.map +1 -0
  17. package/dist/connection-CEqadXSm.d.mts +182 -0
  18. package/dist/connection-CcQokgrd.mjs +186 -0
  19. package/dist/connection-CcQokgrd.mjs.map +1 -0
  20. package/dist/connection-api-DftK7eHo.d.mts +7 -0
  21. package/dist/{dns-record-C_KwGPLf.mjs → dns-record-BLeqo56C.mjs} +23 -3
  22. package/dist/dns-record-BLeqo56C.mjs.map +1 -0
  23. package/dist/effect-cloudflare.d.mts +2 -2
  24. package/dist/effect-cloudflare.mjs +2 -2
  25. package/dist/effect-vercel.d.mts +2 -2
  26. package/dist/effect-vercel.mjs +2 -2
  27. package/dist/effect.d.mts +9 -46
  28. package/dist/effect.mjs +29 -7
  29. package/dist/effect.mjs.map +1 -1
  30. package/dist/index-B1opsgls.d.mts +159 -0
  31. package/dist/{index-CEzk2gbZ.d.mts → index-Ewc__bQu.d.mts} +58 -7
  32. package/dist/index.d.mts +169 -113
  33. package/dist/index.mjs +201 -138
  34. package/dist/index.mjs.map +1 -1
  35. package/dist/{resolver-BHPsADzE.d.mts → resolver-CJOR9mQ8.d.mts} +620 -263
  36. package/dist/resolver-CxE-VDw4.mjs +598 -0
  37. package/dist/resolver-CxE-VDw4.mjs.map +1 -0
  38. package/dist/testing.d.mts +53 -44
  39. package/dist/testing.mjs +379 -107
  40. package/dist/testing.mjs.map +1 -1
  41. package/dist/{vercel-DD89DCqH.mjs → vercel-CWZ33NUS.mjs} +23 -3
  42. package/dist/vercel-CWZ33NUS.mjs.map +1 -0
  43. package/dist/vercel-DgJaUX9D.d.mts +31 -0
  44. package/dist/vercel.d.mts +3 -3
  45. package/dist/vercel.mjs +2 -2
  46. package/dist/verify-BzpiA0Es.mjs +642 -0
  47. package/dist/verify-BzpiA0Es.mjs.map +1 -0
  48. package/dist/verify-DZWLM91T.d.mts +805 -0
  49. package/dist/zone-discovery-BA5I24vu.d.mts +1390 -0
  50. package/dist/zones-DKHTpX9C.mjs +19 -0
  51. package/dist/zones-DKHTpX9C.mjs.map +1 -0
  52. package/package.json +1 -1
  53. package/dist/auth-Vi-hhXbV.mjs.map +0 -1
  54. package/dist/client-BnD7QLRF.mjs.map +0 -1
  55. package/dist/cloudflare-C6ryPlCZ.mjs.map +0 -1
  56. package/dist/cloudflare-tdjNS8vE.d.mts +0 -23
  57. package/dist/dns-record-C_KwGPLf.mjs.map +0 -1
  58. package/dist/index-CS9Rl5LQ.d.mts +0 -71
  59. package/dist/manifest-CP85ePFn.d.mts +0 -583
  60. package/dist/receipt-CGSnfZQ6.mjs +0 -28
  61. package/dist/receipt-CGSnfZQ6.mjs.map +0 -1
  62. package/dist/resolver-tEXpKxmH.mjs +0 -200
  63. package/dist/resolver-tEXpKxmH.mjs.map +0 -1
  64. package/dist/vercel-D-E2aqb1.d.mts +0 -22
  65. package/dist/vercel-DD89DCqH.mjs.map +0 -1
  66. package/dist/verify-B80xCBh2.d.mts +0 -1407
  67. package/dist/verify-BjrR_4NF.mjs +0 -1401
  68. package/dist/verify-BjrR_4NF.mjs.map +0 -1
package/README.md CHANGED
@@ -1,45 +1,39 @@
1
1
  # DomainKit
2
2
 
3
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.
4
+ authorized provisioning plans. Products can connect customer domains without making one
5
+ registrar's API or authorization model their architecture.
6
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.
7
+ DomainKit is additive and fail-closed. It creates missing records, treats exact records as no-ops,
8
+ reports incompatible state as a conflict, and never silently overwrites DNS. Every applied write is
9
+ bound to an approved plan digest and recorded in a receipt that can authorize a separate cleanup.
15
10
 
16
11
  ## Package shape
17
12
 
18
- - `domainkit` — Promise-based API for application code;
13
+ - `domainkit` — Promise API for application code;
19
14
  - `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.
15
+ - `domainkit/cloudflare` and `domainkit/vercel` — Promise provider integrations;
16
+ - `domainkit/effect/cloudflare` and `domainkit/effect/vercel` — Effect provider integrations;
17
+ - `domainkit/testing` — in-memory lifecycle capabilities and the provider conformance runner.
25
18
 
26
- The package is portable ESM built on Fetch and Web APIs. Effect is a peer dependency.
19
+ The package is portable ESM built on Fetch, Web Crypto, and Web APIs. Effect is a peer dependency.
20
+ Promise namespaces delegate to the Effect implementation rather than maintaining another planner,
21
+ authorization engine, or verifier.
27
22
 
28
- Both entry points are organized by capability namespaces. The root API accepts ordinary async
29
- providers and stores:
23
+ ## Plan, approve, and apply
30
24
 
31
25
  ```ts
32
26
  import { Provisioning } from "domainkit";
33
27
 
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
- },
28
+ const { plan } = await Provisioning.create({
29
+ provider,
40
30
  requirements,
41
- zone: "example.com",
31
+ target: Provisioning.Target.ExactZone({ zone: "example.com" }),
42
32
  });
33
+
34
+ // Present plan.operations to the user before authorizing the exact digest.
35
+ const authorization = await Provisioning.authorize(plan);
36
+ const receipt = await Provisioning.apply({ authorization, plan, provider });
43
37
  ```
44
38
 
45
39
  Effect applications provide the same capabilities as Layers and run the canonical program
@@ -49,66 +43,92 @@ directly:
49
43
  import { Effect, Layer } from "effect";
50
44
  import { Digest, DnsProvider, Provisioning } from "domainkit/effect";
51
45
 
52
- const program = Provisioning.create({ requirements, zone: "example.com" }).pipe(
53
- Effect.provide(Layer.merge(DnsProvider.layerFromAsync(provider), Digest.webCryptoLayer)),
46
+ const program = Provisioning.create({
47
+ requirements,
48
+ target: Provisioning.Target.ExactZone({ zone: "example.com" }),
49
+ }).pipe(
50
+ Effect.provide(Layer.merge(Layer.succeed(DnsProvider.Service, provider), Digest.webCryptoLayer)),
54
51
  );
55
52
  ```
56
53
 
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.
54
+ Use `DiscoverFromDomain` when the host has several authorized provider accounts. `ZoneDiscovery`
55
+ checks the requested name and its parents, returns `Resolved`, `SelectionRequired`, or `NotFound`,
56
+ and never guesses between ambiguous accounts.
60
57
 
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:
58
+ ## Connect provider accounts
65
59
 
66
- ```ts
67
- import { Cloudflare, Provisioning, Secret } from "domainkit";
60
+ `Connection.start` handles token and interactive provider methods. It returns either `Connected`
61
+ or `Redirect`; `Connection.complete` consumes an interactive continuation exactly once. Cloudflare
62
+ implements OAuth and token methods. Vercel implements its installation-code flow without
63
+ mislabeling it as generic OAuth.
68
64
 
69
- const provider = Cloudflare.make({
70
- accountId: "cloudflare-account-id",
71
- capabilities: ["dns:read", "dns:write"],
72
- token: Secret.make(apiToken),
73
- });
65
+ Cloudflare can discover the selected account from a domain already visible to the credential, so a
66
+ customer does not need to find or type an account ID. Vercel retains explicit personal or team
67
+ context returned by the installation. Both contexts are versioned, non-secret values that can
68
+ reconstruct the correct provider client later.
74
69
 
75
- const plan = await Provisioning.create({ provider, requirements, zone: "example.com" });
76
- ```
70
+ Hosts provide one `AuthorizationLifecycle` repository for an authorization aggregate, its
71
+ credential, and its owner bindings. SQL hosts can implement one transaction; split database and
72
+ vault hosts can implement a recoverable saga behind the same interface. Interactive continuations
73
+ remain separate, short-lived, one-time state. DomainKit does not choose a database, cache, vault,
74
+ callback route, session model, consent UI, or audit system.
75
+
76
+ Provider scope claims are not treated as proof. Each required capability carries `Declared`,
77
+ `Introspected`, or `Exercised` evidence. Final-binding revocation is fail-closed: durable state
78
+ remains retryable until the provider confirms revocation.
77
79
 
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.
80
+ ## Observe DNS
81
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:
82
+ `Verification.observe` is the only verification operation. Public DNS is the default:
85
83
 
86
84
  ```ts
87
- import { Secret, Vercel } from "domainkit";
85
+ import { Verification } from "domainkit";
86
+
87
+ const result = await Verification.observe({ record });
88
+ ```
89
+
90
+ The default resolver pool queries Cloudflare and Google concurrently using RFC wire-format DNS over
91
+ HTTPS and applies `AnyMatch`. Results preserve every named answer, negative response, timeout, and
92
+ failure. Hosts can supply another resolver pool or choose tagged `AllMatch` and `Quorum` policies.
88
93
 
89
- const provider = Vercel.make({
90
- capabilities: ["dns:read", "dns:write"],
91
- context: { _tag: "team", teamId: "team-id" },
92
- token: Secret.make(apiToken),
94
+ Authoritative-provider observation is opt-in:
95
+
96
+ ```ts
97
+ const result = await Verification.observe({
98
+ provider: Verification.Provider.Enabled({ provider, zone }),
99
+ publicDns: Verification.PublicDns.Disabled(),
100
+ record,
93
101
  });
94
102
  ```
95
103
 
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.
104
+ When both sources are requested, both must match for `Verified`. Every result is exhaustive and
105
+ tagged as `NotObserved`, `Pending`, `Mismatch`, `Unavailable`, or `Verified`.
106
+
107
+ ## Implement a DNS provider
108
+
109
+ Implement the narrow `DnsProvider` contract exported from `domainkit` or `domainkit/effect`. Then
110
+ run the deterministic offline contract exported from `domainkit/testing`:
111
+
112
+ ```ts
113
+ import { Effect } from "effect";
114
+ import { DomainName } from "domainkit";
115
+ import { ProviderConformance } from "domainkit/testing";
116
+
117
+ const report = await Effect.runPromise(
118
+ ProviderConformance.run({
119
+ makeProvider: ProviderConformance.fromAsync(() => makeFreshPromiseProvider()),
120
+ zone: DomainName.parse("example.com"),
121
+ }),
122
+ );
123
+ ```
124
+
125
+ The contract covers complete readback across provider pages, exact no-op, conflict, create, stale
126
+ plans, partial receipts, and receipt-bound cleanup. First-party providers run the same contract and
127
+ credential-gated live profiles against disposable records.
128
+
129
+ Schemas own external and persisted parsing; Effect `Data` values own in-process configuration and
130
+ control flow. See [the architecture decisions](docs/adr/README.md),
131
+ [provider behavior](docs/providers.md), and [runnable examples](examples/).
112
132
 
113
133
  ## License
114
134
 
@@ -1,6 +1,110 @@
1
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-C_KwGPLf.mjs";
3
- import { Clock, Effect, Schema } from "effect";
2
+ import { E as decode$2, c as Opaque, h as decode$3, y as Error } from "./dns-record-BLeqo56C.mjs";
3
+ import { f as Value, g as Method, h as Error$1, p as make$1, r as Evidence, u as codec } from "./authorization-DOg2Re1D.mjs";
4
+ import { t as candidates } from "./zones-DKHTpX9C.mjs";
5
+ import { Clock, Crypto, Effect, Schema } from "effect";
6
+ import * as oauth from "oauth4webapi";
7
+ //#region src/auth/authorization-code.ts
8
+ const start = Effect.fn("AuthorizationCode.start")(function* (input) {
9
+ const crypto = yield* Crypto.Crypto;
10
+ const codeVerifier = base64Url(yield* randomBytes(crypto, 32));
11
+ const challenge = base64Url(yield* digest(crypto, codeVerifier));
12
+ const authorizationUrl = new URL(input.method.authorizationServer.authorization_endpoint);
13
+ authorizationUrl.search = new URLSearchParams({
14
+ client_id: input.client.clientId,
15
+ code_challenge: challenge,
16
+ code_challenge_method: "S256",
17
+ redirect_uri: input.redirectUri,
18
+ response_type: "code",
19
+ scope: input.method.scopes.join(" "),
20
+ state: input.state
21
+ }).toString();
22
+ return {
23
+ authorizationUrl,
24
+ payload: make$1(JSON.stringify({
25
+ codeVerifier,
26
+ redirectUri: input.redirectUri,
27
+ state: input.state
28
+ }))
29
+ };
30
+ });
31
+ const complete = Effect.fn("AuthorizationCode.complete")(function* (input) {
32
+ const payload = yield* decodePayload(input.payload);
33
+ if (input.callbackUrl.searchParams.get("state") !== payload.state) return yield* failure$2("Authorization callback state does not match its continuation");
34
+ const server = input.method.authorizationServer;
35
+ const client = { client_id: input.client.clientId };
36
+ const authentication = yield* clientAuthentication(input.method, input.client);
37
+ return yield* Effect.tryPromise({
38
+ try: async (signal) => {
39
+ const callback = oauth.validateAuthResponse(server, client, input.callbackUrl, payload.state);
40
+ const response = await oauth.authorizationCodeGrantRequest(server, client, authentication, callback, payload.redirectUri, payload.codeVerifier, requestOptions(input.fetch, signal));
41
+ return oauth.processAuthorizationCodeResponse(server, client, response);
42
+ },
43
+ catch: (cause) => new Error$1({
44
+ category: "provider",
45
+ message: cause instanceof globalThis.Error ? cause.name : "OAuth provider request failed",
46
+ operation: "AuthorizationCode.complete",
47
+ retry: "unknown"
48
+ })
49
+ });
50
+ });
51
+ const PayloadSchema = Schema.Struct({
52
+ codeVerifier: Schema.String,
53
+ redirectUri: Schema.String,
54
+ state: Schema.String
55
+ });
56
+ function decodePayload(payload) {
57
+ return Schema.decodeUnknownEffect(Schema.fromJsonString(PayloadSchema))(payload.expose()).pipe(Effect.mapError(() => new Error$1({
58
+ category: "authorization",
59
+ message: "OAuth continuation payload is invalid",
60
+ operation: "AuthorizationCode",
61
+ retry: "after-user-action"
62
+ })));
63
+ }
64
+ function clientAuthentication(method, client) {
65
+ switch (method.clientAuth) {
66
+ case "none": return Effect.succeed(oauth.None());
67
+ case "client_secret_basic": return client.clientSecret === void 0 ? failure$2("OAuth client secret is required") : Effect.succeed(oauth.ClientSecretBasic(client.clientSecret.expose()));
68
+ case "client_secret_post": return client.clientSecret === void 0 ? failure$2("OAuth client secret is required") : Effect.succeed(oauth.ClientSecretPost(client.clientSecret.expose()));
69
+ }
70
+ }
71
+ function requestOptions(fetchImplementation, signal) {
72
+ return fetchImplementation === void 0 ? { signal } : {
73
+ signal,
74
+ [oauth.customFetch]: (url, options) => fetchImplementation(url, {
75
+ ...options,
76
+ body: options.body
77
+ })
78
+ };
79
+ }
80
+ const failure$2 = (message) => Effect.fail(new Error$1({
81
+ category: "authorization",
82
+ message,
83
+ operation: "AuthorizationCode",
84
+ retry: "after-user-action"
85
+ }));
86
+ function randomBytes(crypto, size) {
87
+ return crypto.randomBytes(size).pipe(Effect.mapError((cause) => new Error$1({
88
+ category: "validation",
89
+ message: cause.message,
90
+ operation: "AuthorizationCode.start",
91
+ retry: "safe"
92
+ })));
93
+ }
94
+ function digest(crypto, value) {
95
+ return crypto.digest("SHA-256", new TextEncoder().encode(value)).pipe(Effect.mapError((cause) => new Error$1({
96
+ category: "validation",
97
+ message: cause.message,
98
+ operation: "AuthorizationCode.start",
99
+ retry: "safe"
100
+ })));
101
+ }
102
+ function base64Url(bytes) {
103
+ let binary = "";
104
+ for (const byte of bytes) binary += String.fromCharCode(byte);
105
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
106
+ }
107
+ //#endregion
4
108
  //#region src/providers/cloudflare/protocol.ts
5
109
  const ErrorDetail = Schema.Struct({
6
110
  code: Schema.Number,
@@ -81,7 +185,7 @@ const TokenEnvelope = Schema.Struct({
81
185
  });
82
186
  //#endregion
83
187
  //#region src/providers/cloudflare/records.ts
84
- const Data = Schema.Struct({
188
+ const Data$1 = Schema.Struct({
85
189
  flags: Schema.optionalKey(Schema.Number),
86
190
  port: Schema.optionalKey(Schema.Number),
87
191
  priority: Schema.optionalKey(Schema.Number),
@@ -202,7 +306,7 @@ function encode(record) {
202
306
  };
203
307
  }
204
308
  }
205
- const decodeData = Effect.fn("CloudflareRecords.decodeData")((record) => Schema.decodeUnknownEffect(Data)(record.data).pipe(Effect.mapError((cause) => failure$1("decodeRecord", cause.message))));
309
+ const decodeData = Effect.fn("CloudflareRecords.decodeData")((record) => Schema.decodeUnknownEffect(Data$1)(record.data).pipe(Effect.mapError((cause) => failure$1("decodeRecord", cause.message))));
206
310
  function failure$1(operation, message, fields = {}) {
207
311
  return new Error({
208
312
  message,
@@ -213,9 +317,23 @@ function failure$1(operation, message, fields = {}) {
213
317
  }
214
318
  //#endregion
215
319
  //#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) {
320
+ var client_exports = /* @__PURE__ */ __exportAll({
321
+ discoverAuthorizationAccount: () => discoverAuthorizationAccount,
322
+ discovery: () => discovery,
323
+ make: () => make,
324
+ validateDomainToken: () => validateDomainToken
325
+ });
326
+ /** Exposes Cloudflare zone lookup as the optional provider discovery capability. */
327
+ function discovery(client) {
328
+ return {
329
+ listZones: (name) => client.listZones({ name }).pipe(Effect.map((zones) => zones.map((zone) => ({
330
+ ...zone,
331
+ status: zone.status === "active" || zone.status === "pending" ? zone.status : "unknown"
332
+ })))),
333
+ provider: client
334
+ };
335
+ }
336
+ const makeTransport = (options) => {
219
337
  const fetch = options.fetch ?? globalThis.fetch;
220
338
  const baseUrl = (options.baseUrl ?? "https://api.cloudflare.com/client/v4").replace(/\/$/, "");
221
339
  const request = Effect.fn("CloudflareClient.request")((path, operation, init) => Effect.gen(function* () {
@@ -258,17 +376,62 @@ function make(options) {
258
376
  yield* ensureSuccess(body, "listZones", response);
259
377
  return yield* decode(ZoneListEnvelope, body, "listZones", response);
260
378
  }));
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
- }));
379
+ return {
380
+ allZones: Effect.fn("CloudflareClient.allZones")((accountId, name) => Effect.gen(function* () {
381
+ const zones = [];
382
+ let page = 1;
383
+ while (true) {
384
+ const envelope = yield* listZonesPage(accountId, name, page);
385
+ zones.push(...envelope.result);
386
+ const totalPages = envelope.result_info?.total_pages;
387
+ if (totalPages !== void 0 && page >= totalPages || totalPages === void 0 && envelope.result.length < 50) return zones;
388
+ page += 1;
389
+ }
390
+ })),
391
+ inspectToken: Effect.fn("CloudflareClient.inspectToken")((path) => Effect.gen(function* () {
392
+ const { body, response } = yield* request(path, "validateToken");
393
+ yield* ensureSuccess(body, "validateToken", response);
394
+ const envelope = yield* decode(TokenEnvelope, body, "validateToken", response);
395
+ if (envelope.result.status !== "active") return yield* Effect.fail(failure("validateToken", `Cloudflare token is ${envelope.result.status}`, {
396
+ reason: "authentication",
397
+ status: response.status
398
+ }));
399
+ const expiresAt = envelope.result.expires_on === void 0 ? null : new Date(envelope.result.expires_on);
400
+ if (expiresAt !== null && Number.isNaN(expiresAt.valueOf())) return yield* Effect.fail(failure("validateToken", "Cloudflare token expiry is not a valid timestamp", {
401
+ reason: "response",
402
+ status: response.status
403
+ }));
404
+ return expiresAt;
405
+ })),
406
+ request
407
+ };
408
+ };
409
+ const discoverAccount = Effect.fn("CloudflareClient.discoverAccount")((options) => Effect.gen(function* () {
410
+ const { allZones } = makeTransport(options);
411
+ for (const candidate of candidates(options.domain)) {
412
+ const matches = yield* allZones(void 0, candidate);
413
+ const match = matches[0];
414
+ if (matches.length === 0) continue;
415
+ if (matches.length === 1 && match !== void 0) return match.account;
416
+ return yield* Effect.fail(failure("discoverAccount", `Cloudflare returned multiple accessible zones named ${candidate}`, { reason: "conflict" }));
417
+ }
418
+ return yield* Effect.fail(failure("discoverAccount", `Cloudflare did not expose an accessible zone for ${options.domain}`, { reason: "not_found" }));
419
+ }));
420
+ const discoverAuthorizationAccount = discoverAccount;
421
+ const validateDomainToken = Effect.fn("CloudflareClient.validateDomainToken")((options) => Effect.gen(function* () {
422
+ const transport = makeTransport(options);
423
+ const account = options.tokenKind === "account" ? yield* discoverAccount(options) : void 0;
424
+ const expiresAt = yield* transport.inspectToken(account === void 0 ? "/user/tokens/verify" : `/accounts/${encodeURIComponent(account.id)}/tokens/verify`);
425
+ return {
426
+ accountId: (account ?? (yield* discoverAccount(options))).id,
427
+ capabilities: options.capabilities,
428
+ expiresAt,
429
+ scopes: []
430
+ };
431
+ }));
432
+ /** Creates an Effect-native Cloudflare client without owning the credential lifecycle. */
433
+ function make(options) {
434
+ const { allZones, inspectToken, request } = makeTransport(options);
272
435
  const resolveZone = Effect.fn("CloudflareClient.resolveZone")((name) => Effect.gen(function* () {
273
436
  const matches = yield* allZones(options.accountId, name);
274
437
  const match = matches[0];
@@ -328,19 +491,8 @@ function make(options) {
328
491
  listZones: Effect.fn("CloudflareClient.listZones")((input = {}) => allZones(input.accountId ?? options.accountId, input.name).pipe(Effect.flatMap((zones) => Effect.forEach(zones, projectZone)))),
329
492
  validateToken: Effect.fn("CloudflareClient.validateToken")(() => Effect.gen(function* () {
330
493
  const path = options.tokenKind === "account" ? `/accounts/${encodeURIComponent(options.accountId)}/tokens/verify` : "/user/tokens/verify";
331
- const { body, response } = yield* request(path, "validateToken");
332
- yield* ensureSuccess(body, "validateToken", response);
333
- const envelope = yield* decode(TokenEnvelope, body, "validateToken", response);
334
- if (envelope.result.status !== "active") return yield* Effect.fail(failure("validateToken", `Cloudflare token is ${envelope.result.status}`, {
335
- reason: "authentication",
336
- status: response.status
337
- }));
494
+ const expiresAt = yield* inspectToken(path);
338
495
  yield* allZones(options.accountId);
339
- const expiresAt = envelope.result.expires_on === void 0 ? null : new Date(envelope.result.expires_on);
340
- if (expiresAt !== null && Number.isNaN(expiresAt.valueOf())) return yield* Effect.fail(failure("validateToken", "Cloudflare token expiry is not a valid timestamp", {
341
- reason: "response",
342
- status: response.status
343
- }));
344
496
  return {
345
497
  accountId: options.accountId,
346
498
  capabilities: options.capabilities,
@@ -410,9 +562,13 @@ function failure(operation, message, fields = {}) {
410
562
  //#endregion
411
563
  //#region src/providers/cloudflare/auth.ts
412
564
  var auth_exports = /* @__PURE__ */ __exportAll({
565
+ contextCodec: () => contextCodec,
413
566
  manifest: () => manifest,
567
+ oauthAuthentication: () => oauthAuthentication,
568
+ oauthFlow: () => oauthFlow,
414
569
  oauthMethod: () => oauthMethod,
415
570
  subjectResolver: () => subjectResolver,
571
+ tokenConnectionMethod: () => tokenConnectionMethod,
416
572
  tokenMethod: () => tokenMethod,
417
573
  tokenValidator: () => tokenValidator
418
574
  });
@@ -422,6 +578,8 @@ const authorizationServer = {
422
578
  revocation_endpoint: "https://dash.cloudflare.com/oauth2/revoke",
423
579
  token_endpoint: "https://dash.cloudflare.com/oauth2/token"
424
580
  };
581
+ const Context$1 = Schema.Struct({ tokenKind: Schema.Literals(["account", "user"]) });
582
+ const contextCodec = codec("cloudflare.v1", Context$1);
425
583
  /** Describes Cloudflare's caller-created API-token authorization method. */
426
584
  function tokenMethod(capabilities) {
427
585
  return {
@@ -450,26 +608,131 @@ function manifest(options) {
450
608
  /** Resolves the Cloudflare account selected by a completed OAuth grant. */
451
609
  function subjectResolver(options) {
452
610
  return (tokens, accessToken) => Effect.gen(function* () {
453
- yield* make({
454
- ...options,
611
+ const accountId = options.accountId ?? (yield* discoverAuthorizationAccount({
612
+ ...options.baseUrl === void 0 ? {} : { baseUrl: options.baseUrl },
613
+ domain: options.domain,
614
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch },
615
+ token: accessToken
616
+ }).pipe(Effect.map((account) => account.id)));
617
+ if (options.accountId !== void 0) yield* make({
618
+ accountId: options.accountId,
619
+ ...options.baseUrl === void 0 ? {} : { baseUrl: options.baseUrl },
620
+ capabilities: options.capabilities,
621
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch },
455
622
  token: accessToken,
456
623
  tokenKind: "user"
457
624
  }).listZones();
458
625
  const now = yield* Clock.currentTimeMillis;
459
626
  return {
460
- accountId: options.accountId,
627
+ accountId,
461
628
  expiresAt: typeof tokens.expires_in === "number" ? new Date(now + tokens.expires_in * 1e3) : null
462
629
  };
463
630
  });
464
631
  }
465
632
  /** Creates a host callback that validates a Cloudflare API token. */
466
633
  function tokenValidator(options) {
467
- return (token) => make({
468
- ...options,
469
- token
634
+ return (token) => options.accountId === void 0 ? validateDomainToken({
635
+ ...options.baseUrl === void 0 ? {} : { baseUrl: options.baseUrl },
636
+ capabilities: options.capabilities,
637
+ domain: options.domain,
638
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch },
639
+ token,
640
+ ...options.tokenKind === void 0 ? {} : { tokenKind: options.tokenKind }
641
+ }) : make({
642
+ accountId: options.accountId,
643
+ ...options.baseUrl === void 0 ? {} : { baseUrl: options.baseUrl },
644
+ capabilities: options.capabilities,
645
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch },
646
+ token,
647
+ ...options.tokenKind === void 0 ? {} : { tokenKind: options.tokenKind }
470
648
  }).validateToken();
471
649
  }
650
+ /** Creates a provider-independent token connection method backed by Cloudflare validation. */
651
+ function tokenConnectionMethod(options) {
652
+ const requiredCapabilities = [...options.capabilities];
653
+ return Method.Token({
654
+ authenticate: Effect.fn("CloudflareAuth.authenticateToken")(function* (token) {
655
+ const validated = yield* tokenValidator(options)(token);
656
+ const observedAt = new Date(yield* Clock.currentTimeMillis);
657
+ return {
658
+ capabilityEvidence: validated.capabilities.map((capability) => ({
659
+ capability,
660
+ evidence: Evidence.Introspected({ observedAt })
661
+ })),
662
+ credential: {
663
+ accessToken: token,
664
+ refreshToken: null,
665
+ tokenType: "bearer"
666
+ },
667
+ expiresAt: validated.expiresAt,
668
+ providerAccountId: validated.accountId,
669
+ providerContext: yield* contextCodec.encode({ tokenKind: options.tokenKind ?? "user" }),
670
+ scopes: [...validated.scopes]
671
+ };
672
+ }),
673
+ providerId: "cloudflare",
674
+ requiredCapabilities,
675
+ token: options.token
676
+ });
677
+ }
678
+ /** Converts a completed Cloudflare OAuth grant into the canonical connection authentication. */
679
+ const oauthAuthentication = Effect.fn("CloudflareAuth.oauthAuthentication")(function* (options) {
680
+ const resolved = yield* subjectResolver(options)(options.tokens, options.accessToken);
681
+ const observedAt = new Date(yield* Clock.currentTimeMillis);
682
+ return {
683
+ capabilityEvidence: options.capabilities.map((capability) => ({
684
+ capability,
685
+ evidence: Evidence.Introspected({ observedAt })
686
+ })),
687
+ credential: {
688
+ accessToken: options.accessToken,
689
+ refreshToken: options.tokens.refresh_token === void 0 ? null : Value.from(options.tokens.refresh_token),
690
+ tokenType: options.tokens.token_type
691
+ },
692
+ expiresAt: resolved.expiresAt,
693
+ providerAccountId: resolved.accountId,
694
+ providerContext: yield* contextCodec.encode({ tokenKind: "user" }),
695
+ scopes: (options.tokens.scope ?? "").split(" ").filter(Boolean)
696
+ };
697
+ });
698
+ /** Creates Cloudflare's OAuth implementation of the common interactive connection capability. */
699
+ function oauthFlow(options) {
700
+ const method = oauthMethod({
701
+ capabilities: options.capabilities,
702
+ clientAuth: options.clientAuth,
703
+ scopes: options.scopes
704
+ });
705
+ return {
706
+ complete: Effect.fn("CloudflareAuth.oauthFlow.complete")(function* (payload, callbackUrl) {
707
+ const tokens = yield* complete({
708
+ callbackUrl,
709
+ client: options.client,
710
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch },
711
+ method,
712
+ payload,
713
+ providerId: "cloudflare"
714
+ });
715
+ return yield* oauthAuthentication({
716
+ ...options.accountId === void 0 ? { domain: options.domain } : { accountId: options.accountId },
717
+ ...options.baseUrl === void 0 ? {} : { baseUrl: options.baseUrl },
718
+ capabilities: options.capabilities,
719
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch },
720
+ accessToken: make$1(tokens.access_token),
721
+ tokens
722
+ });
723
+ }),
724
+ method: "oauth2",
725
+ providerId: "cloudflare",
726
+ requiredCapabilities: [...options.capabilities],
727
+ start: (continuationId) => start({
728
+ client: options.client,
729
+ method,
730
+ redirectUri: options.redirectUri,
731
+ state: continuationId
732
+ })
733
+ };
734
+ }
472
735
  //#endregion
473
- export { tokenMethod as a, subjectResolver as i, manifest as n, client_exports as o, oauthMethod as r, make as s, auth_exports as t };
736
+ export { subjectResolver as a, tokenValidator as c, make as d, oauthMethod as i, client_exports as l, manifest as n, tokenConnectionMethod as o, oauthFlow as r, tokenMethod as s, auth_exports as t, discovery as u };
474
737
 
475
- //# sourceMappingURL=auth-Vi-hhXbV.mjs.map
738
+ //# sourceMappingURL=auth-CZ1pZHMZ.mjs.map