domainkit 0.20.1 → 0.22.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 CHANGED
@@ -19,6 +19,8 @@ Node.js 24.10 or newer and Effect 4 are required.
19
19
 
20
20
  ## Plan, approve, apply
21
21
 
22
+ ![A missing DNS record is Create, an exact match is Noop, and incompatible state is Conflict, which blocks apply.](https://domain-kit.dev/launch/semantics-1920x1080.png)
23
+
22
24
  ```ts
23
25
  import { Effect, Match } from "effect";
24
26
  import { DnsRecord, DomainKit, Principal, Provision, Verify } from "domainkit";
@@ -181,11 +183,50 @@ the group.
181
183
  `/callback/:provider` is the one route the provider drives the browser to, so `Identity` has to
182
184
  recognise a credential the browser sends on a top-level navigation.
183
185
 
186
+ A live, authenticated cancellation or completion failure redirects to the saved `returnTo` (or
187
+ `defaultReturnTo`) with `domainkit=cancelled|restart|inspect` and `domainkit-provider=<id>`.
188
+ `Connected` redirects after clearing all recovery parameters. `CallbackRecovery` from `domainkit/client`
189
+ reads or clears this display hint, including optional `domainkit-connection=<connectionId>` for
190
+ a reconnect; `Connect.useRecovery()` from `@domainkit/react` reads and
191
+ dismisses it in the browser. A `callback` policy can override the destination with a same-origin
192
+ string or return `undefined` to keep the default. Unknown, expired, spent, or mismatched flows
193
+ remain errors. See [callback destinations](https://domain-kit.dev/docs/reference/server#callback-destinations).
194
+
184
195
  `authorize` is optional and runs after `principal` on every request with the route's name, one of
185
196
  `Server.EndpointName`. Fail it with reason `Forbidden` for the 403 a UI expects. Omit it and every
186
197
  authenticated principal reaches every route, which is right when your own middleware already gates
187
198
  the mount.
188
199
 
200
+ `Server.registrationSettings({ providers, callbackBaseUrl })` derives secret-free registration
201
+ requirements from the same provider definitions and public callback base used by your server.
202
+ It needs no credentials, identity, storage, or network: OAuth settings describe callback URL,
203
+ client authentication, S256 PKCE, and configured scopes; Integration settings describe callback URL
204
+ and code exchange authentication. Token-only definitions return no settings. Custom interactive
205
+ methods without optional registration metadata fail `Unsupported`. Settings describe requirements,
206
+ not console readiness or granted permissions. See the
207
+ [server reference](https://domain-kit.dev/docs/reference/server#registration-settings).
208
+
209
+ Interactive starts reject empty credentials and malformed setup fields before redirecting or
210
+ storing a continuation, with `InvalidInput` naming the field without rejected values. Vercel resolves
211
+ its required credentials at start and again at completion for rotation. Configured callback bases
212
+ must be HTTP(S) URLs without credentials, query, or fragment; emulator paths remain supported.
213
+
214
+ `Server.Options.callback` optionally selects a same-origin destination for sanitized `Connected`,
215
+ `Cancelled`, or `Failed` outcomes. Return `Effect.succeed(undefined)` to retain the default response,
216
+ or an Effect of a destination string. The policy receives provider, validated `returnTo`, and
217
+ `connectionId` on success, optional `connectionId` on cancelled or failed reconnects, and
218
+ `recovery: "restart" | "inspect"` on failure; it receives no request,
219
+ state, code, provider text, secret, or full storage row. Hosts close over acquired services.
220
+
221
+ Only live flows matching the authenticated owner, actor, and provider are classified inside the
222
+ completion lock. Unknown, expired, spent, and mismatched callbacks invoke no policy, including read
223
+ races. Only `access_denied` is cancellation; other provider errors are failure. Cancellation and
224
+ failure retain expiry and retry behavior. `Connected` means durable account access, not DNS readiness;
225
+ zone selection can still be required. Partial persistence or a policy failure after connection needs
226
+ inspection, not a rollback assumption. `Connect.completeOutcome` exposes the shared classified result
227
+ to Effect hosts; `Connect.complete` retains its result/error contract. See the
228
+ [server reference](https://domain-kit.dev/docs/reference/server#callback-destinations) for compiled usage.
229
+
189
230
  After an interactive connection completes, the callback redirects to the `returnTo` the flow was
190
231
  started with, or to `defaultReturnTo`. The destination is resolved against the callback's own base
191
232
  and must land on its origin, so neither the provider nor a crafted `returnTo` can steer the customer
@@ -0,0 +1,48 @@
1
+ import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
2
+ import { Schema } from "effect";
3
+ //#region src/CallbackRecovery.ts
4
+ /** Browser-safe recovery hints for returning from an authenticated provider callback. */
5
+ var CallbackRecovery_exports = /* @__PURE__ */ __exportAll({
6
+ Model: () => Model,
7
+ read: () => read,
8
+ write: () => write
9
+ });
10
+ const Model = Schema.Struct({
11
+ kind: Schema.Literals([
12
+ "cancelled",
13
+ "restart",
14
+ "inspect"
15
+ ]),
16
+ provider: Schema.String.check(Schema.isMinLength(1)),
17
+ connectionId: Schema.optionalKey(Schema.String.check(Schema.isMinLength(1)))
18
+ });
19
+ /** Reads a complete recovery hint; ignores missing, duplicate, or invalid metadata. */
20
+ const read = (url) => {
21
+ const kinds = url.searchParams.getAll("domainkit");
22
+ const providers = url.searchParams.getAll("domainkit-provider");
23
+ const connections = url.searchParams.getAll("domainkit-connection");
24
+ if (kinds.length !== 1 || providers.length !== 1 || connections.length > 1) return null;
25
+ const outcome = {
26
+ kind: kinds[0],
27
+ provider: providers[0],
28
+ ...connections.length === 0 ? {} : { connectionId: connections[0] }
29
+ };
30
+ return Schema.is(Model)(outcome) ? outcome : null;
31
+ };
32
+ /** Clones the URL and replaces or clears all recovery hints, preserving other parameters and hash. */
33
+ const write = (url, outcome) => {
34
+ const result = new URL(url);
35
+ result.searchParams.delete("domainkit");
36
+ result.searchParams.delete("domainkit-provider");
37
+ result.searchParams.delete("domainkit-connection");
38
+ if (outcome !== null) {
39
+ result.searchParams.set("domainkit", outcome.kind);
40
+ result.searchParams.set("domainkit-provider", outcome.provider);
41
+ if (outcome.connectionId !== void 0) result.searchParams.set("domainkit-connection", outcome.connectionId);
42
+ }
43
+ return result;
44
+ };
45
+ //#endregion
46
+ export { CallbackRecovery_exports, Model, read, write };
47
+
48
+ //# sourceMappingURL=CallbackRecovery.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CallbackRecovery.mjs","names":[],"sources":["../src/CallbackRecovery.ts"],"sourcesContent":["/** Browser-safe recovery hints for returning from an authenticated provider callback. */\nimport { Schema } from \"effect\";\n\nexport const Model = Schema.Struct({\n kind: Schema.Literals([\"cancelled\", \"restart\", \"inspect\"]),\n provider: Schema.String.check(Schema.isMinLength(1)),\n connectionId: Schema.optionalKey(Schema.String.check(Schema.isMinLength(1))),\n});\nexport type Model = typeof Model.Type;\n\n/** Reads a complete recovery hint; ignores missing, duplicate, or invalid metadata. */\nexport const read = (url: URL): Model | null => {\n const kinds = url.searchParams.getAll(\"domainkit\");\n const providers = url.searchParams.getAll(\"domainkit-provider\");\n const connections = url.searchParams.getAll(\"domainkit-connection\");\n if (kinds.length !== 1 || providers.length !== 1 || connections.length > 1) return null;\n const outcome = {\n kind: kinds[0],\n provider: providers[0],\n ...(connections.length === 0 ? {} : { connectionId: connections[0] }),\n };\n return Schema.is(Model)(outcome) ? outcome : null;\n};\n\n/** Clones the URL and replaces or clears all recovery hints, preserving other parameters and hash. */\nexport const write = (url: URL, outcome: Model | null): URL => {\n const result = new URL(url);\n result.searchParams.delete(\"domainkit\");\n result.searchParams.delete(\"domainkit-provider\");\n result.searchParams.delete(\"domainkit-connection\");\n if (outcome !== null) {\n result.searchParams.set(\"domainkit\", outcome.kind);\n result.searchParams.set(\"domainkit-provider\", outcome.provider);\n if (outcome.connectionId !== undefined)\n result.searchParams.set(\"domainkit-connection\", outcome.connectionId);\n }\n return result;\n};\n"],"mappings":";;;;;;;;;AAGA,MAAa,QAAQ,OAAO,OAAO;CACjC,MAAM,OAAO,SAAS;EAAC;EAAa;EAAW;CAAS,CAAC;CACzD,UAAU,OAAO,OAAO,MAAM,OAAO,YAAY,CAAC,CAAC;CACnD,cAAc,OAAO,YAAY,OAAO,OAAO,MAAM,OAAO,YAAY,CAAC,CAAC,CAAC;AAC7E,CAAC;;AAID,MAAa,QAAQ,QAA2B;CAC9C,MAAM,QAAQ,IAAI,aAAa,OAAO,WAAW;CACjD,MAAM,YAAY,IAAI,aAAa,OAAO,oBAAoB;CAC9D,MAAM,cAAc,IAAI,aAAa,OAAO,sBAAsB;CAClE,IAAI,MAAM,WAAW,KAAK,UAAU,WAAW,KAAK,YAAY,SAAS,GAAG,OAAO;CACnF,MAAM,UAAU;EACd,MAAM,MAAM;EACZ,UAAU,UAAU;EACpB,GAAI,YAAY,WAAW,IAAI,CAAC,IAAI,EAAE,cAAc,YAAY,GAAG;CACrE;CACA,OAAO,OAAO,GAAG,KAAK,CAAC,CAAC,OAAO,IAAI,UAAU;AAC/C;;AAGA,MAAa,SAAS,KAAU,YAA+B;CAC7D,MAAM,SAAS,IAAI,IAAI,GAAG;CAC1B,OAAO,aAAa,OAAO,WAAW;CACtC,OAAO,aAAa,OAAO,oBAAoB;CAC/C,OAAO,aAAa,OAAO,sBAAsB;CACjD,IAAI,YAAY,MAAM;EACpB,OAAO,aAAa,IAAI,aAAa,QAAQ,IAAI;EACjD,OAAO,aAAa,IAAI,sBAAsB,QAAQ,QAAQ;EAC9D,IAAI,QAAQ,iBAAiB,KAAA,GAC3B,OAAO,aAAa,IAAI,wBAAwB,QAAQ,YAAY;CACxE;CACA,OAAO;AACT"}
@@ -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 = (options.baseUrl ?? "https://api.cloudflare.com/client/v4").replace(/\/$/, "");
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
- const clientId = yield* resolve(oauth.clientId, "oauth.clientId");
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
@@ -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":";;;;;;;;;;;;;;;;;;;;;AAiBA,MAAa,iBAAiB,OAAO,OAAO;CAC1C,WAAW,OAAO,OAAO,OAAO,MAAM;;CAEtC,WAAW,OAAO,YAAY,OAAO,SAAS,CAAC,QAAQ,SAAS,CAAC,CAAC;CAClE,QAAQ,OAAO,YAAY,OAAO,MAAM;AAC1C,CAAC;;AAGD,MAAa,cAAc,OAAO,OAAO;CACvC,OAAO,OAAO,kBAAkB,OAAO,MAAM;CAC7C,WAAW,OAAO,YAAY,OAAO,MAAM;AAC7C,CAAC;AAoDD,MAAM,gBAAgB;;;;;;AAOtB,MAAM,eAAe,YAGD;CAClB,MAAM,UAAU,QAAQ,QAAQ,QAAQ,OAAO,EAAE;CACjD,MAAM,SAAS,QAAQ,OAAO,QAAQ,OAAO,EAAE;CAC/C,OAAO;EACL,wBAAwB,GAAG,QAAQ;EACnC,QAAQ;EACR,qBAAqB,GAAG,OAAO;EAC/B,gBAAgB,GAAG,OAAO;CAC5B;AACF;AAEA,MAAa,SAAuB,YAAY;CAC9C,SAAS;CACT,QAAQ;AACV,CAAC;AAED,MAAM,gBAAgB;CAAC;CAAa;CAAY;CAAa;AAAgB;AAC7E,MAAM,eAAe,CAAC,YAAY,WAAW;;AAG7C,MAAM,SAAS,OAAO,eACpB,OAAO,OAAO;CAAE,aAAa,OAAO;CAAQ,cAAc,OAAO,OAAO,OAAO,MAAM;AAAE,CAAC,CAC1F;AAEA,MAAM,eAAe,WAAsC;CACzD,MAAM,MAAM,SAAS,MAAM,MAAM;CACjC,MAAM,SAAS,IAAI,WAAW,GAAG,IAAI,OAAO,oBAAoB,MAAM,CAAC,CAAC,GAAG,IAAI,KAAA;CAC/E,OAAO,WAAW,KAAA,KAAa,OAAO,SAAS,SAC3C,OAAO,QACP;EAAE,aAAa;EAAK,cAAc;CAAK;AAC7C;AAEA,MAAM,cAAc,WAClB,SAAS,KACP,OAAO,WAAW,MAAM,CAAC,CAAC;CACxB,aAAa,OAAO;CACpB,cAAc,OAAO;AACvB,CAAC,CACH;AAEF,MAAa,YAAY,UAAmB,CAAC,MAA2C;CACtF,MAAM,QAAQ,QAAQ,SAAS,WAAW;CAC1C,MAAM,WAAW,QAAQ,WAAW,uCAAA,CAAwC,QAAQ,OAAO,EAAE;CAC7F,MAAM,UAAU,WAAmC;EAAE;EAAO;EAAO;CAAQ;;;;;;CAO3E,MAAM,mBAAmB,OAAe,cACtCA,UAAiB,OAAO,KAAK,GAAG,SAAS,CAAC,CAAC,KACzC,OAAO,KAAK,UAAU;EACpB,MAAM,WAAW,IAAI,IAAI,MAAM,KAAK,SAAS,CAAC,KAAK,QAAQ,IAAI,KAAK,OAAO,CAAC,CAAC;EAC7E,MAAM,CAAC,QAAQ,SAAS,OAAO;EAC/B,OAAO,SAAS,SAAS,KAAK,SAAS,KAAA,IAAY,OAAO;CAC5D,CAAC,CACH;CAEF,MAAM,YAAY,UAA0C;EAC1D,MAAM,KAAK;EACX,SAAS;GAAE,WAAW,KAAK,QAAQ;GAAI,QAAQ,KAAK;EAAG;EACvD,OAAO,GAAG,KAAK,KAAK,IAAI,KAAK,QAAQ,KAAK;EAC1C,aAAa,KAAK,gBAAgB,CAAC;CACrC;CAEA,MAAM,eAAe,UACnB,OAAO,IAAI,aAAa;EACtB,MAAM,WAAW,OAAO,QAAQ,MAAM,UAAU,gBAAgB;EAChE,IAAI,MAAM,eAAe,QACvB,OAAO;GAAE;GAAU,cAAc;GAAM,YAAY;EAAO;EAG5D,OAAO;GACL;GACA,cAAA,OAH0B,QAAQ,MAAM,cAAc,oBAAoB;GAI1E,YAAY,MAAM,cAAc;EAClC;CACF,CAAC;CAEH,MAAM,UACJ,WAEA,gBAAgB,OAAO,WAAW,CAAC,CAAC,KAClC,OAAO,KAAK,aAAa;EACvB,QAAQ,WAAW,MAAM;EACzB,SAAS,EAAE,WAAW,SAAS,MAAM,KAAK;EAC1C,OAAO,SAAS,QAAQ;EACxB,WAAW,OAAO;CACpB,EAAE,CACJ;CAEF,MAAM,QAAQ,QAAQ,UAAU,KAAA,IAAY,KAAA,IAAY,UAAU,QAAQ,KAAK;CAE/E,SAAS,UAAU,UAA6D;EAC9E,MAAM,SAAS,SAAS,UAAU;EAClC,MAAM,UAAU,SAAS,UAAU;EACnC,MAAM,YAAY,YAAY;GAAE;GAAS,QAAQ,SAAS,gBAAgB;EAAQ,CAAC;EACnF,MAAM,WACJ,SAAS,mBAAmB,KAAA,IAAY,CAAC,IAAI,EAAE,gBAAgB,SAAS,eAAe;EACzF,OAAO;GACL,OAAO;GACP;GACA,QAAQ,UACN,YAAY,QAAQ,CAAC,CAAC,KACpB,OAAO,KAAK,EAAE,gBAAgB,EAC5B,kBAAkBC,iBAAuB;IACvC,QAAQ;IACR;IACA;IACA,OAAO,MAAM;IACb,aAAa,MAAM;IACnB,eAAe,MAAM;GACvB,CAAC,EACH,EAAE,CACJ;GACF,WAAW,UACT,YAAY,QAAQ,CAAC,CAAC,KACpB,OAAO,SAAS,qBACdC,aAAmB;IACjB,UAAUC;IACV,QAAQ;IACR,QAAQ;IACR,MAAM,MAAM;IACZ,OAAO,MAAM,OAAO,SAAS;IAC7B,aAAa,MAAM;IACnB,cAAc,MAAM;IACpB;IACA,GAAG;GACL,CAAC,CACH,GACA,OAAO,QAAQ,MAAM,CACvB;GACF,UAAU,eACR,OAAO,IAAI,aAAa;IACtB,MAAM,EAAE,iBAAiB,YAAY,WAAW,MAAM;IACtD,IAAI,iBAAiB,MACnB,OAAO,OAAOC,KACZ,IAAIC,gBAAuB,EACzB,SAAS,6CACX,CAAC,CACH;IAEF,MAAM,SAAS,OAAOC,QAAc;KAClC,UAAUH;KACV,QAAQ;KACR,QAAQ,OAAO,YAAY,QAAQ;KACnC;KACA;KACA,GAAG;IACL,CAAC;IACD,OAAO;KACL,QAAQ,WAAW,MAAM;KACzB,SAAS,WAAW;KACpB,WAAW,OAAO;IACpB;GACF,CAAC;GACH,SAAS,eACP,OAAO,IAAI,aAAa;IACtB,MAAM,EAAE,gBAAgB,YAAY,WAAW,MAAM;IACrD,OAAOI,OAAa;KAClB,UAAUJ;KACV,QAAQ;KACR,QAAQ,OAAO,YAAY,QAAQ;KACnC,OAAO;KACP;KACA,GAAG;IACL,CAAC;GACH,CAAC;EACL;CACF;CAEA,OAAOK,KAA8B;EACnC,IAAI;EACJ,MAAM;EACN,aAAa,CAAC,mBAAmB;EACjC,SAAS;EACT,gBAAgB;EAChB,MAAM;GACJ,OAAOC,UAAmB;IACxB,OAAO;IACP,SAAS;IACT,sBAAsB;IACtB,QAAQ;IACR,eAAe,EAAE,OAAO,gBACtB,OAAO,IAAI,aAAa;KACtB,MAAM,MAAM,SAAS,MAAM,KAAK;KAChC,IAAI,cAAc,KAAA,GAAW;MAC3B,MAAM,YAAY,OAAOC,YACvB,OAAO,GAAG,GACV,aAAa,mBAAmB,SAAS,EAAE,eAC7C;MAGA,MAAM,SAAS,OAAO,gBAAgB,KAAK,SAAS,CAAC,CAAC,KACpD,OAAO,oBAAoB,IAAI,CACjC;MACA,OAAO;OACL,QAAQ;OACR,SAAS;QAAE;QAAW,WAAW;OAAU;OAC3C,OAAO,QAAQ,QAAQ;OACvB;MACF;KACF;KACA,MAAM,aAAa,OAAO,gBAAgB,GAAG;KAC7C,MAAM,WAAW,OAAOC,YAAmB,OAAO,GAAG,GAAG,YAAY,MAAM,IAAI;KAC9E,OAAO;MACL,QAAQ;MACR,SAAS;OACP,WAAW,YAAY,MAAM;OAC7B,WAAW,SAAS;MACtB;MACA,OAAO,YAAY,QAAQ;MAC3B,WAAW,SAAS;KACtB;IACF,CAAC;GACL,CAAC;GACD,GAAI,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM;EACzC;EACA,UAAU,eAAe;GACvB,MAAM,EAAE,gBAAgB,YAAY,WAAW,MAAM;GACrD,MAAM,gBAAgB,OAAO,WAAW;GACxC,MAAM,UAAU,OAAO,oBAAoB,cAAc,CAAC,CAAC,WAAW,OAAO;GAC7E,MAAM,YACJ,QAAQ,SAAS,SAAU,QAAQ,MAAM,aAAa,KAAA,IAAa,KAAA;GACrE,MAAM,oBACJX,UAAiB,eAAe,SAAS,CAAC,CAAC,KAAK,OAAO,KAAK,UAAU,MAAM,IAAI,QAAQ,CAAC,CAAC;GAC5F,OAAO;IACL,oBAAoB,OAAO,QAAQ,YAAY;IAC/C;IACA,gBAAgB,WACd,OAAO,IAAI,aAAa;KACtB,MAAM,OAAO,OAAOY,OAAkB,MAAM;KAC5C,MAAM,UAAU,OAAO,YAAY;KACnC,OAAOC,aAAsB,MAAM,OAAO;IAC5C,CAAC;IACH,MAAM,WAAW;KACf,MAAM,OAAO,OAAO,oBAAoB,cAAc,CAAC,CAAC,OAAO,OAAO;KACtE,MAAM,SAAS,KAAK,SAAS,SAAS,KAAK,MAAM,SAAS,KAAA;KAC1D,IAAI,WAAW,KAAA,GAAW;MACxB,MAAM,UAAUT,KACd,IAAIU,YAAmB;OACrB,UAAU;OACV,WAAW;OACX,SAAS;MACX,CAAC,CACH;MACA,OAAO;OACL,YAAY;OACZ,cAAc;OACd,WAAW;OACX,cAAc;MAChB;KACF;KACA,OAAOC,IAAW,eAAe,MAAM;IACzC;GACF;EACF;CACF,CAAC;AACH"}
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 complete = (input) => Effect.gen(function* () {
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({
@@ -470,15 +477,28 @@ const make = Effect.gen(function* () {
470
477
  field: "callbackUrl"
471
478
  }) })
472
479
  });
480
+ const decoded = yield* decode$1(Payload, continuation.payload, "continuation").pipe(Effect.result);
481
+ const reconnection = decoded._tag === "Success" && decoded.success.connectionId !== null && decoded.success.connectionId.length > 0 ? { connectionId: decoded.success.connectionId } : {};
473
482
  const params = Object.fromEntries(callback.searchParams);
474
- const unauthenticated = (message) => fail(new Unauthenticated({ message }));
475
- if (params.error !== void 0) return yield* unauthenticated(`Provider returned ${params.error}`);
476
- if (params.state !== input.continuationId) return yield* unauthenticated("Callback state does not match the continuation");
483
+ const failed = (message) => Completion.Failed({
484
+ error: new DomainKitError({ reason: new Unauthenticated({ message }) }),
485
+ recovery: "restart",
486
+ ...reconnection
487
+ });
488
+ if (params.state !== input.continuationId) return failed("Callback state does not match the continuation");
489
+ if (params.error !== void 0) {
490
+ const outcome = failed(`Provider returned ${params.error}`);
491
+ return params.error === "access_denied" ? Completion.Cancelled({
492
+ error: outcome.error,
493
+ ...reconnection
494
+ }) : outcome;
495
+ }
477
496
  const code = params.code;
478
- if (code === void 0) return yield* unauthenticated("Callback has no code");
479
- return yield* storage.withLock(`continuation:${input.continuationId}`, Effect.gen(function* () {
480
- const continuation = yield* storage.continuations.get(input.continuationId);
481
- const payload = yield* decode$1(Payload, continuation.payload, "continuation");
497
+ if (code === void 0) return failed("Callback has no code");
498
+ let persisting = false;
499
+ return yield* Effect.gen(function* () {
500
+ if (decoded._tag === "Failure") return yield* Effect.fail(decoded.failure);
501
+ const payload = decoded.success;
482
502
  const definition = yield* providers.get(continuation.provider);
483
503
  const issued = payload.method === "oauth" ? yield* Effect.gen(function* () {
484
504
  const oauth = definition.auth.oauth;
@@ -498,6 +518,7 @@ const make = Effect.gen(function* () {
498
518
  params
499
519
  });
500
520
  });
521
+ persisting = true;
501
522
  if (payload.connectionId !== null) {
502
523
  const held = yield* storage.connections.get(payload.connectionId);
503
524
  const existing = yield* storage.authorizations.get(held.authorizationId);
@@ -528,8 +549,13 @@ const make = Effect.gen(function* () {
528
549
  });
529
550
  yield* storage.continuations.consume(input.continuationId).pipe(Effect.ignore);
530
551
  return started(connection, attached);
531
- }));
532
- });
552
+ }).pipe(Effect.map((result) => Completion.Connected({ started: result })), Effect.catch((error) => Effect.succeed(Completion.Failed({
553
+ error,
554
+ ...reconnection,
555
+ recovery: !persisting && error.reason._tag === "Unauthenticated" ? "restart" : "inspect"
556
+ }))));
557
+ }));
558
+ const complete = (input) => completeOutcome(input).pipe(Effect.flatMap((outcome) => outcome._tag === "Connected" ? Effect.succeed(outcome.started) : Effect.fail(outcome.error)));
533
559
  const attach = (input) => Effect.gen(function* () {
534
560
  const connection = yield* storage.connections.get(input.connectionId);
535
561
  const authorization = yield* storage.authorizations.get(connection.authorizationId);
@@ -580,6 +606,7 @@ const make = Effect.gen(function* () {
580
606
  start,
581
607
  reconnect,
582
608
  complete,
609
+ completeOutcome,
583
610
  attach,
584
611
  detach: (attachmentId) => storage.attachments.remove(attachmentId),
585
612
  disconnect,
@@ -595,12 +622,13 @@ const discover = accessor((service) => service.discover);
595
622
  const start = accessor((service) => service.start);
596
623
  const reconnect = accessor((service) => service.reconnect);
597
624
  const complete = accessor((service) => service.complete);
625
+ const completeOutcome = accessor((service) => service.completeOutcome);
598
626
  const attach = accessor((service) => service.attach);
599
627
  const detach = accessor((service) => service.detach);
600
628
  const disconnect = accessor((service) => service.disconnect);
601
629
  const refresh = accessor((service) => service.refresh);
602
630
  const session = accessor((service) => service.session);
603
631
  //#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 };
632
+ 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
633
 
606
634
  //# sourceMappingURL=Connect.mjs.map