domainkit 0.20.0 → 0.21.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 +32 -1
- package/dist/Cloudflare.mjs +19 -10
- package/dist/Cloudflare.mjs.map +1 -1
- package/dist/Connect.mjs +30 -10
- package/dist/Connect.mjs.map +1 -1
- package/dist/Provider.mjs.map +1 -1
- package/dist/Server.mjs +121 -22
- package/dist/Server.mjs.map +1 -1
- package/dist/Vercel.mjs +20 -10
- package/dist/Vercel.mjs.map +1 -1
- package/dist/Verify.mjs +4 -3
- package/dist/Verify.mjs.map +1 -1
- package/dist/entry/client.mjs +1 -2
- package/dist/entry/server.mjs +1 -2
- package/dist/internal/callback.mjs +13 -0
- package/dist/internal/callback.mjs.map +1 -0
- package/dist/internal/config.mjs +19 -5
- package/dist/internal/config.mjs.map +1 -1
- package/dist/internal/provider-async.mjs +2 -0
- package/dist/internal/provider-async.mjs.map +1 -1
- package/dist/package.mjs +1 -1
- package/dist/types/Connect.d.ts +134 -0
- package/dist/types/Provider.d.ts +9 -0
- package/dist/types/Server.d.ts +45 -0
- package/dist/types/Verify.d.ts +5 -1
- package/dist/types/internal/callback.d.ts +4 -0
- package/dist/types/internal/config.d.ts +8 -1
- package/dist/types/internal/provider-async.d.ts +2 -0
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Add custom domains to a TypeScript SaaS application.
|
|
4
4
|
|
|
5
|
+
Documentation: [domain-kit.dev](https://domain-kit.dev)
|
|
6
|
+
|
|
5
7
|
DomainKit turns DNS requirements into plans a customer can review, applies only the plan digest
|
|
6
8
|
they approved, keeps a receipt of every write, and plans cleanup from that receipt. Cloudflare and
|
|
7
9
|
Vercel are built in; a provider is one declarative value, so tokens-only providers are the same
|
|
@@ -10,7 +12,7 @@ shape minus the OAuth case.
|
|
|
10
12
|
## Install
|
|
11
13
|
|
|
12
14
|
```sh
|
|
13
|
-
npm install domainkit effect
|
|
15
|
+
npm install domainkit effect@^4
|
|
14
16
|
```
|
|
15
17
|
|
|
16
18
|
Node.js 24.10 or newer and Effect 4 are required.
|
|
@@ -184,6 +186,35 @@ recognise a credential the browser sends on a top-level navigation.
|
|
|
184
186
|
authenticated principal reaches every route, which is right when your own middleware already gates
|
|
185
187
|
the mount.
|
|
186
188
|
|
|
189
|
+
`Server.registrationSettings({ providers, callbackBaseUrl })` derives secret-free registration
|
|
190
|
+
requirements from the same provider definitions and public callback base used by your server.
|
|
191
|
+
It needs no credentials, identity, storage, or network: OAuth settings describe callback URL,
|
|
192
|
+
client authentication, S256 PKCE, and configured scopes; Integration settings describe callback URL
|
|
193
|
+
and code exchange authentication. Token-only definitions return no settings. Custom interactive
|
|
194
|
+
methods without optional registration metadata fail `Unsupported`. Settings describe requirements,
|
|
195
|
+
not console readiness or granted permissions. See the
|
|
196
|
+
[server reference](https://domain-kit.dev/docs/reference/server#registration-settings).
|
|
197
|
+
|
|
198
|
+
Interactive starts reject empty credentials and malformed setup fields before redirecting or
|
|
199
|
+
storing a continuation, with `InvalidInput` naming the field without rejected values. Vercel resolves
|
|
200
|
+
its required credentials at start and again at completion for rotation. Configured callback bases
|
|
201
|
+
must be HTTP(S) URLs without credentials, query, or fragment; emulator paths remain supported.
|
|
202
|
+
|
|
203
|
+
`Server.Options.callback` optionally selects a same-origin destination for sanitized `Connected`,
|
|
204
|
+
`Cancelled`, or `Failed` outcomes. Return `Effect.succeed(undefined)` to retain the default response,
|
|
205
|
+
or an Effect of a destination string. The policy receives provider, validated `returnTo`, and
|
|
206
|
+
`connectionId` on success or `recovery: "restart" | "inspect"` on failure; it receives no request,
|
|
207
|
+
state, code, provider text, secret, or full storage row. Hosts close over acquired services.
|
|
208
|
+
|
|
209
|
+
Only live flows matching the authenticated owner, actor, and provider are classified inside the
|
|
210
|
+
completion lock. Unknown, expired, spent, and mismatched callbacks invoke no policy, including read
|
|
211
|
+
races. Only `access_denied` is cancellation; other provider errors are failure. Cancellation and
|
|
212
|
+
failure retain expiry and retry behavior. `Connected` means durable account access, not DNS readiness;
|
|
213
|
+
zone selection can still be required. Partial persistence or a policy failure after connection needs
|
|
214
|
+
inspection, not a rollback assumption. `Connect.completeOutcome` exposes the shared classified result
|
|
215
|
+
to Effect hosts; `Connect.complete` retains its result/error contract. See the
|
|
216
|
+
[server reference](https://domain-kit.dev/docs/reference/server#callback-destinations) for compiled usage.
|
|
217
|
+
|
|
187
218
|
After an interactive connection completes, the callback redirects to the `returnTo` the flow was
|
|
188
219
|
started with, or to `defaultReturnTo`. The destination is resolved against the callback's own base
|
|
189
220
|
and must land on its origin, so neither the provider nor a crafted `returnTo` can steer the customer
|
package/dist/Cloudflare.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { fail } from "./internal/error.mjs";
|
|
|
5
5
|
import { authorizationUrl, exchangeCode, refresh, revoke } from "./internal/oauth.mjs";
|
|
6
6
|
import { make, resolveAmong, tokenAuth } from "./Provider.mjs";
|
|
7
7
|
import { dns, listZones, provider as provider$1, tokenExpiry, verifyToken } from "./internal/cloudflare/client.mjs";
|
|
8
|
-
import { resolve } from "./internal/config.mjs";
|
|
8
|
+
import { endpoint, nonempty, requireValue, resolve, scopes, secret } from "./internal/config.mjs";
|
|
9
9
|
import { Effect, Redacted, Schema } from "effect";
|
|
10
10
|
//#region src/Cloudflare.ts
|
|
11
11
|
/** Cloudflare: personal API tokens and OAuth. */
|
|
@@ -76,11 +76,11 @@ const packSecret = (tokens) => Redacted.make(Schema.encodeSync(Secret)({
|
|
|
76
76
|
}));
|
|
77
77
|
const provider = (options = {}) => {
|
|
78
78
|
const fetch = options.fetch ?? globalThis.fetch;
|
|
79
|
-
const baseUrl =
|
|
79
|
+
const baseUrl = options.baseUrl ?? "https://api.cloudflare.com/client/v4";
|
|
80
80
|
const client = (token) => ({
|
|
81
81
|
token,
|
|
82
82
|
fetch,
|
|
83
|
-
baseUrl
|
|
83
|
+
baseUrl: baseUrl.replace(/\/$/, "")
|
|
84
84
|
});
|
|
85
85
|
/**
|
|
86
86
|
* The one account every zone the credential sees belongs to, with the name Cloudflare gives it.
|
|
@@ -102,7 +102,12 @@ const provider = (options = {}) => {
|
|
|
102
102
|
nameservers: zone.name_servers ?? []
|
|
103
103
|
});
|
|
104
104
|
const oauthClient = (oauth) => Effect.gen(function* () {
|
|
105
|
-
|
|
105
|
+
yield* endpoint(options.baseUrl ?? "https://api.cloudflare.com/client/v4", "baseUrl");
|
|
106
|
+
yield* endpoint(oauth.issuer ?? defaultIssuer, "oauth.issuer");
|
|
107
|
+
yield* endpoint(oauth.serverOrigin ?? oauth.issuer ?? defaultIssuer, "oauth.serverOrigin");
|
|
108
|
+
yield* scopes(oauth.scopes ?? defaultScopes, "oauth.scopes");
|
|
109
|
+
yield* requireValue(oauth.clientAuth, (v) => v === void 0 || v === "none" || v === "client_secret_basic" || v === "client_secret_post", "oauth.clientAuth", "oauth.clientAuth is unsupported");
|
|
110
|
+
const clientId = yield* resolve(oauth.clientId, "oauth.clientId").pipe(Effect.flatMap((v) => nonempty(v, "oauth.clientId")));
|
|
106
111
|
if (oauth.clientAuth === "none") return {
|
|
107
112
|
clientId,
|
|
108
113
|
clientSecret: null,
|
|
@@ -110,7 +115,7 @@ const provider = (options = {}) => {
|
|
|
110
115
|
};
|
|
111
116
|
return {
|
|
112
117
|
clientId,
|
|
113
|
-
clientSecret: yield* resolve(oauth.clientSecret, "oauth.clientSecret"),
|
|
118
|
+
clientSecret: yield* resolve(oauth.clientSecret, "oauth.clientSecret").pipe(Effect.flatMap((v) => secret(v, "oauth.clientSecret"))),
|
|
114
119
|
clientAuth: oauth.clientAuth ?? "client_secret_basic"
|
|
115
120
|
};
|
|
116
121
|
});
|
|
@@ -124,16 +129,20 @@ const provider = (options = {}) => {
|
|
|
124
129
|
function oauthAuth(settings) {
|
|
125
130
|
const scopes = settings.scopes ?? defaultScopes;
|
|
126
131
|
const browser = settings.issuer ?? defaultIssuer;
|
|
127
|
-
const endpoints = endpointsOf({
|
|
132
|
+
const endpoints = () => endpointsOf({
|
|
128
133
|
browser,
|
|
129
134
|
server: settings.serverOrigin ?? browser
|
|
130
135
|
});
|
|
131
136
|
const insecure = settings.allowPlaintext === void 0 ? {} : { allowPlaintext: settings.allowPlaintext };
|
|
132
137
|
return {
|
|
133
138
|
label: "Sign in with Cloudflare",
|
|
139
|
+
registration: {
|
|
140
|
+
clientAuth: settings.clientAuth ?? "client_secret_basic",
|
|
141
|
+
pkce: "S256"
|
|
142
|
+
},
|
|
134
143
|
scopes,
|
|
135
144
|
start: (input) => oauthClient(settings).pipe(Effect.map(({ clientId }) => ({ authorizationUrl: authorizationUrl({
|
|
136
|
-
server: endpoints,
|
|
145
|
+
server: endpoints(),
|
|
137
146
|
clientId,
|
|
138
147
|
scopes,
|
|
139
148
|
state: input.state,
|
|
@@ -142,7 +151,7 @@ const provider = (options = {}) => {
|
|
|
142
151
|
}) }))),
|
|
143
152
|
complete: (input) => oauthClient(settings).pipe(Effect.flatMap((oauthClientValue) => exchangeCode({
|
|
144
153
|
provider: provider$1,
|
|
145
|
-
server: endpoints,
|
|
154
|
+
server: endpoints(),
|
|
146
155
|
client: oauthClientValue,
|
|
147
156
|
code: input.code,
|
|
148
157
|
state: input.params.state ?? "",
|
|
@@ -156,8 +165,8 @@ const provider = (options = {}) => {
|
|
|
156
165
|
if (refreshToken === null) return yield* fail(new Unauthenticated({ message: "Cloudflare credential has no refresh token" }));
|
|
157
166
|
const tokens = yield* refresh({
|
|
158
167
|
provider: provider$1,
|
|
159
|
-
server: endpoints,
|
|
160
168
|
client: yield* oauthClient(settings),
|
|
169
|
+
server: endpoints(),
|
|
161
170
|
refreshToken,
|
|
162
171
|
fetch,
|
|
163
172
|
...insecure
|
|
@@ -172,8 +181,8 @@ const provider = (options = {}) => {
|
|
|
172
181
|
const { accessToken } = parseSecret(credential.secret);
|
|
173
182
|
yield* revoke({
|
|
174
183
|
provider: provider$1,
|
|
175
|
-
server: endpoints,
|
|
176
184
|
client: yield* oauthClient(settings),
|
|
185
|
+
server: endpoints(),
|
|
177
186
|
token: accessToken,
|
|
178
187
|
fetch,
|
|
179
188
|
...insecure
|
package/dist/Cloudflare.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Cloudflare.mjs","names":["Client.listZones","OAuth.authorizationUrl","OAuth.exchangeCode","Client.provider","Errors.fail","Reason.Unauthenticated","OAuth.refresh","OAuth.revoke","Provider.make","Provider.tokenAuth","Client.verifyToken","Client.tokenExpiry","DomainName.decode","Provider.resolveAmong","Reason.Unsupported","Client.dns"],"sources":["../src/Cloudflare.ts"],"sourcesContent":["/** Cloudflare: personal API tokens and OAuth. */\nimport { type Config, Effect, Redacted, Schema } from \"effect\";\n\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as DomainName from \"./DomainName.ts\";\nimport * as Client from \"./internal/cloudflare/client.ts\";\nimport type * as Protocol from \"./internal/cloudflare/protocol.ts\";\nimport { resolve } from \"./internal/config.ts\";\nimport type { Fetch } from \"./internal/http.ts\";\nimport * as OAuth from \"./internal/oauth.ts\";\nimport * as Provider from \"./Provider.ts\";\n\n/**\n * Authorization context: `accountId` is `null` when the credential spans several accounts or\n * none yet. Target context: the zone's account and zone id.\n */\nexport const AccountContext = Schema.Struct({\n accountId: Schema.NullOr(Schema.String),\n /** How the token was verified: user tokens span accounts, account tokens are pinned. */\n tokenKind: Schema.optionalKey(Schema.Literals([\"user\", \"account\"])),\n zoneId: Schema.optionalKey(Schema.String),\n});\n\n/** Token-method input: the API token, and the account id for account-owned tokens. */\nexport const TokenFields = Schema.Struct({\n token: Schema.RedactedFromValue(Schema.String),\n accountId: Schema.optionalKey(Schema.String),\n});\nexport type AccountContext = typeof AccountContext.Type;\n\nexport interface Options {\n /** Omit to offer tokens only. */\n readonly oauth?: OAuthOptions;\n readonly fetch?: Fetch;\n readonly baseUrl?: string;\n}\n\ninterface OAuthOptionsBase {\n readonly clientId: string | Config.Config<string>;\n /** Scope ids assigned to the OAuth client. Default: `zone.read`, `dns.read`, `dns.write`, `offline_access`. */\n readonly scopes?: ReadonlyArray<string>;\n /**\n * Origin for consent, token exchange, and revocation, which Cloudflare serves at\n * `/oauth2/auth`, `/oauth2/token`, and `/oauth2/revoke` beneath it. Default\n * `https://dash.cloudflare.com`; a stage points it at an emulator that mounts the same three\n * paths. It stays separate from `baseUrl` because in production these are different hosts:\n * `dash.cloudflare.com` for OAuth, `api.cloudflare.com/client/v4` for the REST API.\n */\n readonly issuer?: string;\n /**\n * Where the server reaches the OAuth endpoints when that differs from where the browser does,\n * for example an API in a container reaching an emulator through `host.docker.internal`.\n * Defaults to `issuer`.\n */\n readonly serverOrigin?: string;\n /**\n * Permit `http:` for these endpoints when they are not loopback, such as an emulator reached\n * through `host.docker.internal`. Development only: the client secret, the code, and the tokens\n * cross the network in the clear, and a name resolves wherever the machine has been told to.\n * Loopback needs no flag.\n */\n readonly allowPlaintext?: boolean;\n}\n\n/** A public OAuth client uses PKCE and has no client secret. */\nexport interface PublicOAuthOptions extends OAuthOptionsBase {\n readonly clientAuth: \"none\";\n readonly clientSecret?: never;\n}\n\n/** A confidential OAuth client authenticates with a required client secret. */\nexport interface ConfidentialOAuthOptions extends OAuthOptionsBase {\n /** Defaults to `client_secret_basic`. */\n readonly clientAuth?: \"client_secret_basic\" | \"client_secret_post\";\n readonly clientSecret: Redacted.Redacted<string> | Config.Config<Redacted.Redacted<string>>;\n}\n\nexport type OAuthOptions = PublicOAuthOptions | ConfidentialOAuthOptions;\n\nconst defaultIssuer = \"https://dash.cloudflare.com\";\n\n/**\n * The three OAuth endpoints Cloudflare serves under one origin. Consent is the browser's, exchange\n * and revocation are the server's, so a stage whose server reaches the same emulator by another\n * name gives two origins and keeps the paths.\n */\nconst endpointsOf = (origins: {\n readonly browser: string;\n readonly server: string;\n}): OAuth.Server => {\n const browser = origins.browser.replace(/\\/$/, \"\");\n const server = origins.server.replace(/\\/$/, \"\");\n return {\n authorization_endpoint: `${browser}/oauth2/auth`,\n issuer: browser,\n revocation_endpoint: `${server}/oauth2/revoke`,\n token_endpoint: `${server}/oauth2/token`,\n };\n};\n\nexport const server: OAuth.Server = endpointsOf({\n browser: defaultIssuer,\n server: defaultIssuer,\n});\n\nconst defaultScopes = [\"zone.read\", \"dns.read\", \"dns.write\", \"offline_access\"];\nconst capabilities = [\"dns:read\", \"dns:write\"] as const;\n\n/** OAuth credentials pack both tokens into `secret`; token credentials are the token itself. */\nconst Secret = Schema.fromJsonString(\n Schema.Struct({ accessToken: Schema.String, refreshToken: Schema.NullOr(Schema.String) }),\n);\n\nconst parseSecret = (secret: Redacted.Redacted<string>) => {\n const raw = Redacted.value(secret);\n const parsed = raw.startsWith(\"{\") ? Schema.decodeUnknownOption(Secret)(raw) : undefined;\n return parsed !== undefined && parsed._tag === \"Some\"\n ? parsed.value\n : { accessToken: raw, refreshToken: null };\n};\n\nconst packSecret = (tokens: OAuth.Tokens) =>\n Redacted.make(\n Schema.encodeSync(Secret)({\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n }),\n );\n\nexport const provider = (options: Options = {}): Provider.Definition<AccountContext> => {\n const fetch = options.fetch ?? globalThis.fetch;\n const baseUrl = (options.baseUrl ?? \"https://api.cloudflare.com/client/v4\").replace(/\\/$/, \"\");\n const client = (token: string): Client.Options => ({ token, fetch, baseUrl });\n\n /**\n * The one account every zone the credential sees belongs to, with the name Cloudflare gives it.\n * `null` when the credential spans several accounts or reaches no zone at all: neither names a\n * single account. `accountId` narrows the listing to a token already pinned to an account.\n */\n const discoverAccount = (token: string, accountId?: string) =>\n Client.listZones(client(token), accountId).pipe(\n Effect.map((zones) => {\n const accounts = new Map(zones.map((zone) => [zone.account.id, zone.account]));\n const [only] = accounts.values();\n return accounts.size === 1 && only !== undefined ? only : null;\n }),\n );\n\n const targetOf = (zone: Protocol.Zone): Provider.Target => ({\n zone: zone.name,\n context: { accountId: zone.account.id, zoneId: zone.id } satisfies AccountContext,\n label: `${zone.name} (${zone.account.name})`,\n nameservers: zone.name_servers ?? [],\n });\n\n const oauthClient = (oauth: NonNullable<Options[\"oauth\"]>) =>\n Effect.gen(function* () {\n const clientId = yield* resolve(oauth.clientId, \"oauth.clientId\");\n if (oauth.clientAuth === \"none\") {\n return { clientId, clientSecret: null, clientAuth: \"none\" } satisfies OAuth.Client;\n }\n const clientSecret = yield* resolve(oauth.clientSecret, \"oauth.clientSecret\");\n return {\n clientId,\n clientSecret,\n clientAuth: oauth.clientAuth ?? \"client_secret_basic\",\n } satisfies OAuth.Client;\n });\n\n const issued = (\n tokens: OAuth.Tokens,\n ): Effect.Effect<Provider.IssuedCredential, Errors.DomainKitError> =>\n discoverAccount(tokens.accessToken).pipe(\n Effect.map((account) => ({\n secret: packSecret(tokens),\n context: { accountId: account?.id ?? null } satisfies AccountContext,\n label: account?.name ?? null,\n expiresAt: tokens.expiresAt,\n })),\n );\n\n const oauth = options.oauth === undefined ? undefined : oauthAuth(options.oauth);\n\n function oauthAuth(settings: NonNullable<Options[\"oauth\"]>): Provider.OAuthAuth {\n const scopes = settings.scopes ?? defaultScopes;\n const browser = settings.issuer ?? defaultIssuer;\n const endpoints = endpointsOf({ browser, server: settings.serverOrigin ?? browser });\n const insecure =\n settings.allowPlaintext === undefined ? {} : { allowPlaintext: settings.allowPlaintext };\n return {\n label: \"Sign in with Cloudflare\",\n scopes,\n start: (input) =>\n oauthClient(settings).pipe(\n Effect.map(({ clientId }) => ({\n authorizationUrl: OAuth.authorizationUrl({\n server: endpoints,\n clientId,\n scopes,\n state: input.state,\n callbackUrl: input.callbackUrl,\n codeChallenge: input.codeChallenge,\n }),\n })),\n ),\n complete: (input) =>\n oauthClient(settings).pipe(\n Effect.flatMap((oauthClientValue) =>\n OAuth.exchangeCode({\n provider: Client.provider,\n server: endpoints,\n client: oauthClientValue,\n code: input.code,\n state: input.params.state ?? \"\",\n callbackUrl: input.callbackUrl,\n codeVerifier: input.codeVerifier,\n fetch,\n ...insecure,\n }),\n ),\n Effect.flatMap(issued),\n ),\n refresh: (credential) =>\n Effect.gen(function* () {\n const { refreshToken } = parseSecret(credential.secret);\n if (refreshToken === null) {\n return yield* Errors.fail(\n new Reason.Unauthenticated({\n message: \"Cloudflare credential has no refresh token\",\n }),\n );\n }\n const tokens = yield* OAuth.refresh({\n provider: Client.provider,\n server: endpoints,\n client: yield* oauthClient(settings),\n refreshToken,\n fetch,\n ...insecure,\n });\n return {\n secret: packSecret(tokens),\n context: credential.context,\n expiresAt: tokens.expiresAt,\n };\n }),\n revoke: (credential) =>\n Effect.gen(function* () {\n const { accessToken } = parseSecret(credential.secret);\n yield* OAuth.revoke({\n provider: Client.provider,\n server: endpoints,\n client: yield* oauthClient(settings),\n token: accessToken,\n fetch,\n ...insecure,\n });\n }),\n };\n }\n\n return Provider.make<AccountContext>({\n id: \"cloudflare\",\n name: \"Cloudflare\",\n nameservers: [\"ns.cloudflare.com\"],\n context: AccountContext,\n contextVersion: \"cloudflare.v1\",\n auth: {\n token: Provider.tokenAuth({\n label: \"API token\",\n docsUrl: \"https://developers.cloudflare.com/fundamentals/api/get-started/create-token/\",\n requiredCapabilities: capabilities,\n fields: TokenFields,\n authenticate: ({ token, accountId }) =>\n Effect.gen(function* () {\n const raw = Redacted.value(token);\n if (accountId !== undefined) {\n const expiresAt = yield* Client.verifyToken(\n client(raw),\n `/accounts/${encodeURIComponent(accountId)}/tokens/verify`,\n );\n // The account name only ever comes from a zone, so a pinned token that reaches no\n // zone connects unnamed rather than failing over a label.\n const pinned = yield* discoverAccount(raw, accountId).pipe(\n Effect.orElseSucceed(() => null),\n );\n return {\n secret: token,\n context: { accountId, tokenKind: \"account\" } satisfies AccountContext,\n label: pinned?.name ?? null,\n expiresAt,\n };\n }\n const discovered = yield* discoverAccount(raw);\n const verified = yield* Client.tokenExpiry(client(raw), discovered?.id ?? null);\n return {\n secret: token,\n context: {\n accountId: discovered?.id ?? null,\n tokenKind: verified.kind,\n } satisfies AccountContext,\n label: discovered?.name ?? null,\n expiresAt: verified.expiresAt,\n };\n }),\n }),\n ...(oauth === undefined ? {} : { oauth }),\n },\n session: (credential) => {\n const { accessToken } = parseSecret(credential.secret);\n const clientOptions = client(accessToken);\n const context = Schema.decodeUnknownOption(AccountContext)(credential.context);\n const accountId =\n context._tag === \"Some\" ? (context.value.accountId ?? undefined) : undefined;\n const listTargets = () =>\n Client.listZones(clientOptions, accountId).pipe(Effect.map((zones) => zones.map(targetOf)));\n return {\n capabilities: () => Effect.succeed(capabilities),\n listTargets,\n resolveTarget: (domain) =>\n Effect.gen(function* () {\n const name = yield* DomainName.decode(domain);\n const targets = yield* listTargets();\n return Provider.resolveAmong(name, targets);\n }),\n dns: (target) => {\n const zone = Schema.decodeUnknownOption(AccountContext)(target.context);\n const zoneId = zone._tag === \"Some\" ? zone.value.zoneId : undefined;\n if (zoneId === undefined) {\n const failure = Errors.fail(\n new Reason.Unsupported({\n provider: \"cloudflare\",\n operation: \"dns\",\n message: \"Cloudflare target has no zone id; only zone targets host records\",\n }),\n );\n return {\n list: () => failure,\n create: () => failure,\n get: () => failure,\n delete: () => failure,\n };\n }\n return Client.dns(clientOptions, zoneId);\n },\n };\n },\n });\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Cloudflare.mjs","names":["Client.listZones","Configuration.endpoint","Configuration.scopes","Configuration.requireValue","Configuration.resolve","Configuration.nonempty","Configuration.secret","OAuth.authorizationUrl","OAuth.exchangeCode","Client.provider","Errors.fail","Reason.Unauthenticated","OAuth.refresh","OAuth.revoke","Provider.make","Provider.tokenAuth","Client.verifyToken","Client.tokenExpiry","DomainName.decode","Provider.resolveAmong","Reason.Unsupported","Client.dns"],"sources":["../src/Cloudflare.ts"],"sourcesContent":["/** Cloudflare: personal API tokens and OAuth. */\nimport { type Config, Effect, Redacted, Schema } from \"effect\";\n\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as DomainName from \"./DomainName.ts\";\nimport * as Client from \"./internal/cloudflare/client.ts\";\nimport type * as Protocol from \"./internal/cloudflare/protocol.ts\";\nimport * as Configuration from \"./internal/config.ts\";\nimport type { Fetch } from \"./internal/http.ts\";\nimport * as OAuth from \"./internal/oauth.ts\";\nimport * as Provider from \"./Provider.ts\";\n\n/**\n * Authorization context: `accountId` is `null` when the credential spans several accounts or\n * none yet. Target context: the zone's account and zone id.\n */\nexport const AccountContext = Schema.Struct({\n accountId: Schema.NullOr(Schema.String),\n /** How the token was verified: user tokens span accounts, account tokens are pinned. */\n tokenKind: Schema.optionalKey(Schema.Literals([\"user\", \"account\"])),\n zoneId: Schema.optionalKey(Schema.String),\n});\n\n/** Token-method input: the API token, and the account id for account-owned tokens. */\nexport const TokenFields = Schema.Struct({\n token: Schema.RedactedFromValue(Schema.String),\n accountId: Schema.optionalKey(Schema.String),\n});\nexport type AccountContext = typeof AccountContext.Type;\n\nexport interface Options {\n /** Omit to offer tokens only. */\n readonly oauth?: OAuthOptions;\n readonly fetch?: Fetch;\n readonly baseUrl?: string;\n}\n\ninterface OAuthOptionsBase {\n readonly clientId: string | Config.Config<string>;\n /** Scope ids assigned to the OAuth client. Default: `zone.read`, `dns.read`, `dns.write`, `offline_access`. */\n readonly scopes?: ReadonlyArray<string>;\n /**\n * Origin for consent, token exchange, and revocation, which Cloudflare serves at\n * `/oauth2/auth`, `/oauth2/token`, and `/oauth2/revoke` beneath it. Default\n * `https://dash.cloudflare.com`; a stage points it at an emulator that mounts the same three\n * paths. It stays separate from `baseUrl` because in production these are different hosts:\n * `dash.cloudflare.com` for OAuth, `api.cloudflare.com/client/v4` for the REST API.\n */\n readonly issuer?: string;\n /**\n * Where the server reaches the OAuth endpoints when that differs from where the browser does,\n * for example an API in a container reaching an emulator through `host.docker.internal`.\n * Defaults to `issuer`.\n */\n readonly serverOrigin?: string;\n /**\n * Permit `http:` for these endpoints when they are not loopback, such as an emulator reached\n * through `host.docker.internal`. Development only: the client secret, the code, and the tokens\n * cross the network in the clear, and a name resolves wherever the machine has been told to.\n * Loopback needs no flag.\n */\n readonly allowPlaintext?: boolean;\n}\n\n/** A public OAuth client uses PKCE and has no client secret. */\nexport interface PublicOAuthOptions extends OAuthOptionsBase {\n readonly clientAuth: \"none\";\n readonly clientSecret?: never;\n}\n\n/** A confidential OAuth client authenticates with a required client secret. */\nexport interface ConfidentialOAuthOptions extends OAuthOptionsBase {\n /** Defaults to `client_secret_basic`. */\n readonly clientAuth?: \"client_secret_basic\" | \"client_secret_post\";\n readonly clientSecret: Redacted.Redacted<string> | Config.Config<Redacted.Redacted<string>>;\n}\n\nexport type OAuthOptions = PublicOAuthOptions | ConfidentialOAuthOptions;\n\nconst defaultIssuer = \"https://dash.cloudflare.com\";\n\n/**\n * The three OAuth endpoints Cloudflare serves under one origin. Consent is the browser's, exchange\n * and revocation are the server's, so a stage whose server reaches the same emulator by another\n * name gives two origins and keeps the paths.\n */\nconst endpointsOf = (origins: {\n readonly browser: string;\n readonly server: string;\n}): OAuth.Server => {\n const browser = origins.browser.replace(/\\/$/, \"\");\n const server = origins.server.replace(/\\/$/, \"\");\n return {\n authorization_endpoint: `${browser}/oauth2/auth`,\n issuer: browser,\n revocation_endpoint: `${server}/oauth2/revoke`,\n token_endpoint: `${server}/oauth2/token`,\n };\n};\n\nexport const server: OAuth.Server = endpointsOf({\n browser: defaultIssuer,\n server: defaultIssuer,\n});\n\nconst defaultScopes = [\"zone.read\", \"dns.read\", \"dns.write\", \"offline_access\"];\nconst capabilities = [\"dns:read\", \"dns:write\"] as const;\n\n/** OAuth credentials pack both tokens into `secret`; token credentials are the token itself. */\nconst Secret = Schema.fromJsonString(\n Schema.Struct({ accessToken: Schema.String, refreshToken: Schema.NullOr(Schema.String) }),\n);\n\nconst parseSecret = (secret: Redacted.Redacted<string>) => {\n const raw = Redacted.value(secret);\n const parsed = raw.startsWith(\"{\") ? Schema.decodeUnknownOption(Secret)(raw) : undefined;\n return parsed !== undefined && parsed._tag === \"Some\"\n ? parsed.value\n : { accessToken: raw, refreshToken: null };\n};\n\nconst packSecret = (tokens: OAuth.Tokens) =>\n Redacted.make(\n Schema.encodeSync(Secret)({\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n }),\n );\n\nexport const provider = (options: Options = {}): Provider.Definition<AccountContext> => {\n const fetch = options.fetch ?? globalThis.fetch;\n const baseUrl = options.baseUrl ?? \"https://api.cloudflare.com/client/v4\";\n const client = (token: string): Client.Options => ({\n token,\n fetch,\n baseUrl: baseUrl.replace(/\\/$/, \"\"),\n });\n\n /**\n * The one account every zone the credential sees belongs to, with the name Cloudflare gives it.\n * `null` when the credential spans several accounts or reaches no zone at all: neither names a\n * single account. `accountId` narrows the listing to a token already pinned to an account.\n */\n const discoverAccount = (token: string, accountId?: string) =>\n Client.listZones(client(token), accountId).pipe(\n Effect.map((zones) => {\n const accounts = new Map(zones.map((zone) => [zone.account.id, zone.account]));\n const [only] = accounts.values();\n return accounts.size === 1 && only !== undefined ? only : null;\n }),\n );\n\n const targetOf = (zone: Protocol.Zone): Provider.Target => ({\n zone: zone.name,\n context: { accountId: zone.account.id, zoneId: zone.id } satisfies AccountContext,\n label: `${zone.name} (${zone.account.name})`,\n nameservers: zone.name_servers ?? [],\n });\n\n const oauthClient = (oauth: NonNullable<Options[\"oauth\"]>) =>\n Effect.gen(function* () {\n yield* Configuration.endpoint(\n options.baseUrl ?? \"https://api.cloudflare.com/client/v4\",\n \"baseUrl\",\n );\n yield* Configuration.endpoint(oauth.issuer ?? defaultIssuer, \"oauth.issuer\");\n yield* Configuration.endpoint(\n oauth.serverOrigin ?? oauth.issuer ?? defaultIssuer,\n \"oauth.serverOrigin\",\n );\n yield* Configuration.scopes(oauth.scopes ?? defaultScopes, \"oauth.scopes\");\n yield* Configuration.requireValue(\n oauth.clientAuth,\n (v) =>\n v === undefined ||\n v === \"none\" ||\n v === \"client_secret_basic\" ||\n v === \"client_secret_post\",\n \"oauth.clientAuth\",\n \"oauth.clientAuth is unsupported\",\n );\n const clientId = yield* Configuration.resolve(oauth.clientId, \"oauth.clientId\").pipe(\n Effect.flatMap((v) => Configuration.nonempty(v, \"oauth.clientId\")),\n );\n if (oauth.clientAuth === \"none\") {\n return { clientId, clientSecret: null, clientAuth: \"none\" } satisfies OAuth.Client;\n }\n const clientSecret = yield* Configuration.resolve(\n oauth.clientSecret,\n \"oauth.clientSecret\",\n ).pipe(Effect.flatMap((v) => Configuration.secret(v, \"oauth.clientSecret\")));\n return {\n clientId,\n clientSecret,\n clientAuth: oauth.clientAuth ?? \"client_secret_basic\",\n } satisfies OAuth.Client;\n });\n\n const issued = (\n tokens: OAuth.Tokens,\n ): Effect.Effect<Provider.IssuedCredential, Errors.DomainKitError> =>\n discoverAccount(tokens.accessToken).pipe(\n Effect.map((account) => ({\n secret: packSecret(tokens),\n context: { accountId: account?.id ?? null } satisfies AccountContext,\n label: account?.name ?? null,\n expiresAt: tokens.expiresAt,\n })),\n );\n\n const oauth = options.oauth === undefined ? undefined : oauthAuth(options.oauth);\n\n function oauthAuth(settings: NonNullable<Options[\"oauth\"]>): Provider.OAuthAuth {\n const scopes = settings.scopes ?? defaultScopes;\n const browser = settings.issuer ?? defaultIssuer;\n const endpoints = () => endpointsOf({ browser, server: settings.serverOrigin ?? browser });\n const insecure =\n settings.allowPlaintext === undefined ? {} : { allowPlaintext: settings.allowPlaintext };\n return {\n label: \"Sign in with Cloudflare\",\n registration: { clientAuth: settings.clientAuth ?? \"client_secret_basic\", pkce: \"S256\" },\n scopes,\n start: (input) =>\n oauthClient(settings).pipe(\n Effect.map(({ clientId }) => ({\n authorizationUrl: OAuth.authorizationUrl({\n server: endpoints(),\n clientId,\n scopes,\n state: input.state,\n callbackUrl: input.callbackUrl,\n codeChallenge: input.codeChallenge,\n }),\n })),\n ),\n complete: (input) =>\n oauthClient(settings).pipe(\n Effect.flatMap((oauthClientValue) =>\n OAuth.exchangeCode({\n provider: Client.provider,\n server: endpoints(),\n client: oauthClientValue,\n code: input.code,\n state: input.params.state ?? \"\",\n callbackUrl: input.callbackUrl,\n codeVerifier: input.codeVerifier,\n fetch,\n ...insecure,\n }),\n ),\n Effect.flatMap(issued),\n ),\n refresh: (credential) =>\n Effect.gen(function* () {\n const { refreshToken } = parseSecret(credential.secret);\n if (refreshToken === null) {\n return yield* Errors.fail(\n new Reason.Unauthenticated({\n message: \"Cloudflare credential has no refresh token\",\n }),\n );\n }\n const tokens = yield* OAuth.refresh({\n provider: Client.provider,\n client: yield* oauthClient(settings),\n server: endpoints(),\n refreshToken,\n fetch,\n ...insecure,\n });\n return {\n secret: packSecret(tokens),\n context: credential.context,\n expiresAt: tokens.expiresAt,\n };\n }),\n revoke: (credential) =>\n Effect.gen(function* () {\n const { accessToken } = parseSecret(credential.secret);\n yield* OAuth.revoke({\n provider: Client.provider,\n client: yield* oauthClient(settings),\n server: endpoints(),\n token: accessToken,\n fetch,\n ...insecure,\n });\n }),\n };\n }\n\n return Provider.make<AccountContext>({\n id: \"cloudflare\",\n name: \"Cloudflare\",\n nameservers: [\"ns.cloudflare.com\"],\n context: AccountContext,\n contextVersion: \"cloudflare.v1\",\n auth: {\n token: Provider.tokenAuth({\n label: \"API token\",\n docsUrl: \"https://developers.cloudflare.com/fundamentals/api/get-started/create-token/\",\n requiredCapabilities: capabilities,\n fields: TokenFields,\n authenticate: ({ token, accountId }) =>\n Effect.gen(function* () {\n const raw = Redacted.value(token);\n if (accountId !== undefined) {\n const expiresAt = yield* Client.verifyToken(\n client(raw),\n `/accounts/${encodeURIComponent(accountId)}/tokens/verify`,\n );\n // The account name only ever comes from a zone, so a pinned token that reaches no\n // zone connects unnamed rather than failing over a label.\n const pinned = yield* discoverAccount(raw, accountId).pipe(\n Effect.orElseSucceed(() => null),\n );\n return {\n secret: token,\n context: { accountId, tokenKind: \"account\" } satisfies AccountContext,\n label: pinned?.name ?? null,\n expiresAt,\n };\n }\n const discovered = yield* discoverAccount(raw);\n const verified = yield* Client.tokenExpiry(client(raw), discovered?.id ?? null);\n return {\n secret: token,\n context: {\n accountId: discovered?.id ?? null,\n tokenKind: verified.kind,\n } satisfies AccountContext,\n label: discovered?.name ?? null,\n expiresAt: verified.expiresAt,\n };\n }),\n }),\n ...(oauth === undefined ? {} : { oauth }),\n },\n session: (credential) => {\n const { accessToken } = parseSecret(credential.secret);\n const clientOptions = client(accessToken);\n const context = Schema.decodeUnknownOption(AccountContext)(credential.context);\n const accountId =\n context._tag === \"Some\" ? (context.value.accountId ?? undefined) : undefined;\n const listTargets = () =>\n Client.listZones(clientOptions, accountId).pipe(Effect.map((zones) => zones.map(targetOf)));\n return {\n capabilities: () => Effect.succeed(capabilities),\n listTargets,\n resolveTarget: (domain) =>\n Effect.gen(function* () {\n const name = yield* DomainName.decode(domain);\n const targets = yield* listTargets();\n return Provider.resolveAmong(name, targets);\n }),\n dns: (target) => {\n const zone = Schema.decodeUnknownOption(AccountContext)(target.context);\n const zoneId = zone._tag === \"Some\" ? zone.value.zoneId : undefined;\n if (zoneId === undefined) {\n const failure = Errors.fail(\n new Reason.Unsupported({\n provider: \"cloudflare\",\n operation: \"dns\",\n message: \"Cloudflare target has no zone id; only zone targets host records\",\n }),\n );\n return {\n list: () => failure,\n create: () => failure,\n get: () => failure,\n delete: () => failure,\n };\n }\n return Client.dns(clientOptions, zoneId);\n },\n };\n },\n });\n};\n"],"mappings":"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"}
|
package/dist/Connect.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { DomainKitError, decode as decode$1, fail } from "./internal/error.mjs";
|
|
|
5
5
|
import { Service as Service$1 } from "./Custody.mjs";
|
|
6
6
|
import { fresh } from "./internal/ids.mjs";
|
|
7
7
|
import { pkce } from "./internal/oauth.mjs";
|
|
8
|
+
import { refuse } from "./internal/callback.mjs";
|
|
8
9
|
import { Service as Service$2 } from "./Principal.mjs";
|
|
9
10
|
import { Resolution, decodeContext, describeMethods, encodeContext } from "./Provider.mjs";
|
|
10
11
|
import { Service as Service$3 } from "./Providers.mjs";
|
|
@@ -18,6 +19,7 @@ import { Context, Data, DateTime, Duration, Effect, Layer, Option, Redacted, Sch
|
|
|
18
19
|
* call. Reuse of an existing connection for a new domain is `attach`.
|
|
19
20
|
*/
|
|
20
21
|
var Connect_exports = /* @__PURE__ */ __exportAll({
|
|
22
|
+
Completion: () => Completion,
|
|
21
23
|
Discovery: () => Discovery,
|
|
22
24
|
Method: () => Method,
|
|
23
25
|
Policy: () => Policy,
|
|
@@ -26,6 +28,7 @@ var Connect_exports = /* @__PURE__ */ __exportAll({
|
|
|
26
28
|
Started: () => Started,
|
|
27
29
|
attach: () => attach,
|
|
28
30
|
complete: () => complete,
|
|
31
|
+
completeOutcome: () => completeOutcome,
|
|
29
32
|
defaults: () => defaults,
|
|
30
33
|
detach: () => detach,
|
|
31
34
|
disconnect: () => disconnect,
|
|
@@ -48,6 +51,7 @@ const Method = {
|
|
|
48
51
|
integration: (options = {}) => MethodEnum.Integration(options)
|
|
49
52
|
};
|
|
50
53
|
const Started = Data.taggedEnum();
|
|
54
|
+
const Completion = Data.taggedEnum();
|
|
51
55
|
const Selection = Data.taggedEnum();
|
|
52
56
|
const Discovery = Data.taggedEnum();
|
|
53
57
|
var Service = class extends Context.Service()("@domainkit/Connect") {};
|
|
@@ -462,7 +466,10 @@ const make = Effect.gen(function* () {
|
|
|
462
466
|
});
|
|
463
467
|
}
|
|
464
468
|
});
|
|
465
|
-
const
|
|
469
|
+
const completeOutcome = (input) => storage.withLock(`continuation:${input.continuationId}`, Effect.gen(function* () {
|
|
470
|
+
const continuation = yield* storage.continuations.get(input.continuationId);
|
|
471
|
+
const principal = yield* Service$2;
|
|
472
|
+
if (continuation.ownerId !== principal.ownerId || continuation.actorId !== principal.actorId || input.provider !== void 0 && continuation.provider !== input.provider) return yield* refuse;
|
|
466
473
|
const callback = yield* Effect.try({
|
|
467
474
|
try: () => new URL(input.callbackUrl),
|
|
468
475
|
catch: () => new DomainKitError({ reason: new InvalidInput({
|
|
@@ -471,13 +478,19 @@ const make = Effect.gen(function* () {
|
|
|
471
478
|
}) })
|
|
472
479
|
});
|
|
473
480
|
const params = Object.fromEntries(callback.searchParams);
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
481
|
+
const failed = (message) => Completion.Failed({
|
|
482
|
+
error: new DomainKitError({ reason: new Unauthenticated({ message }) }),
|
|
483
|
+
recovery: "restart"
|
|
484
|
+
});
|
|
485
|
+
if (params.state !== input.continuationId) return failed("Callback state does not match the continuation");
|
|
486
|
+
if (params.error !== void 0) {
|
|
487
|
+
const outcome = failed(`Provider returned ${params.error}`);
|
|
488
|
+
return params.error === "access_denied" ? Completion.Cancelled({ error: outcome.error }) : outcome;
|
|
489
|
+
}
|
|
477
490
|
const code = params.code;
|
|
478
|
-
if (code === void 0) return
|
|
479
|
-
|
|
480
|
-
|
|
491
|
+
if (code === void 0) return failed("Callback has no code");
|
|
492
|
+
let persisting = false;
|
|
493
|
+
return yield* Effect.gen(function* () {
|
|
481
494
|
const payload = yield* decode$1(Payload, continuation.payload, "continuation");
|
|
482
495
|
const definition = yield* providers.get(continuation.provider);
|
|
483
496
|
const issued = payload.method === "oauth" ? yield* Effect.gen(function* () {
|
|
@@ -498,6 +511,7 @@ const make = Effect.gen(function* () {
|
|
|
498
511
|
params
|
|
499
512
|
});
|
|
500
513
|
});
|
|
514
|
+
persisting = true;
|
|
501
515
|
if (payload.connectionId !== null) {
|
|
502
516
|
const held = yield* storage.connections.get(payload.connectionId);
|
|
503
517
|
const existing = yield* storage.authorizations.get(held.authorizationId);
|
|
@@ -528,8 +542,12 @@ const make = Effect.gen(function* () {
|
|
|
528
542
|
});
|
|
529
543
|
yield* storage.continuations.consume(input.continuationId).pipe(Effect.ignore);
|
|
530
544
|
return started(connection, attached);
|
|
531
|
-
}))
|
|
532
|
-
|
|
545
|
+
}).pipe(Effect.map((result) => Completion.Connected({ started: result })), Effect.catch((error) => Effect.succeed(Completion.Failed({
|
|
546
|
+
error,
|
|
547
|
+
recovery: !persisting && error.reason._tag === "Unauthenticated" ? "restart" : "inspect"
|
|
548
|
+
}))));
|
|
549
|
+
}));
|
|
550
|
+
const complete = (input) => completeOutcome(input).pipe(Effect.flatMap((outcome) => outcome._tag === "Connected" ? Effect.succeed(outcome.started) : Effect.fail(outcome.error)));
|
|
533
551
|
const attach = (input) => Effect.gen(function* () {
|
|
534
552
|
const connection = yield* storage.connections.get(input.connectionId);
|
|
535
553
|
const authorization = yield* storage.authorizations.get(connection.authorizationId);
|
|
@@ -580,6 +598,7 @@ const make = Effect.gen(function* () {
|
|
|
580
598
|
start,
|
|
581
599
|
reconnect,
|
|
582
600
|
complete,
|
|
601
|
+
completeOutcome,
|
|
583
602
|
attach,
|
|
584
603
|
detach: (attachmentId) => storage.attachments.remove(attachmentId),
|
|
585
604
|
disconnect,
|
|
@@ -595,12 +614,13 @@ const discover = accessor((service) => service.discover);
|
|
|
595
614
|
const start = accessor((service) => service.start);
|
|
596
615
|
const reconnect = accessor((service) => service.reconnect);
|
|
597
616
|
const complete = accessor((service) => service.complete);
|
|
617
|
+
const completeOutcome = accessor((service) => service.completeOutcome);
|
|
598
618
|
const attach = accessor((service) => service.attach);
|
|
599
619
|
const detach = accessor((service) => service.detach);
|
|
600
620
|
const disconnect = accessor((service) => service.disconnect);
|
|
601
621
|
const refresh = accessor((service) => service.refresh);
|
|
602
622
|
const session = accessor((service) => service.session);
|
|
603
623
|
//#endregion
|
|
604
|
-
export { Connect_exports, Discovery, Method, Policy, Selection, Service, Started, attach, complete, defaults, detach, disconnect, discover, inspect, layer, make, reconnect, refresh, session, start, zones };
|
|
624
|
+
export { Completion, Connect_exports, Discovery, Method, Policy, Selection, Service, Started, attach, complete, completeOutcome, defaults, detach, disconnect, discover, inspect, layer, make, reconnect, refresh, session, start, zones };
|
|
605
625
|
|
|
606
626
|
//# sourceMappingURL=Connect.mjs.map
|