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 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@rc
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
@@ -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":"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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 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({
@@ -471,13 +478,19 @@ const make = Effect.gen(function* () {
471
478
  }) })
472
479
  });
473
480
  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");
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 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);
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