domainkit 0.20.1 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/dist/CallbackRecovery.mjs +48 -0
- package/dist/CallbackRecovery.mjs.map +1 -0
- package/dist/Cloudflare.mjs +19 -10
- package/dist/Cloudflare.mjs.map +1 -1
- package/dist/Connect.mjs +39 -11
- package/dist/Connect.mjs.map +1 -1
- package/dist/Provider.mjs.map +1 -1
- package/dist/Server.mjs +139 -22
- package/dist/Server.mjs.map +1 -1
- package/dist/Vercel.mjs +20 -10
- package/dist/Vercel.mjs.map +1 -1
- package/dist/entry/client.mjs +2 -1
- package/dist/entry/server.mjs +1 -2
- package/dist/index.mjs +3 -2
- package/dist/index.mjs.map +1 -1
- package/dist/internal/callback.mjs +13 -0
- package/dist/internal/callback.mjs.map +1 -0
- package/dist/internal/config.mjs +19 -5
- package/dist/internal/config.mjs.map +1 -1
- package/dist/internal/provider-async.mjs +2 -0
- package/dist/internal/provider-async.mjs.map +1 -1
- package/dist/package.mjs +1 -1
- package/dist/types/CallbackRecovery.d.ts +12 -0
- package/dist/types/Connect.d.ts +150 -0
- package/dist/types/Provider.d.ts +9 -0
- package/dist/types/Server.d.ts +48 -0
- package/dist/types/entry/client.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/internal/callback.d.ts +4 -0
- package/dist/types/internal/config.d.ts +8 -1
- package/dist/types/internal/provider-async.d.ts +2 -0
- package/package.json +14 -10
package/README.md
CHANGED
|
@@ -19,6 +19,8 @@ Node.js 24.10 or newer and Effect 4 are required.
|
|
|
19
19
|
|
|
20
20
|
## Plan, approve, apply
|
|
21
21
|
|
|
22
|
+

