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.
- package/dist/Cloudflare.mjs +35 -12
- package/dist/Cloudflare.mjs.map +1 -1
- package/dist/Connect.mjs +159 -35
- package/dist/Connect.mjs.map +1 -1
- package/dist/Provider.mjs +12 -13
- package/dist/Provider.mjs.map +1 -1
- package/dist/Server.mjs +139 -22
- package/dist/Server.mjs.map +1 -1
- package/dist/Storage.mjs +2 -0
- package/dist/Storage.mjs.map +1 -1
- package/dist/Testing.mjs +8 -7
- package/dist/Testing.mjs.map +1 -1
- package/dist/Transport.mjs +25 -2
- package/dist/Transport.mjs.map +1 -1
- package/dist/internal/conformance/storage.mjs +16 -0
- package/dist/internal/conformance/storage.mjs.map +1 -1
- package/dist/internal/oauth.mjs +35 -7
- package/dist/internal/oauth.mjs.map +1 -1
- package/dist/internal/storage-memory.mjs +1 -0
- package/dist/internal/storage-memory.mjs.map +1 -1
- package/dist/package.mjs +1 -1
- package/dist/types/Cloudflare.d.ts +21 -0
- package/dist/types/Connect.d.ts +65 -0
- package/dist/types/Provider.d.ts +5 -0
- package/dist/types/Server.d.ts +954 -26
- package/dist/types/Storage.d.ts +4 -0
- package/dist/types/Testing.d.ts +12 -3
- package/dist/types/Transport.d.ts +16 -1
- package/dist/types/internal/oauth.d.ts +4 -0
- package/package.json +1 -1
package/dist/Cloudflare.mjs
CHANGED
|
@@ -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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
};
|
package/dist/Cloudflare.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Cloudflare.mjs","names":["Client.listZones","OAuth.authorizationUrl","OAuth.exchangeCode","Client.provider","Errors.fail","Reason.Unauthenticated","OAuth.refresh","OAuth.revoke","Provider.make","Provider.tokenAuth","Client.verifyToken","Client.tokenExpiry","DomainName.decode","Provider.resolveAmong","Reason.Unsupported","Client.dns"],"sources":["../src/Cloudflare.ts"],"sourcesContent":["/** Cloudflare: personal API tokens and OAuth. */\nimport { type Config, Effect, Redacted, Schema } from \"effect\";\n\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as DomainName from \"./DomainName.ts\";\nimport * as Client from \"./internal/cloudflare/client.ts\";\nimport type * as Protocol from \"./internal/cloudflare/protocol.ts\";\nimport { resolve } from \"./internal/config.ts\";\nimport type { Fetch } from \"./internal/http.ts\";\nimport * as OAuth from \"./internal/oauth.ts\";\nimport * as Provider from \"./Provider.ts\";\n\n/**\n * Authorization context: `accountId` is `null` when the credential spans several accounts or\n * none yet. Target context: the zone's account and zone id.\n */\nexport const AccountContext = Schema.Struct({\n accountId: Schema.NullOr(Schema.String),\n /** How the token was verified: user tokens span accounts, account tokens are pinned. */\n tokenKind: Schema.optionalKey(Schema.Literals([\"user\", \"account\"])),\n zoneId: Schema.optionalKey(Schema.String),\n});\n\n/** Token-method input: the API token, and the account id for account-owned tokens. */\nexport const TokenFields = Schema.Struct({\n token: Schema.RedactedFromValue(Schema.String),\n accountId: Schema.optionalKey(Schema.String),\n});\nexport type AccountContext = typeof AccountContext.Type;\n\nexport interface Options {\n /** Omit to offer tokens only. */\n readonly oauth?: {\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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|