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.
- package/README.md +91 -71
- package/dist/{auth-Vi-hhXbV.mjs → auth-CZ1pZHMZ.mjs} +301 -38
- package/dist/auth-CZ1pZHMZ.mjs.map +1 -0
- package/dist/{client-BnD7QLRF.mjs → auth-CzhZ3Cuc.mjs} +235 -131
- package/dist/auth-CzhZ3Cuc.mjs.map +1 -0
- package/dist/authorization-DOg2Re1D.mjs +247 -0
- package/dist/authorization-DOg2Re1D.mjs.map +1 -0
- package/dist/canonical-json-CpvqGMcr.mjs +57 -0
- package/dist/canonical-json-CpvqGMcr.mjs.map +1 -0
- package/dist/{cloudflare-C6ryPlCZ.mjs → cloudflare-BedFw4cX.mjs} +28 -7
- package/dist/cloudflare-BedFw4cX.mjs.map +1 -0
- package/dist/cloudflare-FDl5q7Dm.d.mts +32 -0
- package/dist/cloudflare.d.mts +3 -3
- package/dist/cloudflare.mjs +2 -2
- package/dist/connection-BWrBwqjk.mjs +58 -0
- package/dist/connection-BWrBwqjk.mjs.map +1 -0
- package/dist/connection-CEqadXSm.d.mts +182 -0
- package/dist/connection-CcQokgrd.mjs +186 -0
- package/dist/connection-CcQokgrd.mjs.map +1 -0
- package/dist/connection-api-DftK7eHo.d.mts +7 -0
- package/dist/{dns-record-C_KwGPLf.mjs → dns-record-BLeqo56C.mjs} +23 -3
- package/dist/dns-record-BLeqo56C.mjs.map +1 -0
- package/dist/effect-cloudflare.d.mts +2 -2
- package/dist/effect-cloudflare.mjs +2 -2
- package/dist/effect-vercel.d.mts +2 -2
- package/dist/effect-vercel.mjs +2 -2
- package/dist/effect.d.mts +9 -46
- package/dist/effect.mjs +29 -7
- package/dist/effect.mjs.map +1 -1
- package/dist/index-B1opsgls.d.mts +159 -0
- package/dist/{index-CEzk2gbZ.d.mts → index-Ewc__bQu.d.mts} +58 -7
- package/dist/index.d.mts +169 -113
- package/dist/index.mjs +201 -138
- package/dist/index.mjs.map +1 -1
- package/dist/{resolver-BHPsADzE.d.mts → resolver-CJOR9mQ8.d.mts} +620 -263
- package/dist/resolver-CxE-VDw4.mjs +598 -0
- package/dist/resolver-CxE-VDw4.mjs.map +1 -0
- package/dist/testing.d.mts +53 -44
- package/dist/testing.mjs +379 -107
- package/dist/testing.mjs.map +1 -1
- package/dist/{vercel-DD89DCqH.mjs → vercel-CWZ33NUS.mjs} +23 -3
- package/dist/vercel-CWZ33NUS.mjs.map +1 -0
- package/dist/vercel-DgJaUX9D.d.mts +31 -0
- package/dist/vercel.d.mts +3 -3
- package/dist/vercel.mjs +2 -2
- package/dist/verify-BzpiA0Es.mjs +642 -0
- package/dist/verify-BzpiA0Es.mjs.map +1 -0
- package/dist/verify-DZWLM91T.d.mts +805 -0
- package/dist/zone-discovery-BA5I24vu.d.mts +1390 -0
- package/dist/zones-DKHTpX9C.mjs +19 -0
- package/dist/zones-DKHTpX9C.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/auth-Vi-hhXbV.mjs.map +0 -1
- package/dist/client-BnD7QLRF.mjs.map +0 -1
- package/dist/cloudflare-C6ryPlCZ.mjs.map +0 -1
- package/dist/cloudflare-tdjNS8vE.d.mts +0 -23
- package/dist/dns-record-C_KwGPLf.mjs.map +0 -1
- package/dist/index-CS9Rl5LQ.d.mts +0 -71
- package/dist/manifest-CP85ePFn.d.mts +0 -583
- package/dist/receipt-CGSnfZQ6.mjs +0 -28
- package/dist/receipt-CGSnfZQ6.mjs.map +0 -1
- package/dist/resolver-tEXpKxmH.mjs +0 -200
- package/dist/resolver-tEXpKxmH.mjs.map +0 -1
- package/dist/vercel-D-E2aqb1.d.mts +0 -22
- package/dist/vercel-DD89DCqH.mjs.map +0 -1
- package/dist/verify-B80xCBh2.d.mts +0 -1407
- package/dist/verify-BjrR_4NF.mjs +0 -1401
- 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.
|
|
5
|
-
|
|
4
|
+
authorized provisioning plans. Products can connect customer domains without making one
|
|
5
|
+
registrar's API or authorization model their architecture.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
13
|
+
- `domainkit` — Promise API for application code;
|
|
19
14
|
- `domainkit/effect` — canonical Effect-native services and programs;
|
|
20
|
-
- `domainkit/cloudflare` — Promise
|
|
21
|
-
- `domainkit/effect/cloudflare` —
|
|
22
|
-
- `domainkit/
|
|
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
|
-
|
|
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({
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
assigned during OAuth client registration. `Cloudflare.Auth.tokenMethod` describes the equivalent
|
|
80
|
-
API-token capability.
|
|
80
|
+
## Observe DNS
|
|
81
81
|
|
|
82
|
-
|
|
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 {
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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 {
|
|
3
|
-
import {
|
|
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({
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
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*
|
|
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
|
|
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) =>
|
|
468
|
-
...options,
|
|
469
|
-
|
|
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 {
|
|
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-
|
|
738
|
+
//# sourceMappingURL=auth-CZ1pZHMZ.mjs.map
|