|
|
23
|
+
|
|
22
24
|
```ts
|
|
23
25
|
import { Effect, Match } from "effect";
|
|
24
26
|
import { DnsRecord, DomainKit, Principal, Provision, Verify } from "domainkit";
|
|
@@ -181,11 +183,50 @@ the group.
|
|
|
181
183
|
`/callback/:provider` is the one route the provider drives the browser to, so `Identity` has to
|
|
182
184
|
recognise a credential the browser sends on a top-level navigation.
|
|
183
185
|
|
|
186
|
+
A live, authenticated cancellation or completion failure redirects to the saved `returnTo` (or
|
|
187
|
+
`defaultReturnTo`) with `domainkit=cancelled|restart|inspect` and `domainkit-provider=<id>`.
|
|
188
|
+
`Connected` redirects after clearing all recovery parameters. `CallbackRecovery` from `domainkit/client`
|
|
189
|
+
reads or clears this display hint, including optional `domainkit-connection=<connectionId>` for
|
|
190
|
+
a reconnect; `Connect.useRecovery()` from `@domainkit/react` reads and
|
|
191
|
+
dismisses it in the browser. A `callback` policy can override the destination with a same-origin
|
|
192
|
+
string or return `undefined` to keep the default. Unknown, expired, spent, or mismatched flows
|
|
193
|
+
remain errors. See [callback destinations](https://domain-kit.dev/docs/reference/server#callback-destinations).
|
|
194
|
+
|
|
184
195
|
`authorize` is optional and runs after `principal` on every request with the route's name, one of
|
|
185
196
|
`Server.EndpointName`. Fail it with reason `Forbidden` for the 403 a UI expects. Omit it and every
|
|
186
197
|
authenticated principal reaches every route, which is right when your own middleware already gates
|
|
187
198
|
the mount.
|
|
188
199
|
|
|
200
|
+
`Server.registrationSettings({ providers, callbackBaseUrl })` derives secret-free registration
|
|
201
|
+
requirements from the same provider definitions and public callback base used by your server.
|
|
202
|
+
It needs no credentials, identity, storage, or network: OAuth settings describe callback URL,
|
|
203
|
+
client authentication, S256 PKCE, and configured scopes; Integration settings describe callback URL
|
|
204
|
+
and code exchange authentication. Token-only definitions return no settings. Custom interactive
|
|
205
|
+
methods without optional registration metadata fail `Unsupported`. Settings describe requirements,
|
|
206
|
+
not console readiness or granted permissions. See the
|
|
207
|
+
[server reference](https://domain-kit.dev/docs/reference/server#registration-settings).
|
|
208
|
+
|
|
209
|
+
Interactive starts reject empty credentials and malformed setup fields before redirecting or
|
|
210
|
+
storing a continuation, with `InvalidInput` naming the field without rejected values. Vercel resolves
|
|
211
|
+
its required credentials at start and again at completion for rotation. Configured callback bases
|
|
212
|
+
must be HTTP(S) URLs without credentials, query, or fragment; emulator paths remain supported.
|
|
213
|
+
|
|
214
|
+
`Server.Options.callback` optionally selects a same-origin destination for sanitized `Connected`,
|
|
215
|
+
`Cancelled`, or `Failed` outcomes. Return `Effect.succeed(undefined)` to retain the default response,
|
|
216
|
+
or an Effect of a destination string. The policy receives provider, validated `returnTo`, and
|
|
217
|
+
`connectionId` on success, optional `connectionId` on cancelled or failed reconnects, and
|
|
218
|
+
`recovery: "restart" | "inspect"` on failure; it receives no request,
|
|
219
|
+
state, code, provider text, secret, or full storage row. Hosts close over acquired services.
|
|
220
|
+
|
|
221
|
+
Only live flows matching the authenticated owner, actor, and provider are classified inside the
|
|
222
|
+
completion lock. Unknown, expired, spent, and mismatched callbacks invoke no policy, including read
|
|
223
|
+
races. Only `access_denied` is cancellation; other provider errors are failure. Cancellation and
|
|
224
|
+
failure retain expiry and retry behavior. `Connected` means durable account access, not DNS readiness;
|
|
225
|
+
zone selection can still be required. Partial persistence or a policy failure after connection needs
|
|
226
|
+
inspection, not a rollback assumption. `Connect.completeOutcome` exposes the shared classified result
|
|
227
|
+
to Effect hosts; `Connect.complete` retains its result/error contract. See the
|
|
228
|
+
[server reference](https://domain-kit.dev/docs/reference/server#callback-destinations) for compiled usage.
|
|
229
|
+
|
|
189
230
|
After an interactive connection completes, the callback redirects to the `returnTo` the flow was
|
|
190
231
|
started with, or to `defaultReturnTo`. The destination is resolved against the callback's own base
|
|
191
232
|
and must land on its origin, so neither the provider nor a crafted `returnTo` can steer the customer
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
//#region src/CallbackRecovery.ts
|
|
4
|
+
/** Browser-safe recovery hints for returning from an authenticated provider callback. */
|
|
5
|
+
var CallbackRecovery_exports = /* @__PURE__ */ __exportAll({
|
|
6
|
+
Model: () => Model,
|
|
7
|
+
read: () => read,
|
|
8
|
+
write: () => write
|
|
9
|
+
});
|
|
10
|
+
const Model = Schema.Struct({
|
|
11
|
+
kind: Schema.Literals([
|
|
12
|
+
"cancelled",
|
|
13
|
+
"restart",
|
|
14
|
+
"inspect"
|
|
15
|
+
]),
|
|
16
|
+
provider: Schema.String.check(Schema.isMinLength(1)),
|
|
17
|
+
connectionId: Schema.optionalKey(Schema.String.check(Schema.isMinLength(1)))
|
|
18
|
+
});
|
|
19
|
+
/** Reads a complete recovery hint; ignores missing, duplicate, or invalid metadata. */
|
|
20
|
+
const read = (url) => {
|
|
21
|
+
const kinds = url.searchParams.getAll("domainkit");
|
|
22
|
+
const providers = url.searchParams.getAll("domainkit-provider");
|
|
23
|
+
const connections = url.searchParams.getAll("domainkit-connection");
|
|
24
|
+
if (kinds.length !== 1 || providers.length !== 1 || connections.length > 1) return null;
|
|
25
|
+
const outcome = {
|
|
26
|
+
kind: kinds[0],
|
|
27
|
+
provider: providers[0],
|
|
28
|
+
...connections.length === 0 ? {} : { connectionId: connections[0] }
|
|
29
|
+
};
|
|
30
|
+
return Schema.is(Model)(outcome) ? outcome : null;
|
|
31
|
+
};
|
|
32
|
+
/** Clones the URL and replaces or clears all recovery hints, preserving other parameters and hash. */
|
|
33
|
+
const write = (url, outcome) => {
|
|
34
|
+
const result = new URL(url);
|
|
35
|
+
result.searchParams.delete("domainkit");
|
|
36
|
+
result.searchParams.delete("domainkit-provider");
|
|
37
|
+
result.searchParams.delete("domainkit-connection");
|
|
38
|
+
if (outcome !== null) {
|
|
39
|
+
result.searchParams.set("domainkit", outcome.kind);
|
|
40
|
+
result.searchParams.set("domainkit-provider", outcome.provider);
|
|
41
|
+
if (outcome.connectionId !== void 0) result.searchParams.set("domainkit-connection", outcome.connectionId);
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
46
|
+
export { CallbackRecovery_exports, Model, read, write };
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=CallbackRecovery.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CallbackRecovery.mjs","names":[],"sources":["../src/CallbackRecovery.ts"],"sourcesContent":["/** Browser-safe recovery hints for returning from an authenticated provider callback. */\nimport { Schema } from \"effect\";\n\nexport const Model = Schema.Struct({\n kind: Schema.Literals([\"cancelled\", \"restart\", \"inspect\"]),\n provider: Schema.String.check(Schema.isMinLength(1)),\n connectionId: Schema.optionalKey(Schema.String.check(Schema.isMinLength(1))),\n});\nexport type Model = typeof Model.Type;\n\n/** Reads a complete recovery hint; ignores missing, duplicate, or invalid metadata. */\nexport const read = (url: URL): Model | null => {\n const kinds = url.searchParams.getAll(\"domainkit\");\n const providers = url.searchParams.getAll(\"domainkit-provider\");\n const connections = url.searchParams.getAll(\"domainkit-connection\");\n if (kinds.length !== 1 || providers.length !== 1 || connections.length > 1) return null;\n const outcome = {\n kind: kinds[0],\n provider: providers[0],\n ...(connections.length === 0 ? {} : { connectionId: connections[0] }),\n };\n return Schema.is(Model)(outcome) ? outcome : null;\n};\n\n/** Clones the URL and replaces or clears all recovery hints, preserving other parameters and hash. */\nexport const write = (url: URL, outcome: Model | null): URL => {\n const result = new URL(url);\n result.searchParams.delete(\"domainkit\");\n result.searchParams.delete(\"domainkit-provider\");\n result.searchParams.delete(\"domainkit-connection\");\n if (outcome !== null) {\n result.searchParams.set(\"domainkit\", outcome.kind);\n result.searchParams.set(\"domainkit-provider\", outcome.provider);\n if (outcome.connectionId !== undefined)\n result.searchParams.set(\"domainkit-connection\", outcome.connectionId);\n }\n return result;\n};\n"],"mappings":";;;;;;;;;AAGA,MAAa,QAAQ,OAAO,OAAO;CACjC,MAAM,OAAO,SAAS;EAAC;EAAa;EAAW;CAAS,CAAC;CACzD,UAAU,OAAO,OAAO,MAAM,OAAO,YAAY,CAAC,CAAC;CACnD,cAAc,OAAO,YAAY,OAAO,OAAO,MAAM,OAAO,YAAY,CAAC,CAAC,CAAC;AAC7E,CAAC;;AAID,MAAa,QAAQ,QAA2B;CAC9C,MAAM,QAAQ,IAAI,aAAa,OAAO,WAAW;CACjD,MAAM,YAAY,IAAI,aAAa,OAAO,oBAAoB;CAC9D,MAAM,cAAc,IAAI,aAAa,OAAO,sBAAsB;CAClE,IAAI,MAAM,WAAW,KAAK,UAAU,WAAW,KAAK,YAAY,SAAS,GAAG,OAAO;CACnF,MAAM,UAAU;EACd,MAAM,MAAM;EACZ,UAAU,UAAU;EACpB,GAAI,YAAY,WAAW,IAAI,CAAC,IAAI,EAAE,cAAc,YAAY,GAAG;CACrE;CACA,OAAO,OAAO,GAAG,KAAK,CAAC,CAAC,OAAO,IAAI,UAAU;AAC/C;;AAGA,MAAa,SAAS,KAAU,YAA+B;CAC7D,MAAM,SAAS,IAAI,IAAI,GAAG;CAC1B,OAAO,aAAa,OAAO,WAAW;CACtC,OAAO,aAAa,OAAO,oBAAoB;CAC/C,OAAO,aAAa,OAAO,sBAAsB;CACjD,IAAI,YAAY,MAAM;EACpB,OAAO,aAAa,IAAI,aAAa,QAAQ,IAAI;EACjD,OAAO,aAAa,IAAI,sBAAsB,QAAQ,QAAQ;EAC9D,IAAI,QAAQ,iBAAiB,KAAA,GAC3B,OAAO,aAAa,IAAI,wBAAwB,QAAQ,YAAY;CACxE;CACA,OAAO;AACT"}
|
package/dist/Cloudflare.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { fail } from "./internal/error.mjs";
|
|
|
5
5
|
import { authorizationUrl, exchangeCode, refresh, revoke } from "./internal/oauth.mjs";
|
|
6
6
|
import { make, resolveAmong, tokenAuth } from "./Provider.mjs";
|
|
7
7
|
import { dns, listZones, provider as provider$1, tokenExpiry, verifyToken } from "./internal/cloudflare/client.mjs";
|
|
8
|
-
import { resolve } from "./internal/config.mjs";
|
|
8
|
+
import { endpoint, nonempty, requireValue, resolve, scopes, secret } from "./internal/config.mjs";
|
|
9
9
|
import { Effect, Redacted, Schema } from "effect";
|
|
10
10
|
//#region src/Cloudflare.ts
|
|
11
11
|
/** Cloudflare: personal API tokens and OAuth. */
|
|
@@ -76,11 +76,11 @@ const packSecret = (tokens) => Redacted.make(Schema.encodeSync(Secret)({
|
|
|
76
76
|
}));
|
|
77
77
|
const provider = (options = {}) => {
|
|
78
78
|
const fetch = options.fetch ?? globalThis.fetch;
|
|
79
|
-
const baseUrl =
|
|
79
|
+
const baseUrl = options.baseUrl ?? "https://api.cloudflare.com/client/v4";
|
|
80
80
|
const client = (token) => ({
|
|
81
81
|
token,
|
|
82
82
|
fetch,
|
|
83
|
-
baseUrl
|
|
83
|
+
baseUrl: baseUrl.replace(/\/$/, "")
|
|
84
84
|
});
|
|
85
85
|
/**
|
|
86
86
|
* The one account every zone the credential sees belongs to, with the name Cloudflare gives it.
|
|
@@ -102,7 +102,12 @@ const provider = (options = {}) => {
|
|
|
102
102
|
nameservers: zone.name_servers ?? []
|
|
103
103
|
});
|
|
104
104
|
const oauthClient = (oauth) => Effect.gen(function* () {
|
|
105
|
-
|
|
105
|
+
yield* endpoint(options.baseUrl ?? "https://api.cloudflare.com/client/v4", "baseUrl");
|
|
106
|
+
yield* endpoint(oauth.issuer ?? defaultIssuer, "oauth.issuer");
|
|
107
|
+
yield* endpoint(oauth.serverOrigin ?? oauth.issuer ?? defaultIssuer, "oauth.serverOrigin");
|
|
108
|
+
yield* scopes(oauth.scopes ?? defaultScopes, "oauth.scopes");
|
|
109
|
+
yield* requireValue(oauth.clientAuth, (v) => v === void 0 || v === "none" || v === "client_secret_basic" || v === "client_secret_post", "oauth.clientAuth", "oauth.clientAuth is unsupported");
|
|
110
|
+
const clientId = yield* resolve(oauth.clientId, "oauth.clientId").pipe(Effect.flatMap((v) => nonempty(v, "oauth.clientId")));
|
|
106
111
|
if (oauth.clientAuth === "none") return {
|
|
107
112
|
clientId,
|
|
108
113
|
clientSecret: null,
|
|
@@ -110,7 +115,7 @@ const provider = (options = {}) => {
|
|
|
110
115
|
};
|
|
111
116
|
return {
|
|
112
117
|
clientId,
|
|
113
|
-
clientSecret: yield* resolve(oauth.clientSecret, "oauth.clientSecret"),
|
|
118
|
+
clientSecret: yield* resolve(oauth.clientSecret, "oauth.clientSecret").pipe(Effect.flatMap((v) => secret(v, "oauth.clientSecret"))),
|
|
114
119
|
clientAuth: oauth.clientAuth ?? "client_secret_basic"
|
|
115
120
|
};
|
|
116
121
|
});
|
|
@@ -124,16 +129,20 @@ const provider = (options = {}) => {
|
|
|
124
129
|
function oauthAuth(settings) {
|
|
125
130
|
const scopes = settings.scopes ?? defaultScopes;
|
|
126
131
|
const browser = settings.issuer ?? defaultIssuer;
|
|
127
|
-
const endpoints = endpointsOf({
|
|
132
|
+
const endpoints = () => endpointsOf({
|
|
128
133
|
browser,
|
|
129
134
|
server: settings.serverOrigin ?? browser
|
|
130
135
|
});
|
|
131
136
|
const insecure = settings.allowPlaintext === void 0 ? {} : { allowPlaintext: settings.allowPlaintext };
|
|
132
137
|
return {
|
|
133
138
|
label: "Sign in with Cloudflare",
|
|
139
|
+
registration: {
|
|
140
|
+
clientAuth: settings.clientAuth ?? "client_secret_basic",
|
|
141
|
+
pkce: "S256"
|
|
142
|
+
},
|
|
134
143
|
scopes,
|
|
135
144
|
start: (input) => oauthClient(settings).pipe(Effect.map(({ clientId }) => ({ authorizationUrl: authorizationUrl({
|
|
136
|
-
server: endpoints,
|
|
145
|
+
server: endpoints(),
|
|
137
146
|
clientId,
|
|
138
147
|
scopes,
|
|
139
148
|
state: input.state,
|
|
@@ -142,7 +151,7 @@ const provider = (options = {}) => {
|
|
|
142
151
|
}) }))),
|
|
143
152
|
complete: (input) => oauthClient(settings).pipe(Effect.flatMap((oauthClientValue) => exchangeCode({
|
|
144
153
|
provider: provider$1,
|
|
145
|
-
server: endpoints,
|
|
154
|
+
server: endpoints(),
|
|
146
155
|
client: oauthClientValue,
|
|
147
156
|
code: input.code,
|
|
148
157
|
state: input.params.state ?? "",
|
|
@@ -156,8 +165,8 @@ const provider = (options = {}) => {
|
|
|
156
165
|
if (refreshToken === null) return yield* fail(new Unauthenticated({ message: "Cloudflare credential has no refresh token" }));
|
|
157
166
|
const tokens = yield* refresh({
|
|
158
167
|
provider: provider$1,
|
|
159
|
-
server: endpoints,
|
|
160
168
|
client: yield* oauthClient(settings),
|
|
169
|
+
server: endpoints(),
|
|
161
170
|
refreshToken,
|
|
162
171
|
fetch,
|
|
163
172
|
...insecure
|
|
@@ -172,8 +181,8 @@ const provider = (options = {}) => {
|
|
|
172
181
|
const { accessToken } = parseSecret(credential.secret);
|
|
173
182
|
yield* revoke({
|
|
174
183
|
provider: provider$1,
|
|
175
|
-
server: endpoints,
|
|
176
184
|
client: yield* oauthClient(settings),
|
|
185
|
+
server: endpoints(),
|
|
177
186
|
token: accessToken,
|
|
178
187
|
fetch,
|
|
179
188
|
...insecure
|
package/dist/Cloudflare.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Cloudflare.mjs","names":["Client.listZones","OAuth.authorizationUrl","OAuth.exchangeCode","Client.provider","Errors.fail","Reason.Unauthenticated","OAuth.refresh","OAuth.revoke","Provider.make","Provider.tokenAuth","Client.verifyToken","Client.tokenExpiry","DomainName.decode","Provider.resolveAmong","Reason.Unsupported","Client.dns"],"sources":["../src/Cloudflare.ts"],"sourcesContent":["/** Cloudflare: personal API tokens and OAuth. */\nimport { type Config, Effect, Redacted, Schema } from \"effect\";\n\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as DomainName from \"./DomainName.ts\";\nimport * as Client from \"./internal/cloudflare/client.ts\";\nimport type * as Protocol from \"./internal/cloudflare/protocol.ts\";\nimport { resolve } from \"./internal/config.ts\";\nimport type { Fetch } from \"./internal/http.ts\";\nimport * as OAuth from \"./internal/oauth.ts\";\nimport * as Provider from \"./Provider.ts\";\n\n/**\n * Authorization context: `accountId` is `null` when the credential spans several accounts or\n * none yet. Target context: the zone's account and zone id.\n */\nexport const AccountContext = Schema.Struct({\n accountId: Schema.NullOr(Schema.String),\n /** How the token was verified: user tokens span accounts, account tokens are pinned. */\n tokenKind: Schema.optionalKey(Schema.Literals([\"user\", \"account\"])),\n zoneId: Schema.optionalKey(Schema.String),\n});\n\n/** Token-method input: the API token, and the account id for account-owned tokens. */\nexport const TokenFields = Schema.Struct({\n token: Schema.RedactedFromValue(Schema.String),\n accountId: Schema.optionalKey(Schema.String),\n});\nexport type AccountContext = typeof AccountContext.Type;\n\nexport interface Options {\n /** Omit to offer tokens only. */\n readonly oauth?: OAuthOptions;\n readonly fetch?: Fetch;\n readonly baseUrl?: string;\n}\n\ninterface OAuthOptionsBase {\n readonly clientId: string | Config.Config<string>;\n /** Scope ids assigned to the OAuth client. Default: `zone.read`, `dns.read`, `dns.write`, `offline_access`. */\n readonly scopes?: ReadonlyArray<string>;\n /**\n * Origin for consent, token exchange, and revocation, which Cloudflare serves at\n * `/oauth2/auth`, `/oauth2/token`, and `/oauth2/revoke` beneath it. Default\n * `https://dash.cloudflare.com`; a stage points it at an emulator that mounts the same three\n * paths. It stays separate from `baseUrl` because in production these are different hosts:\n * `dash.cloudflare.com` for OAuth, `api.cloudflare.com/client/v4` for the REST API.\n */\n readonly issuer?: string;\n /**\n * Where the server reaches the OAuth endpoints when that differs from where the browser does,\n * for example an API in a container reaching an emulator through `host.docker.internal`.\n * Defaults to `issuer`.\n */\n readonly serverOrigin?: string;\n /**\n * Permit `http:` for these endpoints when they are not loopback, such as an emulator reached\n * through `host.docker.internal`. Development only: the client secret, the code, and the tokens\n * cross the network in the clear, and a name resolves wherever the machine has been told to.\n * Loopback needs no flag.\n */\n readonly allowPlaintext?: boolean;\n}\n\n/** A public OAuth client uses PKCE and has no client secret. */\nexport interface PublicOAuthOptions extends OAuthOptionsBase {\n readonly clientAuth: \"none\";\n readonly clientSecret?: never;\n}\n\n/** A confidential OAuth client authenticates with a required client secret. */\nexport interface ConfidentialOAuthOptions extends OAuthOptionsBase {\n /** Defaults to `client_secret_basic`. */\n readonly clientAuth?: \"client_secret_basic\" | \"client_secret_post\";\n readonly clientSecret: Redacted.Redacted<string> | Config.Config<Redacted.Redacted<string>>;\n}\n\nexport type OAuthOptions = PublicOAuthOptions | ConfidentialOAuthOptions;\n\nconst defaultIssuer = \"https://dash.cloudflare.com\";\n\n/**\n * The three OAuth endpoints Cloudflare serves under one origin. Consent is the browser's, exchange\n * and revocation are the server's, so a stage whose server reaches the same emulator by another\n * name gives two origins and keeps the paths.\n */\nconst endpointsOf = (origins: {\n readonly browser: string;\n readonly server: string;\n}): OAuth.Server => {\n const browser = origins.browser.replace(/\\/$/, \"\");\n const server = origins.server.replace(/\\/$/, \"\");\n return {\n authorization_endpoint: `${browser}/oauth2/auth`,\n issuer: browser,\n revocation_endpoint: `${server}/oauth2/revoke`,\n token_endpoint: `${server}/oauth2/token`,\n };\n};\n\nexport const server: OAuth.Server = endpointsOf({\n browser: defaultIssuer,\n server: defaultIssuer,\n});\n\nconst defaultScopes = [\"zone.read\", \"dns.read\", \"dns.write\", \"offline_access\"];\nconst capabilities = [\"dns:read\", \"dns:write\"] as const;\n\n/** OAuth credentials pack both tokens into `secret`; token credentials are the token itself. */\nconst Secret = Schema.fromJsonString(\n Schema.Struct({ accessToken: Schema.String, refreshToken: Schema.NullOr(Schema.String) }),\n);\n\nconst parseSecret = (secret: Redacted.Redacted<string>) => {\n const raw = Redacted.value(secret);\n const parsed = raw.startsWith(\"{\") ? Schema.decodeUnknownOption(Secret)(raw) : undefined;\n return parsed !== undefined && parsed._tag === \"Some\"\n ? parsed.value\n : { accessToken: raw, refreshToken: null };\n};\n\nconst packSecret = (tokens: OAuth.Tokens) =>\n Redacted.make(\n Schema.encodeSync(Secret)({\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n }),\n );\n\nexport const provider = (options: Options = {}): Provider.Definition<AccountContext> => {\n const fetch = options.fetch ?? globalThis.fetch;\n const baseUrl = (options.baseUrl ?? \"https://api.cloudflare.com/client/v4\").replace(/\\/$/, \"\");\n const client = (token: string): Client.Options => ({ token, fetch, baseUrl });\n\n /**\n * The one account every zone the credential sees belongs to, with the name Cloudflare gives it.\n * `null` when the credential spans several accounts or reaches no zone at all: neither names a\n * single account. `accountId` narrows the listing to a token already pinned to an account.\n */\n const discoverAccount = (token: string, accountId?: string) =>\n Client.listZones(client(token), accountId).pipe(\n Effect.map((zones) => {\n const accounts = new Map(zones.map((zone) => [zone.account.id, zone.account]));\n const [only] = accounts.values();\n return accounts.size === 1 && only !== undefined ? only : null;\n }),\n );\n\n const targetOf = (zone: Protocol.Zone): Provider.Target => ({\n zone: zone.name,\n context: { accountId: zone.account.id, zoneId: zone.id } satisfies AccountContext,\n label: `${zone.name} (${zone.account.name})`,\n nameservers: zone.name_servers ?? [],\n });\n\n const oauthClient = (oauth: NonNullable<Options[\"oauth\"]>) =>\n Effect.gen(function* () {\n const clientId = yield* resolve(oauth.clientId, \"oauth.clientId\");\n if (oauth.clientAuth === \"none\") {\n return { clientId, clientSecret: null, clientAuth: \"none\" } satisfies OAuth.Client;\n }\n const clientSecret = yield* resolve(oauth.clientSecret, \"oauth.clientSecret\");\n return {\n clientId,\n clientSecret,\n clientAuth: oauth.clientAuth ?? \"client_secret_basic\",\n } satisfies OAuth.Client;\n });\n\n const issued = (\n tokens: OAuth.Tokens,\n ): Effect.Effect<Provider.IssuedCredential, Errors.DomainKitError> =>\n discoverAccount(tokens.accessToken).pipe(\n Effect.map((account) => ({\n secret: packSecret(tokens),\n context: { accountId: account?.id ?? null } satisfies AccountContext,\n label: account?.name ?? null,\n expiresAt: tokens.expiresAt,\n })),\n );\n\n const oauth = options.oauth === undefined ? undefined : oauthAuth(options.oauth);\n\n function oauthAuth(settings: NonNullable<Options[\"oauth\"]>): Provider.OAuthAuth {\n const scopes = settings.scopes ?? defaultScopes;\n const browser = settings.issuer ?? defaultIssuer;\n const endpoints = endpointsOf({ browser, server: settings.serverOrigin ?? browser });\n const insecure =\n settings.allowPlaintext === undefined ? {} : { allowPlaintext: settings.allowPlaintext };\n return {\n label: \"Sign in with Cloudflare\",\n scopes,\n start: (input) =>\n oauthClient(settings).pipe(\n Effect.map(({ clientId }) => ({\n authorizationUrl: OAuth.authorizationUrl({\n server: endpoints,\n clientId,\n scopes,\n state: input.state,\n callbackUrl: input.callbackUrl,\n codeChallenge: input.codeChallenge,\n }),\n })),\n ),\n complete: (input) =>\n oauthClient(settings).pipe(\n Effect.flatMap((oauthClientValue) =>\n OAuth.exchangeCode({\n provider: Client.provider,\n server: endpoints,\n client: oauthClientValue,\n code: input.code,\n state: input.params.state ?? \"\",\n callbackUrl: input.callbackUrl,\n codeVerifier: input.codeVerifier,\n fetch,\n ...insecure,\n }),\n ),\n Effect.flatMap(issued),\n ),\n refresh: (credential) =>\n Effect.gen(function* () {\n const { refreshToken } = parseSecret(credential.secret);\n if (refreshToken === null) {\n return yield* Errors.fail(\n new Reason.Unauthenticated({\n message: \"Cloudflare credential has no refresh token\",\n }),\n );\n }\n const tokens = yield* OAuth.refresh({\n provider: Client.provider,\n server: endpoints,\n client: yield* oauthClient(settings),\n refreshToken,\n fetch,\n ...insecure,\n });\n return {\n secret: packSecret(tokens),\n context: credential.context,\n expiresAt: tokens.expiresAt,\n };\n }),\n revoke: (credential) =>\n Effect.gen(function* () {\n const { accessToken } = parseSecret(credential.secret);\n yield* OAuth.revoke({\n provider: Client.provider,\n server: endpoints,\n client: yield* oauthClient(settings),\n token: accessToken,\n fetch,\n ...insecure,\n });\n }),\n };\n }\n\n return Provider.make<AccountContext>({\n id: \"cloudflare\",\n name: \"Cloudflare\",\n nameservers: [\"ns.cloudflare.com\"],\n context: AccountContext,\n contextVersion: \"cloudflare.v1\",\n auth: {\n token: Provider.tokenAuth({\n label: \"API token\",\n docsUrl: \"https://developers.cloudflare.com/fundamentals/api/get-started/create-token/\",\n requiredCapabilities: capabilities,\n fields: TokenFields,\n authenticate: ({ token, accountId }) =>\n Effect.gen(function* () {\n const raw = Redacted.value(token);\n if (accountId !== undefined) {\n const expiresAt = yield* Client.verifyToken(\n client(raw),\n `/accounts/${encodeURIComponent(accountId)}/tokens/verify`,\n );\n // The account name only ever comes from a zone, so a pinned token that reaches no\n // zone connects unnamed rather than failing over a label.\n const pinned = yield* discoverAccount(raw, accountId).pipe(\n Effect.orElseSucceed(() => null),\n );\n return {\n secret: token,\n context: { accountId, tokenKind: \"account\" } satisfies AccountContext,\n label: pinned?.name ?? null,\n expiresAt,\n };\n }\n const discovered = yield* discoverAccount(raw);\n const verified = yield* Client.tokenExpiry(client(raw), discovered?.id ?? null);\n return {\n secret: token,\n context: {\n accountId: discovered?.id ?? null,\n tokenKind: verified.kind,\n } satisfies AccountContext,\n label: discovered?.name ?? null,\n expiresAt: verified.expiresAt,\n };\n }),\n }),\n ...(oauth === undefined ? {} : { oauth }),\n },\n session: (credential) => {\n const { accessToken } = parseSecret(credential.secret);\n const clientOptions = client(accessToken);\n const context = Schema.decodeUnknownOption(AccountContext)(credential.context);\n const accountId =\n context._tag === \"Some\" ? (context.value.accountId ?? undefined) : undefined;\n const listTargets = () =>\n Client.listZones(clientOptions, accountId).pipe(Effect.map((zones) => zones.map(targetOf)));\n return {\n capabilities: () => Effect.succeed(capabilities),\n listTargets,\n resolveTarget: (domain) =>\n Effect.gen(function* () {\n const name = yield* DomainName.decode(domain);\n const targets = yield* listTargets();\n return Provider.resolveAmong(name, targets);\n }),\n dns: (target) => {\n const zone = Schema.decodeUnknownOption(AccountContext)(target.context);\n const zoneId = zone._tag === \"Some\" ? zone.value.zoneId : undefined;\n if (zoneId === undefined) {\n const failure = Errors.fail(\n new Reason.Unsupported({\n provider: \"cloudflare\",\n operation: \"dns\",\n message: \"Cloudflare target has no zone id; only zone targets host records\",\n }),\n );\n return {\n list: () => failure,\n create: () => failure,\n get: () => failure,\n delete: () => failure,\n };\n }\n return Client.dns(clientOptions, zoneId);\n },\n };\n },\n });\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Cloudflare.mjs","names":["Client.listZones","Configuration.endpoint","Configuration.scopes","Configuration.requireValue","Configuration.resolve","Configuration.nonempty","Configuration.secret","OAuth.authorizationUrl","OAuth.exchangeCode","Client.provider","Errors.fail","Reason.Unauthenticated","OAuth.refresh","OAuth.revoke","Provider.make","Provider.tokenAuth","Client.verifyToken","Client.tokenExpiry","DomainName.decode","Provider.resolveAmong","Reason.Unsupported","Client.dns"],"sources":["../src/Cloudflare.ts"],"sourcesContent":["/** Cloudflare: personal API tokens and OAuth. */\nimport { type Config, Effect, Redacted, Schema } from \"effect\";\n\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as DomainName from \"./DomainName.ts\";\nimport * as Client from \"./internal/cloudflare/client.ts\";\nimport type * as Protocol from \"./internal/cloudflare/protocol.ts\";\nimport * as Configuration from \"./internal/config.ts\";\nimport type { Fetch } from \"./internal/http.ts\";\nimport * as OAuth from \"./internal/oauth.ts\";\nimport * as Provider from \"./Provider.ts\";\n\n/**\n * Authorization context: `accountId` is `null` when the credential spans several accounts or\n * none yet. Target context: the zone's account and zone id.\n */\nexport const AccountContext = Schema.Struct({\n accountId: Schema.NullOr(Schema.String),\n /** How the token was verified: user tokens span accounts, account tokens are pinned. */\n tokenKind: Schema.optionalKey(Schema.Literals([\"user\", \"account\"])),\n zoneId: Schema.optionalKey(Schema.String),\n});\n\n/** Token-method input: the API token, and the account id for account-owned tokens. */\nexport const TokenFields = Schema.Struct({\n token: Schema.RedactedFromValue(Schema.String),\n accountId: Schema.optionalKey(Schema.String),\n});\nexport type AccountContext = typeof AccountContext.Type;\n\nexport interface Options {\n /** Omit to offer tokens only. */\n readonly oauth?: OAuthOptions;\n readonly fetch?: Fetch;\n readonly baseUrl?: string;\n}\n\ninterface OAuthOptionsBase {\n readonly clientId: string | Config.Config<string>;\n /** Scope ids assigned to the OAuth client. Default: `zone.read`, `dns.read`, `dns.write`, `offline_access`. */\n readonly scopes?: ReadonlyArray<string>;\n /**\n * Origin for consent, token exchange, and revocation, which Cloudflare serves at\n * `/oauth2/auth`, `/oauth2/token`, and `/oauth2/revoke` beneath it. Default\n * `https://dash.cloudflare.com`; a stage points it at an emulator that mounts the same three\n * paths. It stays separate from `baseUrl` because in production these are different hosts:\n * `dash.cloudflare.com` for OAuth, `api.cloudflare.com/client/v4` for the REST API.\n */\n readonly issuer?: string;\n /**\n * Where the server reaches the OAuth endpoints when that differs from where the browser does,\n * for example an API in a container reaching an emulator through `host.docker.internal`.\n * Defaults to `issuer`.\n */\n readonly serverOrigin?: string;\n /**\n * Permit `http:` for these endpoints when they are not loopback, such as an emulator reached\n * through `host.docker.internal`. Development only: the client secret, the code, and the tokens\n * cross the network in the clear, and a name resolves wherever the machine has been told to.\n * Loopback needs no flag.\n */\n readonly allowPlaintext?: boolean;\n}\n\n/** A public OAuth client uses PKCE and has no client secret. */\nexport interface PublicOAuthOptions extends OAuthOptionsBase {\n readonly clientAuth: \"none\";\n readonly clientSecret?: never;\n}\n\n/** A confidential OAuth client authenticates with a required client secret. */\nexport interface ConfidentialOAuthOptions extends OAuthOptionsBase {\n /** Defaults to `client_secret_basic`. */\n readonly clientAuth?: \"client_secret_basic\" | \"client_secret_post\";\n readonly clientSecret: Redacted.Redacted<string> | Config.Config<Redacted.Redacted<string>>;\n}\n\nexport type OAuthOptions = PublicOAuthOptions | ConfidentialOAuthOptions;\n\nconst defaultIssuer = \"https://dash.cloudflare.com\";\n\n/**\n * The three OAuth endpoints Cloudflare serves under one origin. Consent is the browser's, exchange\n * and revocation are the server's, so a stage whose server reaches the same emulator by another\n * name gives two origins and keeps the paths.\n */\nconst endpointsOf = (origins: {\n readonly browser: string;\n readonly server: string;\n}): OAuth.Server => {\n const browser = origins.browser.replace(/\\/$/, \"\");\n const server = origins.server.replace(/\\/$/, \"\");\n return {\n authorization_endpoint: `${browser}/oauth2/auth`,\n issuer: browser,\n revocation_endpoint: `${server}/oauth2/revoke`,\n token_endpoint: `${server}/oauth2/token`,\n };\n};\n\nexport const server: OAuth.Server = endpointsOf({\n browser: defaultIssuer,\n server: defaultIssuer,\n});\n\nconst defaultScopes = [\"zone.read\", \"dns.read\", \"dns.write\", \"offline_access\"];\nconst capabilities = [\"dns:read\", \"dns:write\"] as const;\n\n/** OAuth credentials pack both tokens into `secret`; token credentials are the token itself. */\nconst Secret = Schema.fromJsonString(\n Schema.Struct({ accessToken: Schema.String, refreshToken: Schema.NullOr(Schema.String) }),\n);\n\nconst parseSecret = (secret: Redacted.Redacted<string>) => {\n const raw = Redacted.value(secret);\n const parsed = raw.startsWith(\"{\") ? Schema.decodeUnknownOption(Secret)(raw) : undefined;\n return parsed !== undefined && parsed._tag === \"Some\"\n ? parsed.value\n : { accessToken: raw, refreshToken: null };\n};\n\nconst packSecret = (tokens: OAuth.Tokens) =>\n Redacted.make(\n Schema.encodeSync(Secret)({\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n }),\n );\n\nexport const provider = (options: Options = {}): Provider.Definition<AccountContext> => {\n const fetch = options.fetch ?? globalThis.fetch;\n const baseUrl = options.baseUrl ?? \"https://api.cloudflare.com/client/v4\";\n const client = (token: string): Client.Options => ({\n token,\n fetch,\n baseUrl: baseUrl.replace(/\\/$/, \"\"),\n });\n\n /**\n * The one account every zone the credential sees belongs to, with the name Cloudflare gives it.\n * `null` when the credential spans several accounts or reaches no zone at all: neither names a\n * single account. `accountId` narrows the listing to a token already pinned to an account.\n */\n const discoverAccount = (token: string, accountId?: string) =>\n Client.listZones(client(token), accountId).pipe(\n Effect.map((zones) => {\n const accounts = new Map(zones.map((zone) => [zone.account.id, zone.account]));\n const [only] = accounts.values();\n return accounts.size === 1 && only !== undefined ? only : null;\n }),\n );\n\n const targetOf = (zone: Protocol.Zone): Provider.Target => ({\n zone: zone.name,\n context: { accountId: zone.account.id, zoneId: zone.id } satisfies AccountContext,\n label: `${zone.name} (${zone.account.name})`,\n nameservers: zone.name_servers ?? [],\n });\n\n const oauthClient = (oauth: NonNullable<Options[\"oauth\"]>) =>\n Effect.gen(function* () {\n yield* Configuration.endpoint(\n options.baseUrl ?? \"https://api.cloudflare.com/client/v4\",\n \"baseUrl\",\n );\n yield* Configuration.endpoint(oauth.issuer ?? defaultIssuer, \"oauth.issuer\");\n yield* Configuration.endpoint(\n oauth.serverOrigin ?? oauth.issuer ?? defaultIssuer,\n \"oauth.serverOrigin\",\n );\n yield* Configuration.scopes(oauth.scopes ?? defaultScopes, \"oauth.scopes\");\n yield* Configuration.requireValue(\n oauth.clientAuth,\n (v) =>\n v === undefined ||\n v === \"none\" ||\n v === \"client_secret_basic\" ||\n v === \"client_secret_post\",\n \"oauth.clientAuth\",\n \"oauth.clientAuth is unsupported\",\n );\n const clientId = yield* Configuration.resolve(oauth.clientId, \"oauth.clientId\").pipe(\n Effect.flatMap((v) => Configuration.nonempty(v, \"oauth.clientId\")),\n );\n if (oauth.clientAuth === \"none\") {\n return { clientId, clientSecret: null, clientAuth: \"none\" } satisfies OAuth.Client;\n }\n const clientSecret = yield* Configuration.resolve(\n oauth.clientSecret,\n \"oauth.clientSecret\",\n ).pipe(Effect.flatMap((v) => Configuration.secret(v, \"oauth.clientSecret\")));\n return {\n clientId,\n clientSecret,\n clientAuth: oauth.clientAuth ?? \"client_secret_basic\",\n } satisfies OAuth.Client;\n });\n\n const issued = (\n tokens: OAuth.Tokens,\n ): Effect.Effect<Provider.IssuedCredential, Errors.DomainKitError> =>\n discoverAccount(tokens.accessToken).pipe(\n Effect.map((account) => ({\n secret: packSecret(tokens),\n context: { accountId: account?.id ?? null } satisfies AccountContext,\n label: account?.name ?? null,\n expiresAt: tokens.expiresAt,\n })),\n );\n\n const oauth = options.oauth === undefined ? undefined : oauthAuth(options.oauth);\n\n function oauthAuth(settings: NonNullable<Options[\"oauth\"]>): Provider.OAuthAuth {\n const scopes = settings.scopes ?? defaultScopes;\n const browser = settings.issuer ?? defaultIssuer;\n const endpoints = () => endpointsOf({ browser, server: settings.serverOrigin ?? browser });\n const insecure =\n settings.allowPlaintext === undefined ? {} : { allowPlaintext: settings.allowPlaintext };\n return {\n label: \"Sign in with Cloudflare\",\n registration: { clientAuth: settings.clientAuth ?? \"client_secret_basic\", pkce: \"S256\" },\n scopes,\n start: (input) =>\n oauthClient(settings).pipe(\n Effect.map(({ clientId }) => ({\n authorizationUrl: OAuth.authorizationUrl({\n server: endpoints(),\n clientId,\n scopes,\n state: input.state,\n callbackUrl: input.callbackUrl,\n codeChallenge: input.codeChallenge,\n }),\n })),\n ),\n complete: (input) =>\n oauthClient(settings).pipe(\n Effect.flatMap((oauthClientValue) =>\n OAuth.exchangeCode({\n provider: Client.provider,\n server: endpoints(),\n client: oauthClientValue,\n code: input.code,\n state: input.params.state ?? \"\",\n callbackUrl: input.callbackUrl,\n codeVerifier: input.codeVerifier,\n fetch,\n ...insecure,\n }),\n ),\n Effect.flatMap(issued),\n ),\n refresh: (credential) =>\n Effect.gen(function* () {\n const { refreshToken } = parseSecret(credential.secret);\n if (refreshToken === null) {\n return yield* Errors.fail(\n new Reason.Unauthenticated({\n message: \"Cloudflare credential has no refresh token\",\n }),\n );\n }\n const tokens = yield* OAuth.refresh({\n provider: Client.provider,\n client: yield* oauthClient(settings),\n server: endpoints(),\n refreshToken,\n fetch,\n ...insecure,\n });\n return {\n secret: packSecret(tokens),\n context: credential.context,\n expiresAt: tokens.expiresAt,\n };\n }),\n revoke: (credential) =>\n Effect.gen(function* () {\n const { accessToken } = parseSecret(credential.secret);\n yield* OAuth.revoke({\n provider: Client.provider,\n client: yield* oauthClient(settings),\n server: endpoints(),\n token: accessToken,\n fetch,\n ...insecure,\n });\n }),\n };\n }\n\n return Provider.make<AccountContext>({\n id: \"cloudflare\",\n name: \"Cloudflare\",\n nameservers: [\"ns.cloudflare.com\"],\n context: AccountContext,\n contextVersion: \"cloudflare.v1\",\n auth: {\n token: Provider.tokenAuth({\n label: \"API token\",\n docsUrl: \"https://developers.cloudflare.com/fundamentals/api/get-started/create-token/\",\n requiredCapabilities: capabilities,\n fields: TokenFields,\n authenticate: ({ token, accountId }) =>\n Effect.gen(function* () {\n const raw = Redacted.value(token);\n if (accountId !== undefined) {\n const expiresAt = yield* Client.verifyToken(\n client(raw),\n `/accounts/${encodeURIComponent(accountId)}/tokens/verify`,\n );\n // The account name only ever comes from a zone, so a pinned token that reaches no\n // zone connects unnamed rather than failing over a label.\n const pinned = yield* discoverAccount(raw, accountId).pipe(\n Effect.orElseSucceed(() => null),\n );\n return {\n secret: token,\n context: { accountId, tokenKind: \"account\" } satisfies AccountContext,\n label: pinned?.name ?? null,\n expiresAt,\n };\n }\n const discovered = yield* discoverAccount(raw);\n const verified = yield* Client.tokenExpiry(client(raw), discovered?.id ?? null);\n return {\n secret: token,\n context: {\n accountId: discovered?.id ?? null,\n tokenKind: verified.kind,\n } satisfies AccountContext,\n label: discovered?.name ?? null,\n expiresAt: verified.expiresAt,\n };\n }),\n }),\n ...(oauth === undefined ? {} : { oauth }),\n },\n session: (credential) => {\n const { accessToken } = parseSecret(credential.secret);\n const clientOptions = client(accessToken);\n const context = Schema.decodeUnknownOption(AccountContext)(credential.context);\n const accountId =\n context._tag === \"Some\" ? (context.value.accountId ?? undefined) : undefined;\n const listTargets = () =>\n Client.listZones(clientOptions, accountId).pipe(Effect.map((zones) => zones.map(targetOf)));\n return {\n capabilities: () => Effect.succeed(capabilities),\n listTargets,\n resolveTarget: (domain) =>\n Effect.gen(function* () {\n const name = yield* DomainName.decode(domain);\n const targets = yield* listTargets();\n return Provider.resolveAmong(name, targets);\n }),\n dns: (target) => {\n const zone = Schema.decodeUnknownOption(AccountContext)(target.context);\n const zoneId = zone._tag === \"Some\" ? zone.value.zoneId : undefined;\n if (zoneId === undefined) {\n const failure = Errors.fail(\n new Reason.Unsupported({\n provider: \"cloudflare\",\n operation: \"dns\",\n message: \"Cloudflare target has no zone id; only zone targets host records\",\n }),\n );\n return {\n list: () => failure,\n create: () => failure,\n get: () => failure,\n delete: () => failure,\n };\n }\n return Client.dns(clientOptions, zoneId);\n },\n };\n },\n });\n};\n"],"mappings":"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"}
|
package/dist/Connect.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { DomainKitError, decode as decode$1, fail } from "./internal/error.mjs";
|
|
|
5
5
|
import { Service as Service$1 } from "./Custody.mjs";
|
|
6
6
|
import { fresh } from "./internal/ids.mjs";
|
|
7
7
|
import { pkce } from "./internal/oauth.mjs";
|
|
8
|
+
import { refuse } from "./internal/callback.mjs";
|
|
8
9
|
import { Service as Service$2 } from "./Principal.mjs";
|
|
9
10
|
import { Resolution, decodeContext, describeMethods, encodeContext } from "./Provider.mjs";
|
|
10
11
|
import { Service as Service$3 } from "./Providers.mjs";
|
|
@@ -18,6 +19,7 @@ import { Context, Data, DateTime, Duration, Effect, Layer, Option, Redacted, Sch
|
|
|
18
19
|
* call. Reuse of an existing connection for a new domain is `attach`.
|
|
19
20
|
*/
|
|
20
21
|
var Connect_exports = /* @__PURE__ */ __exportAll({
|
|
22
|
+
Completion: () => Completion,
|
|
21
23
|
Discovery: () => Discovery,
|
|
22
24
|
Method: () => Method,
|
|
23
25
|
Policy: () => Policy,
|
|
@@ -26,6 +28,7 @@ var Connect_exports = /* @__PURE__ */ __exportAll({
|
|
|
26
28
|
Started: () => Started,
|
|
27
29
|
attach: () => attach,
|
|
28
30
|
complete: () => complete,
|
|
31
|
+
completeOutcome: () => completeOutcome,
|
|
29
32
|
defaults: () => defaults,
|
|
30
33
|
detach: () => detach,
|
|
31
34
|
disconnect: () => disconnect,
|
|
@@ -48,6 +51,7 @@ const Method = {
|
|
|
48
51
|
integration: (options = {}) => MethodEnum.Integration(options)
|
|
49
52
|
};
|
|
50
53
|
const Started = Data.taggedEnum();
|
|
54
|
+
const Completion = Data.taggedEnum();
|
|
51
55
|
const Selection = Data.taggedEnum();
|
|
52
56
|
const Discovery = Data.taggedEnum();
|
|
53
57
|
var Service = class extends Context.Service()("@domainkit/Connect") {};
|
|
@@ -462,7 +466,10 @@ const make = Effect.gen(function* () {
|
|
|
462
466
|
});
|
|
463
467
|
}
|
|
464
468
|
});
|
|
465
|
-
const
|
|
469
|
+
const completeOutcome = (input) => storage.withLock(`continuation:${input.continuationId}`, Effect.gen(function* () {
|
|
470
|
+
const continuation = yield* storage.continuations.get(input.continuationId);
|
|
471
|
+
const principal = yield* Service$2;
|
|
472
|
+
if (continuation.ownerId !== principal.ownerId || continuation.actorId !== principal.actorId || input.provider !== void 0 && continuation.provider !== input.provider) return yield* refuse;
|
|
466
473
|
const callback = yield* Effect.try({
|
|
467
474
|
try: () => new URL(input.callbackUrl),
|
|
468
475
|
catch: () => new DomainKitError({ reason: new InvalidInput({
|
|
@@ -470,15 +477,28 @@ const make = Effect.gen(function* () {
|
|
|
470
477
|
field: "callbackUrl"
|
|
471
478
|
}) })
|
|
472
479
|
});
|
|
480
|
+
const decoded = yield* decode$1(Payload, continuation.payload, "continuation").pipe(Effect.result);
|
|
481
|
+
const reconnection = decoded._tag === "Success" && decoded.success.connectionId !== null && decoded.success.connectionId.length > 0 ? { connectionId: decoded.success.connectionId } : {};
|
|
473
482
|
const params = Object.fromEntries(callback.searchParams);
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
483
|
+
const failed = (message) => Completion.Failed({
|
|
484
|
+
error: new DomainKitError({ reason: new Unauthenticated({ message }) }),
|
|
485
|
+
recovery: "restart",
|
|
486
|
+
...reconnection
|
|
487
|
+
});
|
|
488
|
+
if (params.state !== input.continuationId) return failed("Callback state does not match the continuation");
|
|
489
|
+
if (params.error !== void 0) {
|
|
490
|
+
const outcome = failed(`Provider returned ${params.error}`);
|
|
491
|
+
return params.error === "access_denied" ? Completion.Cancelled({
|
|
492
|
+
error: outcome.error,
|
|
493
|
+
...reconnection
|
|
494
|
+
}) : outcome;
|
|
495
|
+
}
|
|
477
496
|
const code = params.code;
|
|
478
|
-
if (code === void 0) return
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
497
|
+
if (code === void 0) return failed("Callback has no code");
|
|
498
|
+
let persisting = false;
|
|
499
|
+
return yield* Effect.gen(function* () {
|
|
500
|
+
if (decoded._tag === "Failure") return yield* Effect.fail(decoded.failure);
|
|
501
|
+
const payload = decoded.success;
|
|
482
502
|
const definition = yield* providers.get(continuation.provider);
|
|
483
503
|
const issued = payload.method === "oauth" ? yield* Effect.gen(function* () {
|
|
484
504
|
const oauth = definition.auth.oauth;
|
|
@@ -498,6 +518,7 @@ const make = Effect.gen(function* () {
|
|
|
498
518
|
params
|
|
499
519
|
});
|
|
500
520
|
});
|
|
521
|
+
persisting = true;
|
|
501
522
|
if (payload.connectionId !== null) {
|
|
502
523
|
const held = yield* storage.connections.get(payload.connectionId);
|
|
503
524
|
const existing = yield* storage.authorizations.get(held.authorizationId);
|
|
@@ -528,8 +549,13 @@ const make = Effect.gen(function* () {
|
|
|
528
549
|
});
|
|
529
550
|
yield* storage.continuations.consume(input.continuationId).pipe(Effect.ignore);
|
|
530
551
|
return started(connection, attached);
|
|
531
|
-
}))
|
|
532
|
-
|
|
552
|
+
}).pipe(Effect.map((result) => Completion.Connected({ started: result })), Effect.catch((error) => Effect.succeed(Completion.Failed({
|
|
553
|
+
error,
|
|
554
|
+
...reconnection,
|
|
555
|
+
recovery: !persisting && error.reason._tag === "Unauthenticated" ? "restart" : "inspect"
|
|
556
|
+
}))));
|
|
557
|
+
}));
|
|
558
|
+
const complete = (input) => completeOutcome(input).pipe(Effect.flatMap((outcome) => outcome._tag === "Connected" ? Effect.succeed(outcome.started) : Effect.fail(outcome.error)));
|
|
533
559
|
const attach = (input) => Effect.gen(function* () {
|
|
534
560
|
const connection = yield* storage.connections.get(input.connectionId);
|
|
535
561
|
const authorization = yield* storage.authorizations.get(connection.authorizationId);
|
|
@@ -580,6 +606,7 @@ const make = Effect.gen(function* () {
|
|
|
580
606
|
start,
|
|
581
607
|
reconnect,
|
|
582
608
|
complete,
|
|
609
|
+
completeOutcome,
|
|
583
610
|
attach,
|
|
584
611
|
detach: (attachmentId) => storage.attachments.remove(attachmentId),
|
|
585
612
|
disconnect,
|
|
@@ -595,12 +622,13 @@ const discover = accessor((service) => service.discover);
|
|
|
595
622
|
const start = accessor((service) => service.start);
|
|
596
623
|
const reconnect = accessor((service) => service.reconnect);
|
|
597
624
|
const complete = accessor((service) => service.complete);
|
|
625
|
+
const completeOutcome = accessor((service) => service.completeOutcome);
|
|
598
626
|
const attach = accessor((service) => service.attach);
|
|
599
627
|
const detach = accessor((service) => service.detach);
|
|
600
628
|
const disconnect = accessor((service) => service.disconnect);
|
|
601
629
|
const refresh = accessor((service) => service.refresh);
|
|
602
630
|
const session = accessor((service) => service.session);
|
|
603
631
|
//#endregion
|
|
604
|
-
export { Connect_exports, Discovery, Method, Policy, Selection, Service, Started, attach, complete, defaults, detach, disconnect, discover, inspect, layer, make, reconnect, refresh, session, start, zones };
|
|
632
|
+
export { Completion, Connect_exports, Discovery, Method, Policy, Selection, Service, Started, attach, complete, completeOutcome, defaults, detach, disconnect, discover, inspect, layer, make, reconnect, refresh, session, start, zones };
|
|
605
633
|
|
|
606
634
|
//# sourceMappingURL=Connect.mjs.map
|