domainkit 0.10.0 → 0.12.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.
@@ -30,12 +30,26 @@ const TokenFields = Schema.Struct({
30
30
  token: Schema.RedactedFromValue(Schema.String),
31
31
  accountId: Schema.optionalKey(Schema.String)
32
32
  });
33
- const server = {
34
- authorization_endpoint: "https://dash.cloudflare.com/oauth2/auth",
35
- issuer: "https://dash.cloudflare.com",
36
- revocation_endpoint: "https://dash.cloudflare.com/oauth2/revoke",
37
- token_endpoint: "https://dash.cloudflare.com/oauth2/token"
33
+ const defaultIssuer = "https://dash.cloudflare.com";
34
+ /**
35
+ * The three OAuth endpoints Cloudflare serves under one origin. Consent is the browser's, exchange
36
+ * and revocation are the server's, so a stage whose server reaches the same emulator by another
37
+ * name gives two origins and keeps the paths.
38
+ */
39
+ const endpointsOf = (origins) => {
40
+ const browser = origins.browser.replace(/\/$/, "");
41
+ const server = origins.server.replace(/\/$/, "");
42
+ return {
43
+ authorization_endpoint: `${browser}/oauth2/auth`,
44
+ issuer: browser,
45
+ revocation_endpoint: `${server}/oauth2/revoke`,
46
+ token_endpoint: `${server}/oauth2/token`
47
+ };
38
48
  };
49
+ const server = endpointsOf({
50
+ browser: defaultIssuer,
51
+ server: defaultIssuer
52
+ });
39
53
  const defaultScopes = [
40
54
  "zone:read",
41
55
  "dns_records:edit",
@@ -96,11 +110,17 @@ const provider = (options = {}) => {
96
110
  const oauth = options.oauth === void 0 ? void 0 : oauthAuth(options.oauth);
97
111
  function oauthAuth(settings) {
98
112
  const scopes = settings.scopes ?? defaultScopes;
113
+ const browser = settings.issuer ?? defaultIssuer;
114
+ const endpoints = endpointsOf({
115
+ browser,
116
+ server: settings.serverOrigin ?? browser
117
+ });
118
+ const insecure = settings.allowPlaintext === void 0 ? {} : { allowPlaintext: settings.allowPlaintext };
99
119
  return {
100
120
  label: "Sign in with Cloudflare",
101
121
  scopes,
102
122
  start: (input) => oauthClient(settings).pipe(Effect.map(({ clientId }) => ({ authorizationUrl: authorizationUrl({
103
- server,
123
+ server: endpoints,
104
124
  clientId,
105
125
  scopes,
106
126
  state: input.state,
@@ -109,23 +129,25 @@ const provider = (options = {}) => {
109
129
  }) }))),
110
130
  complete: (input) => oauthClient(settings).pipe(Effect.flatMap((oauthClientValue) => exchangeCode({
111
131
  provider: provider$1,
112
- server,
132
+ server: endpoints,
113
133
  client: oauthClientValue,
114
134
  code: input.code,
115
135
  state: input.params.state ?? "",
116
136
  callbackUrl: input.callbackUrl,
117
137
  codeVerifier: input.codeVerifier,
118
- fetch
138
+ fetch,
139
+ ...insecure
119
140
  })), Effect.flatMap(issued)),
120
141
  refresh: (credential) => Effect.gen(function* () {
121
142
  const { refreshToken } = parseSecret(credential.secret);
122
143
  if (refreshToken === null) return yield* fail(new Unauthenticated({ message: "Cloudflare credential has no refresh token" }));
123
144
  const tokens = yield* refresh({
124
145
  provider: provider$1,
125
- server,
146
+ server: endpoints,
126
147
  client: yield* oauthClient(settings),
127
148
  refreshToken,
128
- fetch
149
+ fetch,
150
+ ...insecure
129
151
  });
130
152
  return {
131
153
  secret: packSecret(tokens),
@@ -137,10 +159,11 @@ const provider = (options = {}) => {
137
159
  const { accessToken } = parseSecret(credential.secret);
138
160
  yield* revoke({
139
161
  provider: provider$1,
140
- server,
162
+ server: endpoints,
141
163
  client: yield* oauthClient(settings),
142
164
  token: accessToken,
143
- fetch
165
+ fetch,
166
+ ...insecure
144
167
  });
145
168
  })
146
169
  };
@@ -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?: {\n readonly clientId: string | Config.Config<string>;\n readonly clientSecret: Redacted.Redacted<string> | Config.Config<Redacted.Redacted<string>>;\n /** Scope ids assigned to the OAuth client. Default: `zone:read`, `dns_records:edit`, `offline_access`. */\n readonly scopes?: ReadonlyArray<string>;\n /** Default `client_secret_basic`. */\n readonly clientAuth?: OAuth.ClientAuth;\n };\n readonly fetch?: Fetch;\n readonly baseUrl?: string;\n}\n\nexport const server: OAuth.Server = {\n authorization_endpoint: \"https://dash.cloudflare.com/oauth2/auth\",\n issuer: \"https://dash.cloudflare.com\",\n revocation_endpoint: \"https://dash.cloudflare.com/oauth2/revoke\",\n token_endpoint: \"https://dash.cloudflare.com/oauth2/token\",\n};\n\nconst defaultScopes = [\"zone:read\", \"dns_records:edit\", \"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 const discoverAccount = (token: string) =>\n Client.listZones(client(token)).pipe(\n Effect.map((zones) => {\n const accounts = new Set(zones.map((zone) => zone.account.id));\n const [only] = accounts;\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 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((accountId) => ({\n secret: packSecret(tokens),\n context: { accountId } satisfies AccountContext,\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 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,\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,\n client: oauthClientValue,\n code: input.code,\n state: input.params.state ?? \"\",\n callbackUrl: input.callbackUrl,\n codeVerifier: input.codeVerifier,\n fetch,\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,\n client: yield* oauthClient(settings),\n refreshToken,\n fetch,\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,\n client: yield* oauthClient(settings),\n token: accessToken,\n fetch,\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 return {\n secret: token,\n context: { accountId, tokenKind: \"account\" } satisfies AccountContext,\n expiresAt,\n };\n }\n const discovered = yield* discoverAccount(raw);\n const verified = yield* Client.tokenExpiry(client(raw), discovered);\n return {\n secret: token,\n context: { accountId: discovered, tokenKind: verified.kind } satisfies AccountContext,\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","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?: {\n readonly clientId: string | Config.Config<string>;\n readonly clientSecret: Redacted.Redacted<string> | Config.Config<Redacted.Redacted<string>>;\n /** Scope ids assigned to the OAuth client. Default: `zone:read`, `dns_records:edit`, `offline_access`. */\n readonly scopes?: ReadonlyArray<string>;\n /** Default `client_secret_basic`. */\n readonly clientAuth?: OAuth.ClientAuth;\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 readonly fetch?: Fetch;\n readonly baseUrl?: string;\n}\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_records:edit\", \"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 const discoverAccount = (token: string) =>\n Client.listZones(client(token)).pipe(\n Effect.map((zones) => {\n const accounts = new Set(zones.map((zone) => zone.account.id));\n const [only] = accounts;\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 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((accountId) => ({\n secret: packSecret(tokens),\n context: { accountId } satisfies AccountContext,\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 return {\n secret: token,\n context: { accountId, tokenKind: \"account\" } satisfies AccountContext,\n expiresAt,\n };\n }\n const discovered = yield* discoverAccount(raw);\n const verified = yield* Client.tokenExpiry(client(raw), discovered);\n return {\n secret: token,\n context: { accountId: discovered, tokenKind: verified.kind } satisfies AccountContext,\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
@@ -33,9 +33,11 @@ var Connect_exports = /* @__PURE__ */ __exportAll({
33
33
  inspect: () => inspect,
34
34
  layer: () => layer,
35
35
  make: () => make,
36
+ reconnect: () => reconnect,
36
37
  refresh: () => refresh,
37
38
  session: () => session,
38
- start: () => start
39
+ start: () => start,
40
+ zones: () => zones
39
41
  });
40
42
  const MethodEnum = Data.taggedEnum();
41
43
  /** `token` accepts a lone string (`{ token }`) or a record; every value is wrapped in `Redacted`. */
@@ -59,7 +61,9 @@ const Payload = Schema.Struct({
59
61
  method: Schema.Literals(["oauth", "integration"]),
60
62
  codeVerifier: Schema.NullOr(Schema.String),
61
63
  callbackUrl: Schema.String,
62
- domain: Schema.NullOr(Schema.String)
64
+ domain: Schema.NullOr(Schema.String),
65
+ /** Set when the flow is re-crediting a connection rather than making one. */
66
+ connectionId: Schema.NullOr(Schema.String)
63
67
  });
64
68
  const Target = Schema.Struct({
65
69
  zone: Schema.String,
@@ -167,6 +171,32 @@ const make = Effect.gen(function* () {
167
171
  session
168
172
  };
169
173
  });
174
+ /**
175
+ * Put a fresh credential behind an authorization the owner already has. The row keeps its id, so
176
+ * every connection over it, and every domain on those connections, stays exactly where it is.
177
+ */
178
+ const replaceWith = (input) => Effect.gen(function* () {
179
+ const principal = yield* Service$2;
180
+ const held = yield* input.definition.session(input.issued).capabilities();
181
+ const missing = input.requiredCapabilities.filter((capability) => !held.includes(capability));
182
+ if (missing.length > 0) return yield* fail(new Forbidden({ message: `${input.definition.name} credential lacks ${missing.join(", ")}` }));
183
+ const authorization = new Authorization({
184
+ id: input.authorizationId,
185
+ ownerId: principal.ownerId,
186
+ provider: input.definition.id,
187
+ method: input.method,
188
+ capabilities: held,
189
+ context: yield* encodeContext(input.definition, input.issued.context),
190
+ revocation: "active",
191
+ createdBy: principal.actorId,
192
+ createdAt: yield* DateTime.now
193
+ });
194
+ yield* storage.authorizations.upsert({
195
+ authorization,
196
+ credential: yield* seal(input.issued),
197
+ expectedId: input.authorizationId
198
+ });
199
+ });
170
200
  const attachWith = (input) => Effect.gen(function* () {
171
201
  const domain = yield* decode(input.domain);
172
202
  const existing = yield* storage.attachments.byDomain(domain);
@@ -185,6 +215,7 @@ const make = Effect.gen(function* () {
185
215
  connectionId: input.connection.id,
186
216
  domain,
187
217
  zone: resolution.target.zone,
218
+ label: resolution.target.label,
188
219
  target: {
189
220
  ...resolution.target,
190
221
  context: yield* encodeContext(input.definition, resolution.target.context)
@@ -251,6 +282,35 @@ const make = Effect.gen(function* () {
251
282
  const [host] = hosts;
252
283
  return hosts.length === 1 && host !== void 0 ? { provider: host.id } : null;
253
284
  };
285
+ const zones = (options = {}) => Effect.gen(function* () {
286
+ const reachable = [];
287
+ const connections = [];
288
+ const filter = options.provider === void 0 ? void 0 : { provider: options.provider };
289
+ for (const connection of yield* storage.connections.list(filter)) {
290
+ const authorization = yield* storage.authorizations.get(connection.authorizationId);
291
+ const targets = yield* sessionFor(connection).pipe(Effect.flatMap((session) => session.listTargets()), Effect.map(Option.some), Effect.catchIf((error) => error.reason._tag === "Reconnect" || error.reason._tag === "Unauthenticated" || error.reason._tag === "Forbidden", () => Effect.succeed(Option.none())));
292
+ connections.push({
293
+ connectionId: connection.id,
294
+ provider: authorization.provider,
295
+ status: Option.isNone(targets) ? "reconnect" : "connected"
296
+ });
297
+ if (Option.isNone(targets)) continue;
298
+ for (const target of targets.value) reachable.push({
299
+ connectionId: connection.id,
300
+ provider: authorization.provider,
301
+ target
302
+ });
303
+ }
304
+ return {
305
+ zones: reachable.sort((left, right) => left.target.zone.localeCompare(right.target.zone) || left.connectionId.localeCompare(right.connectionId)),
306
+ connections,
307
+ providers: providers.list().map((definition) => ({
308
+ id: definition.id,
309
+ name: definition.name,
310
+ methods: describeMethods(definition)
311
+ }))
312
+ };
313
+ });
254
314
  const inspect = (input) => Effect.gen(function* () {
255
315
  const domain = yield* decode(input);
256
316
  const policy = yield* Policy;
@@ -258,6 +318,7 @@ const make = Effect.gen(function* () {
258
318
  const connection = attachment === null ? null : yield* storage.connections.get(attachment.connectionId);
259
319
  const authorization = connection === null ? null : yield* storage.authorizations.get(connection.authorizationId);
260
320
  const latest = attachment === null ? Option.none() : yield* storage.attempts.latest(attachment.id, "provisioning");
321
+ const attached = connection === null ? [] : yield* storage.attachments.list(connection.id);
261
322
  const reusable = [];
262
323
  for (const candidate of yield* storage.connections.list()) {
263
324
  if (candidate.id === connection?.id) continue;
@@ -285,6 +346,7 @@ const make = Effect.gen(function* () {
285
346
  revocation: authorization.revocation
286
347
  },
287
348
  lastReceiptId: Option.isSome(latest) ? latest.value.receipt?.id ?? null : null,
349
+ connectionDomains: attached.length,
288
350
  reusable,
289
351
  providers: providers.list().map((definition) => ({
290
352
  id: definition.id,
@@ -295,8 +357,6 @@ const make = Effect.gen(function* () {
295
357
  });
296
358
  const start = (input) => Effect.gen(function* () {
297
359
  yield* recover;
298
- const principal = yield* Service$2;
299
- const policy = yield* Policy;
300
360
  const definition = yield* providers.get(input.provider);
301
361
  if (input.domain !== void 0) yield* decode(input.domain);
302
362
  switch (input.method._tag) {
@@ -319,39 +379,83 @@ const make = Effect.gen(function* () {
319
379
  return started(connection, attached);
320
380
  }
321
381
  case "OAuth":
322
- case "Integration": {
323
- const interactive = input.method._tag === "OAuth" ? definition.auth.oauth : definition.auth.integration;
324
- if (interactive === void 0) return yield* invalid(`${definition.name} does not offer ${input.method._tag.toLowerCase()} connections`, "method");
325
- if (input.callbackUrl === void 0) return yield* invalid("Interactive connections need a callbackUrl", "callbackUrl");
326
- const continuationId = yield* fresh("cont");
327
- const pkce$1 = input.method._tag === "OAuth" ? yield* pkce() : null;
328
- const redirect = input.method._tag === "OAuth" ? yield* interactive.start({
329
- state: continuationId,
330
- callbackUrl: input.callbackUrl,
331
- codeChallenge: pkce$1?.codeChallenge ?? ""
332
- }) : yield* interactive.start({
333
- state: continuationId,
334
- callbackUrl: input.callbackUrl
382
+ case "Integration": return yield* redirectFor({
383
+ definition,
384
+ method: input.method,
385
+ callbackUrl: input.callbackUrl,
386
+ domain: input.domain ?? null,
387
+ connectionId: null
388
+ });
389
+ }
390
+ });
391
+ /** The redirect an interactive method answers with, and the continuation that finishes it. */
392
+ const redirectFor = (input) => Effect.gen(function* () {
393
+ const principal = yield* Service$2;
394
+ const policy = yield* Policy;
395
+ const interactive = input.method._tag === "OAuth" ? input.definition.auth.oauth : input.definition.auth.integration;
396
+ if (interactive === void 0) return yield* invalid(`${input.definition.name} does not offer ${input.method._tag.toLowerCase()} connections`, "method");
397
+ if (input.callbackUrl === void 0) return yield* invalid("Interactive connections need a callbackUrl", "callbackUrl");
398
+ const callbackUrl = input.callbackUrl;
399
+ const continuationId = yield* fresh("cont");
400
+ const pkce$1 = input.method._tag === "OAuth" ? yield* pkce() : null;
401
+ const redirect = input.method._tag === "OAuth" ? yield* interactive.start({
402
+ state: continuationId,
403
+ callbackUrl,
404
+ codeChallenge: pkce$1?.codeChallenge ?? ""
405
+ }) : yield* interactive.start({
406
+ state: continuationId,
407
+ callbackUrl
408
+ });
409
+ yield* storage.continuations.put(new Continuation({
410
+ id: continuationId,
411
+ ownerId: principal.ownerId,
412
+ actorId: principal.actorId,
413
+ provider: input.definition.id,
414
+ payload: Schema.encodeSync(Payload)({
415
+ method: input.method._tag === "OAuth" ? "oauth" : "integration",
416
+ codeVerifier: pkce$1?.codeVerifier ?? null,
417
+ callbackUrl,
418
+ domain: input.domain,
419
+ connectionId: input.connectionId
420
+ }),
421
+ returnTo: input.method.returnTo ?? null,
422
+ expiresAt: DateTime.addDuration(yield* DateTime.now, Duration.millis(policy.continuationTtlMs))
423
+ }));
424
+ return Started.Redirect({
425
+ authorizationUrl: redirect.authorizationUrl,
426
+ continuationId
427
+ });
428
+ });
429
+ const reconnect = (input) => Effect.gen(function* () {
430
+ yield* recover;
431
+ const connection = yield* storage.connections.get(input.connectionId);
432
+ const existing = yield* storage.authorizations.get(connection.authorizationId);
433
+ const definition = yield* providers.get(existing.provider);
434
+ switch (input.method._tag) {
435
+ case "Token": {
436
+ const token = definition.auth.token;
437
+ if (token === void 0) return yield* invalid(`${definition.name} does not accept tokens`, "method");
438
+ const issued = yield* token.authenticate(input.method.values);
439
+ yield* replaceWith({
440
+ definition,
441
+ authorizationId: existing.id,
442
+ method: "token",
443
+ issued,
444
+ requiredCapabilities: token.requiredCapabilities
335
445
  });
336
- yield* storage.continuations.put(new Continuation({
337
- id: continuationId,
338
- ownerId: principal.ownerId,
339
- actorId: principal.actorId,
340
- provider: definition.id,
341
- payload: Schema.encodeSync(Payload)({
342
- method: input.method._tag === "OAuth" ? "oauth" : "integration",
343
- codeVerifier: pkce$1?.codeVerifier ?? null,
344
- callbackUrl: input.callbackUrl,
345
- domain: input.domain ?? null
346
- }),
347
- returnTo: input.method.returnTo ?? null,
348
- expiresAt: DateTime.addDuration(yield* DateTime.now, Duration.millis(policy.continuationTtlMs))
349
- }));
350
- return Started.Redirect({
351
- authorizationUrl: redirect.authorizationUrl,
352
- continuationId
446
+ return Started.Connected({
447
+ connection,
448
+ attachment: null
353
449
  });
354
450
  }
451
+ case "OAuth":
452
+ case "Integration": return yield* redirectFor({
453
+ definition,
454
+ method: input.method,
455
+ callbackUrl: input.callbackUrl,
456
+ domain: null,
457
+ connectionId: connection.id
458
+ });
355
459
  }
356
460
  });
357
461
  const complete = (input) => Effect.gen(function* () {
@@ -390,6 +494,22 @@ const make = Effect.gen(function* () {
390
494
  params
391
495
  });
392
496
  });
497
+ if (payload.connectionId !== null) {
498
+ const held = yield* storage.connections.get(payload.connectionId);
499
+ const existing = yield* storage.authorizations.get(held.authorizationId);
500
+ yield* replaceWith({
501
+ definition,
502
+ authorizationId: existing.id,
503
+ method: payload.method,
504
+ issued,
505
+ requiredCapabilities: []
506
+ });
507
+ yield* storage.continuations.consume(input.continuationId).pipe(Effect.ignore);
508
+ return Started.Connected({
509
+ connection: held,
510
+ attachment: null
511
+ });
512
+ }
393
513
  const { connection, session } = yield* connectWith({
394
514
  definition,
395
515
  method: payload.method,
@@ -451,8 +571,10 @@ const make = Effect.gen(function* () {
451
571
  });
452
572
  return {
453
573
  inspect,
574
+ zones,
454
575
  discover,
455
576
  start,
577
+ reconnect,
456
578
  complete,
457
579
  attach,
458
580
  detach: (attachmentId) => storage.attachments.remove(attachmentId),
@@ -464,8 +586,10 @@ const make = Effect.gen(function* () {
464
586
  const layer = Layer.effect(Service)(make);
465
587
  const accessor = (pick) => (...args) => Effect.flatMap(Service, (service) => pick(service)(...args));
466
588
  const inspect = accessor((service) => service.inspect);
589
+ const zones = accessor((service) => service.zones);
467
590
  const discover = accessor((service) => service.discover);
468
591
  const start = accessor((service) => service.start);
592
+ const reconnect = accessor((service) => service.reconnect);
469
593
  const complete = accessor((service) => service.complete);
470
594
  const attach = accessor((service) => service.attach);
471
595
  const detach = accessor((service) => service.detach);
@@ -473,6 +597,6 @@ const disconnect = accessor((service) => service.disconnect);
473
597
  const refresh = accessor((service) => service.refresh);
474
598
  const session = accessor((service) => service.session);
475
599
  //#endregion
476
- export { Connect_exports, Discovery, Method, Policy, Selection, Service, Started, attach, complete, defaults, detach, disconnect, discover, inspect, layer, make, refresh, session, start };
600
+ export { Connect_exports, Discovery, Method, Policy, Selection, Service, Started, attach, complete, defaults, detach, disconnect, discover, inspect, layer, make, reconnect, refresh, session, start, zones };
477
601
 
478
602
  //# sourceMappingURL=Connect.mjs.map