domainkit 0.10.0 → 0.11.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
@@ -258,6 +258,7 @@ const make = Effect.gen(function* () {
258
258
  const connection = attachment === null ? null : yield* storage.connections.get(attachment.connectionId);
259
259
  const authorization = connection === null ? null : yield* storage.authorizations.get(connection.authorizationId);
260
260
  const latest = attachment === null ? Option.none() : yield* storage.attempts.latest(attachment.id, "provisioning");
261
+ const attached = connection === null ? [] : yield* storage.attachments.list(connection.id);
261
262
  const reusable = [];
262
263
  for (const candidate of yield* storage.connections.list()) {
263
264
  if (candidate.id === connection?.id) continue;
@@ -285,6 +286,7 @@ const make = Effect.gen(function* () {
285
286
  revocation: authorization.revocation
286
287
  },
287
288
  lastReceiptId: Option.isSome(latest) ? latest.value.receipt?.id ?? null : null,
289
+ connectionDomains: attached.length,
288
290
  reusable,
289
291
  providers: providers.list().map((definition) => ({
290
292
  id: definition.id,
@@ -1 +1 @@
1
- {"version":3,"file":"Connect.mjs","names":["Errors.fail","Reason.InvalidInput","Storage.Service","Custody.Service","Providers.Service","Resolver.Service","Storage.Credential","Provider.decodeContext","Reason.Reconnect","Principal.Service","Reason.Forbidden","Storage.Authorization","Provider.encodeContext","DomainName.decode","Reason.NotFound","Storage.Attachment","candidates","DomainName.candidates","Provider.describeMethods","pkce","OAuth.pkce","Storage.Continuation","Errors.DomainKitError","Reason.Unauthenticated","Errors.decode"],"sources":["../src/Connect.ts"],"sourcesContent":["/**\n * Connect a provider, attach domains to it, and keep the credential alive. Interactive methods\n * (OAuth, integration) return a redirect and complete on callback; token methods connect in one\n * call. Reuse of an existing connection for a new domain is `attach`.\n */\nimport { Context, Data, DateTime, Duration, Effect, Layer, Option, Redacted, Schema } from \"effect\";\n\nimport * as Custody from \"./Custody.ts\";\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as DomainName from \"./DomainName.ts\";\nimport { fresh } from \"./internal/ids.ts\";\nimport * as OAuth from \"./internal/oauth.ts\";\nimport * as Principal from \"./Principal.ts\";\nimport * as Provider from \"./Provider.ts\";\nimport * as Providers from \"./Providers.ts\";\nimport * as Resolver from \"./Resolver.ts\";\nimport * as Storage from \"./Storage.ts\";\n\ntype Fx<A> = Effect.Effect<A, Errors.DomainKitError, Principal.Service>;\n\nexport type Method = Data.TaggedEnum<{\n /** Values keyed by the provider's declared `auth.token.fields`. */\n Token: { readonly values: Provider.TokenValues };\n OAuth: { readonly returnTo?: string };\n Integration: { readonly returnTo?: string };\n}>;\nconst MethodEnum = Data.taggedEnum<Method>();\n/** `token` accepts a lone string (`{ token }`) or a record; every value is wrapped in `Redacted`. */\nexport const Method = {\n ...MethodEnum,\n token: (\n values: string | Readonly<Record<string, Redacted.Redacted<string> | string | undefined>>,\n ): Method =>\n MethodEnum.Token({\n values:\n typeof values === \"string\"\n ? { token: Redacted.make(values) }\n : Object.fromEntries(\n Object.entries(values).map(([name, value]) => [\n name,\n typeof value === \"string\" ? Redacted.make(value) : value,\n ]),\n ),\n }),\n oauth: (options: { readonly returnTo?: string } = {}): Method => MethodEnum.OAuth(options),\n integration: (options: { readonly returnTo?: string } = {}): Method =>\n MethodEnum.Integration(options),\n} as const;\n\n/** Everything the UI needs about a domain's provider state. */\nexport interface Snapshot {\n readonly domain: string;\n readonly attachment: Storage.Attachment | null;\n readonly connection: Storage.Connection | null;\n readonly authorization: Pick<\n Storage.Authorization,\n \"id\" | \"provider\" | \"method\" | \"capabilities\" | \"revocation\"\n > | null;\n readonly lastReceiptId: string | null;\n /** Other connections of this owner that could serve this domain. */\n readonly reusable: ReadonlyArray<{\n readonly connection: Storage.Connection;\n readonly provider: string;\n readonly method: Storage.AuthMethod;\n }>;\n readonly providers: ReadonlyArray<{\n readonly id: string;\n readonly name: string;\n readonly methods: ReadonlyArray<Provider.MethodDescriptor>;\n }>;\n}\n\nexport type Started = Data.TaggedEnum<{\n Connected: {\n readonly connection: Storage.Connection;\n readonly attachment: Storage.Attachment | null;\n };\n Redirect: { readonly authorizationUrl: string; readonly continuationId: string };\n SelectionRequired: {\n readonly connection: Storage.Connection;\n readonly candidates: ReadonlyArray<Provider.Target>;\n };\n}>;\nexport const Started = Data.taggedEnum<Started>();\n\n/** `attach` either attached the domain or needs the caller to pick a zone. */\nexport type Selection = Data.TaggedEnum<{\n SelectionRequired: { readonly candidates: ReadonlyArray<Provider.Target> };\n}>;\nexport const Selection = Data.taggedEnum<Selection>();\n\nexport type Attached = Storage.Attachment | Selection;\n\nexport type Refreshed = \"current\" | \"refreshed\" | \"reconnect\";\n\n/** Which of the principal's connections serves a domain, from nameserver evidence and zone lists. */\nexport type Discovery = Data.TaggedEnum<{\n Resolved: { readonly connectionId: string; readonly target: Provider.Target };\n SelectionRequired: {\n readonly candidates: ReadonlyArray<{\n readonly connectionId: string;\n readonly target: Provider.Target;\n }>;\n };\n /**\n * No connection reaches the domain. `host` names the one registered provider whose declared\n * nameserver suffixes cover every authoritative nameserver; `null` when none or several do.\n */\n NotFound: {\n readonly nameservers: ReadonlyArray<string>;\n readonly host: { readonly provider: string } | null;\n };\n}>;\nexport const Discovery = Data.taggedEnum<Discovery>();\n\nexport interface Interface {\n readonly inspect: (domain: string) => Fx<Snapshot>;\n /**\n * Resolve the domain's authoritative nameservers, then match them against the zones each of\n * the principal's connections can reach: the closest zone wins, a decisive nameserver match\n * breaks ties, and anything else needs a selection.\n */\n readonly discover: (domain: string) => Fx<Discovery>;\n /** Connect a provider and, when `domain` is given, attach it. Token methods finish here. */\n readonly start: (input: {\n readonly provider: string;\n readonly method: Method;\n readonly domain?: string;\n readonly callbackUrl?: string;\n }) => Fx<Started>;\n /** Finish an interactive connection from the provider callback (the full callback URL). */\n readonly complete: (input: {\n readonly continuationId: string;\n readonly callbackUrl: string;\n }) => Fx<Started>;\n readonly attach: (input: {\n readonly connectionId: string;\n readonly domain: string;\n readonly target?: Provider.Target;\n }) => Fx<Attached>;\n /** Records stay in DNS; DomainKit forgets the domain. Use `Cleanup` first to remove records. */\n readonly detach: (attachmentId: string) => Fx<void>;\n /** Detach every domain and revoke the credential at the provider when the method supports it. */\n readonly disconnect: (connectionId: string) => Fx<void>;\n /** Refresh a near-expiry credential, single-flighted. Called automatically before provider use. */\n readonly refresh: (connectionId: string) => Fx<Refreshed>;\n /** Live provider session for an attachment; used by Provision, Cleanup, Verify. */\n readonly session: (\n attachmentId: string,\n ) => Fx<{ readonly session: Provider.Session; readonly target: Provider.Target }>;\n}\n\nexport class Service extends Context.Service<Service, Interface>()(\"@domainkit/Connect\") {}\n\n/** Which owner-scoped reuse is allowed. Default: any connection of the same owner may serve any of its domains. */\nexport interface PolicyShape {\n readonly allowReuse: (input: {\n readonly connection: Storage.Connection;\n readonly domain: string;\n }) => Effect.Effect<boolean, never, Principal.Service>;\n /** Refresh this long before expiry. Default 10 minutes. */\n readonly refreshBeforeMs: number;\n /** Continuation lifetime. Default 15 minutes. */\n readonly continuationTtlMs: number;\n}\nexport const defaults: PolicyShape = {\n allowReuse: () => Effect.succeed(true),\n refreshBeforeMs: 10 * 60_000,\n continuationTtlMs: 15 * 60_000,\n};\nexport class Policy extends Context.Reference<PolicyShape>(\"@domainkit/Connect/Policy\", {\n defaultValue: () => defaults,\n}) {}\n\n// ---------------------------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------------------------\n\nconst Payload = Schema.Struct({\n method: Schema.Literals([\"oauth\", \"integration\"]),\n codeVerifier: Schema.NullOr(Schema.String),\n callbackUrl: Schema.String,\n domain: Schema.NullOr(Schema.String),\n});\n\nconst Target = Schema.Struct({\n zone: Schema.String,\n context: Schema.Unknown,\n label: Schema.String,\n});\n\nconst invalid = (message: string, field?: string) =>\n Errors.fail(new Reason.InvalidInput({ message, ...(field === undefined ? {} : { field }) }));\n\n/** Nameserver hostnames compare case-insensitively and without a trailing dot. */\nconst normalize = (name: string) => name.toLowerCase().replace(/\\.$/, \"\");\n\nexport const make: Effect.Effect<\n Interface,\n never,\n Storage.Service | Custody.Service | Providers.Service | Resolver.Service\n> = Effect.gen(function* () {\n const storage = yield* Storage.Service;\n const custody = yield* Custody.Service;\n const providers = yield* Providers.Service;\n const resolver = yield* Resolver.Service;\n\n const seal = (issued: Provider.IssuedCredential) =>\n Effect.gen(function* () {\n const ciphertext = yield* custody.seal(issued.secret);\n return new Storage.Credential({\n ciphertext,\n expiresAt: issued.expiresAt,\n rotatedAt: yield* DateTime.now,\n });\n });\n\n /** Plaintext credential with the context decoded from its stored envelope. */\n const open = (authorization: Storage.Authorization): Fx<Provider.Credential> =>\n Effect.gen(function* () {\n const definition = yield* providers.get(authorization.provider);\n const row = yield* storage.authorizations.credential(authorization.id);\n const secret = yield* custody.open(row.ciphertext);\n const context = yield* Provider.decodeContext(definition, authorization.context);\n return { secret, context };\n });\n\n const refresherFor = (definition: Provider.Definition, method: Storage.AuthMethod) =>\n method === \"oauth\"\n ? definition.auth.oauth?.refresh\n : method === \"integration\"\n ? definition.auth.integration?.refresh\n : undefined;\n\n const revokerFor = (definition: Provider.Definition, method: Storage.AuthMethod) =>\n method === \"oauth\"\n ? definition.auth.oauth?.revoke\n : method === \"integration\"\n ? definition.auth.integration?.revoke\n : undefined;\n\n const revokeAt = (authorization: Storage.Authorization): Fx<void> =>\n Effect.gen(function* () {\n const definition = yield* providers.get(authorization.provider);\n const revoke = revokerFor(definition, authorization.method);\n if (revoke === undefined) return;\n const credential = yield* open(authorization);\n yield* revoke(credential);\n });\n\n /** Finish revocations a crash interrupted; best effort, never blocks the caller. */\n const recover = storage.authorizations.recoverRevocations(revokeAt).pipe(Effect.ignore);\n\n const refresh: Interface[\"refresh\"] = (connectionId) =>\n Effect.gen(function* () {\n const policy = yield* Policy;\n const connection = yield* storage.connections.get(connectionId);\n const authorization = yield* storage.authorizations.get(connection.authorizationId);\n const definition = yield* providers.get(authorization.provider);\n const refresher = refresherFor(definition, authorization.method);\n const dueAt = (row: Storage.Credential, now: DateTime.Utc) =>\n row.expiresAt !== null &&\n DateTime.toEpochMillis(row.expiresAt) - DateTime.toEpochMillis(now) <=\n policy.refreshBeforeMs;\n const expired = (row: Storage.Credential, now: DateTime.Utc) =>\n row.expiresAt !== null &&\n DateTime.toEpochMillis(row.expiresAt) <= DateTime.toEpochMillis(now);\n const current = yield* storage.authorizations.credential(authorization.id);\n const now = yield* DateTime.now;\n if (!dueAt(current, now)) return \"current\" as const;\n if (refresher === undefined)\n return expired(current, now) ? (\"reconnect\" as const) : (\"current\" as const);\n return yield* storage\n .withLock(\n `refresh:${authorization.id}`,\n Effect.gen(function* () {\n // Disconnect's two-phase revocation does not take this lock: re-read the row here\n // so a revocation that started meanwhile skips the refresh instead of racing it.\n const live = yield* storage.authorizations.get(authorization.id).pipe(\n Effect.map(Option.some),\n Effect.catchIf(\n (error) => error.reason._tag === \"NotFound\",\n () => Effect.succeed(Option.none<Storage.Authorization>()),\n ),\n );\n if (Option.isNone(live) || live.value.revocation !== \"active\") {\n return \"reconnect\" as const;\n }\n const latest = yield* storage.authorizations.credential(authorization.id);\n if (!dueAt(latest, yield* DateTime.now)) return \"current\" as const;\n const secret = yield* custody.open(latest.ciphertext);\n const context = yield* Provider.decodeContext(definition, live.value.context);\n const issued = yield* refresher({ secret, context });\n // A revocation that completed while the provider was issuing leaves no row to\n // rotate; revoke the fresh credential so it does not stay live without a home.\n yield* storage.authorizations\n .rotate(authorization.id, yield* seal(issued))\n .pipe(\n Effect.tapError((error) =>\n error.reason._tag === \"NotFound\"\n ? (revokerFor(definition, authorization.method)?.(issued) ?? Effect.void).pipe(\n Effect.ignore,\n )\n : Effect.void,\n ),\n );\n return \"refreshed\" as const;\n }),\n )\n .pipe(\n Effect.catchIf(\n (error) => error.reason._tag === \"Busy\" && !expired(current, now),\n () => Effect.succeed(\"current\" as const),\n ),\n Effect.catchIf(\n (error) => error.reason._tag === \"Unauthenticated\",\n () => Effect.succeed(\"reconnect\" as const),\n ),\n );\n });\n\n const sessionFor = (connection: Storage.Connection): Fx<Provider.Session> =>\n Effect.gen(function* () {\n const authorization = yield* storage.authorizations.get(connection.authorizationId);\n const reconnect = Errors.fail(\n new Reason.Reconnect({\n provider: authorization.provider,\n connectionId: connection.id,\n }),\n );\n if (authorization.revocation !== \"active\") return yield* reconnect;\n if ((yield* refresh(connection.id)) === \"reconnect\") return yield* reconnect;\n const definition = yield* providers.get(authorization.provider);\n return definition.session(yield* open(authorization));\n });\n\n const connectWith = (input: {\n readonly definition: Provider.Definition;\n readonly method: Storage.AuthMethod;\n readonly issued: Provider.IssuedCredential;\n readonly requiredCapabilities: ReadonlyArray<Storage.Capability>;\n }): Fx<{ readonly connection: Storage.Connection; readonly session: Provider.Session }> =>\n Effect.gen(function* () {\n const principal = yield* Principal.Service;\n const session = input.definition.session(input.issued);\n const held = yield* session.capabilities();\n const missing = input.requiredCapabilities.filter((capability) => !held.includes(capability));\n if (missing.length > 0) {\n return yield* Errors.fail(\n new Reason.Forbidden({\n message: `${input.definition.name} credential lacks ${missing.join(\", \")}`,\n }),\n );\n }\n const authorization = new Storage.Authorization({\n id: yield* fresh(\"auth\"),\n ownerId: principal.ownerId,\n provider: input.definition.id,\n method: input.method,\n capabilities: held,\n context: yield* Provider.encodeContext(input.definition, input.issued.context),\n revocation: \"active\",\n createdBy: principal.actorId,\n createdAt: yield* DateTime.now,\n });\n yield* storage.authorizations.upsert({\n authorization,\n credential: yield* seal(input.issued),\n });\n const connection = yield* storage.connections.create(authorization.id);\n return { connection, session };\n });\n\n const attachWith = (input: {\n readonly definition: Provider.Definition;\n readonly connection: Storage.Connection;\n readonly session: Provider.Session;\n readonly domain: string;\n readonly target?: Provider.Target;\n }): Fx<Attached> =>\n Effect.gen(function* () {\n const domain = yield* DomainName.decode(input.domain);\n const existing = yield* storage.attachments.byDomain(domain);\n if (Option.isSome(existing)) {\n if (existing.value.connectionId === input.connection.id) return existing.value;\n return yield* invalid(`${domain} is already attached to another connection`, \"domain\");\n }\n const resolution: Provider.Resolution =\n input.target === undefined\n ? yield* input.session.resolveTarget(domain)\n : Provider.Resolution.Resolved({ target: input.target });\n switch (resolution._tag) {\n case \"SelectionRequired\":\n return Selection.SelectionRequired({ candidates: resolution.candidates });\n case \"NotFound\":\n return yield* Errors.fail(new Reason.NotFound({ entity: \"zone\", id: domain }));\n case \"Resolved\":\n return yield* storage.attachments.create({\n connectionId: input.connection.id,\n domain,\n zone: resolution.target.zone,\n target: {\n ...resolution.target,\n context: yield* Provider.encodeContext(input.definition, resolution.target.context),\n },\n });\n }\n });\n\n const started = (connection: Storage.Connection, attached: Attached | null): Started =>\n attached === null\n ? Started.Connected({ connection, attachment: null })\n : attached instanceof Storage.Attachment\n ? Started.Connected({ connection, attachment: attached })\n : Started.SelectionRequired({ connection, candidates: attached.candidates });\n\n /** Authoritative nameservers for the closest zone that answers an NS query. */\n const authoritative = (candidates: ReadonlyArray<DomainName.Model>) =>\n Effect.gen(function* () {\n for (const zone of candidates) {\n const outcomes = yield* resolver.resolve(zone, \"NS\");\n const names = outcomes.flatMap((outcome) =>\n outcome._tag === \"Answered\"\n ? outcome.answer.records.flatMap((record) =>\n record._tag === \"NS\" ? [record.nameserver] : [],\n )\n : [],\n );\n if (names.length > 0) return [...new Set(names)].sort();\n }\n return [] as ReadonlyArray<string>;\n });\n\n const discover: Interface[\"discover\"] = (input) =>\n Effect.gen(function* () {\n const domain = yield* DomainName.decode(input);\n const candidates = DomainName.candidates(domain);\n const nameservers = yield* authoritative(candidates);\n const reachable: Array<{\n readonly connectionId: string;\n readonly target: Provider.Target;\n }> = [];\n for (const connection of yield* storage.connections.list()) {\n const targets = yield* sessionFor(connection).pipe(\n Effect.flatMap((session) => session.listTargets()),\n Effect.catch(() => Effect.succeed([] as ReadonlyArray<Provider.Target>)),\n );\n for (const target of targets) {\n if (candidates.includes(target.zone as DomainName.Model)) {\n reachable.push({ connectionId: connection.id, target });\n }\n }\n }\n for (const zone of candidates) {\n const matches = reachable\n .filter(({ target }) => target.zone === zone)\n .sort((left, right) => left.connectionId.localeCompare(right.connectionId));\n if (matches.length === 0) continue;\n const [only] = matches;\n if (matches.length === 1 && only !== undefined) return Discovery.Resolved(only);\n const decisive = matches.filter(({ target }) => {\n const configured = new Set((target.nameservers ?? []).map(normalize));\n return nameservers.length > 0 && nameservers.every((name) => configured.has(name));\n });\n const [winner] = decisive;\n if (decisive.length === 1 && winner !== undefined) return Discovery.Resolved(winner);\n return Discovery.SelectionRequired({ candidates: matches });\n }\n return Discovery.NotFound({ nameservers, host: hostOf(nameservers) });\n });\n\n /** The one registered provider whose declared suffixes cover every nameserver, else `null`. */\n const hostOf = (nameservers: ReadonlyArray<string>): { readonly provider: string } | null => {\n if (nameservers.length === 0) return null;\n const names = nameservers.map(normalize);\n const hosts = providers.list().filter((definition) => {\n const suffixes = (definition.nameservers ?? []).map(normalize);\n return (\n suffixes.length > 0 &&\n names.every((name) =>\n suffixes.some((suffix) => name === suffix || name.endsWith(`.${suffix}`)),\n )\n );\n });\n const [host] = hosts;\n return hosts.length === 1 && host !== undefined ? { provider: host.id } : null;\n };\n\n const inspect: Interface[\"inspect\"] = (input) =>\n Effect.gen(function* () {\n const domain = yield* DomainName.decode(input);\n const policy = yield* Policy;\n const attachment = Option.getOrNull(yield* storage.attachments.byDomain(domain));\n const connection =\n attachment === null ? null : yield* storage.connections.get(attachment.connectionId);\n const authorization =\n connection === null ? null : yield* storage.authorizations.get(connection.authorizationId);\n const latest =\n attachment === null\n ? Option.none()\n : yield* storage.attempts.latest(attachment.id, \"provisioning\");\n const reusable: Array<Snapshot[\"reusable\"][number]> = [];\n for (const candidate of yield* storage.connections.list()) {\n if (candidate.id === connection?.id) continue;\n if (!(yield* policy.allowReuse({ connection: candidate, domain }))) continue;\n const candidateAuthorization = yield* storage.authorizations.get(candidate.authorizationId);\n if (candidateAuthorization.revocation !== \"active\") continue;\n reusable.push({\n connection: candidate,\n provider: candidateAuthorization.provider,\n method: candidateAuthorization.method,\n });\n }\n return {\n domain,\n attachment,\n connection,\n authorization:\n authorization === null\n ? null\n : {\n id: authorization.id,\n provider: authorization.provider,\n method: authorization.method,\n capabilities: authorization.capabilities,\n revocation: authorization.revocation,\n },\n lastReceiptId: Option.isSome(latest) ? (latest.value.receipt?.id ?? null) : null,\n reusable,\n providers: providers.list().map((definition) => ({\n id: definition.id,\n name: definition.name,\n methods: Provider.describeMethods(definition),\n })),\n };\n });\n\n const start: Interface[\"start\"] = (input) =>\n Effect.gen(function* () {\n yield* recover;\n const principal = yield* Principal.Service;\n const policy = yield* Policy;\n const definition = yield* providers.get(input.provider);\n if (input.domain !== undefined) yield* DomainName.decode(input.domain);\n switch (input.method._tag) {\n case \"Token\": {\n const token = definition.auth.token;\n if (token === undefined) {\n return yield* invalid(`${definition.name} does not accept tokens`, \"method\");\n }\n const issued = yield* token.authenticate(input.method.values);\n const { connection, session } = yield* connectWith({\n definition,\n method: \"token\",\n issued,\n requiredCapabilities: token.requiredCapabilities,\n });\n const attached =\n input.domain === undefined\n ? null\n : yield* attachWith({ definition, connection, session, domain: input.domain });\n return started(connection, attached);\n }\n case \"OAuth\":\n case \"Integration\": {\n const interactive =\n input.method._tag === \"OAuth\" ? definition.auth.oauth : definition.auth.integration;\n if (interactive === undefined) {\n return yield* invalid(\n `${definition.name} does not offer ${input.method._tag.toLowerCase()} connections`,\n \"method\",\n );\n }\n if (input.callbackUrl === undefined) {\n return yield* invalid(\"Interactive connections need a callbackUrl\", \"callbackUrl\");\n }\n const continuationId = yield* fresh(\"cont\");\n const pkce = input.method._tag === \"OAuth\" ? yield* OAuth.pkce() : null;\n const redirect =\n input.method._tag === \"OAuth\"\n ? yield* (interactive as Provider.OAuthAuth).start({\n state: continuationId,\n callbackUrl: input.callbackUrl,\n codeChallenge: pkce?.codeChallenge ?? \"\",\n })\n : yield* (interactive as Provider.IntegrationAuth).start({\n state: continuationId,\n callbackUrl: input.callbackUrl,\n });\n yield* storage.continuations.put(\n new Storage.Continuation({\n id: continuationId,\n ownerId: principal.ownerId,\n actorId: principal.actorId,\n provider: definition.id,\n payload: Schema.encodeSync(Payload)({\n method: input.method._tag === \"OAuth\" ? \"oauth\" : \"integration\",\n codeVerifier: pkce?.codeVerifier ?? null,\n callbackUrl: input.callbackUrl,\n domain: input.domain ?? null,\n }),\n returnTo: input.method.returnTo ?? null,\n expiresAt: DateTime.addDuration(\n yield* DateTime.now,\n Duration.millis(policy.continuationTtlMs),\n ),\n }),\n );\n return Started.Redirect({\n authorizationUrl: redirect.authorizationUrl,\n continuationId,\n });\n }\n }\n });\n\n const complete: Interface[\"complete\"] = (input) =>\n Effect.gen(function* () {\n // The continuation is spent only after the credential and connection are persisted, so a\n // failure before that (bad callback, provider outage, storage outage) leaves the flow\n // retryable. Concurrent callbacks for the same continuation serialize on a lock; the\n // loser sees Busy or NotFound. A provider code is single-use, so a retry after the\n // provider already redeemed it fails Unauthenticated and the customer starts over.\n const callback = yield* Effect.try({\n try: () => new URL(input.callbackUrl),\n catch: () =>\n new Errors.DomainKitError({\n reason: new Reason.InvalidInput({\n message: \"callbackUrl is not a URL\",\n field: \"callbackUrl\",\n }),\n }),\n });\n const params = Object.fromEntries(callback.searchParams);\n const unauthenticated = (message: string) =>\n Errors.fail(new Reason.Unauthenticated({ message }));\n if (params.error !== undefined) {\n return yield* unauthenticated(`Provider returned ${params.error}`);\n }\n if (params.state !== input.continuationId) {\n return yield* unauthenticated(\"Callback state does not match the continuation\");\n }\n const code = params.code;\n if (code === undefined) return yield* unauthenticated(\"Callback has no code\");\n return yield* storage.withLock(\n `continuation:${input.continuationId}`,\n Effect.gen(function* () {\n const continuation = yield* storage.continuations.get(input.continuationId);\n const payload = yield* Errors.decode(Payload, continuation.payload, \"continuation\");\n const definition = yield* providers.get(continuation.provider);\n const issued =\n payload.method === \"oauth\"\n ? yield* Effect.gen(function* () {\n const oauth = definition.auth.oauth;\n if (oauth === undefined || payload.codeVerifier === null) {\n return yield* invalid(\n `${definition.name} does not offer oauth connections`,\n \"method\",\n );\n }\n return yield* oauth.complete({\n code,\n callbackUrl: payload.callbackUrl,\n codeVerifier: payload.codeVerifier,\n params,\n });\n })\n : yield* Effect.gen(function* () {\n const integration = definition.auth.integration;\n if (integration === undefined) {\n return yield* invalid(\n `${definition.name} does not offer integration connections`,\n \"method\",\n );\n }\n return yield* integration.complete({\n code,\n callbackUrl: payload.callbackUrl,\n params,\n });\n });\n const { connection, session } = yield* connectWith({\n definition,\n method: payload.method,\n issued,\n requiredCapabilities: [],\n });\n const attached =\n payload.domain === null\n ? null\n : yield* attachWith({ definition, connection, session, domain: payload.domain });\n // The connection is durable at this point. Whatever happens to the continuation\n // now (expired, already spent, or a storage failure while spending it) no longer\n // changes the outcome: the row expires on its own and a replayed callback fails at\n // the provider, so the persisted result is returned.\n yield* storage.continuations.consume(input.continuationId).pipe(Effect.ignore);\n return started(connection, attached);\n }),\n );\n });\n\n const attach: Interface[\"attach\"] = (input) =>\n Effect.gen(function* () {\n const connection = yield* storage.connections.get(input.connectionId);\n const authorization = yield* storage.authorizations.get(connection.authorizationId);\n const definition = yield* providers.get(authorization.provider);\n const session = yield* sessionFor(connection);\n return yield* attachWith({\n definition,\n connection,\n session,\n domain: input.domain,\n ...(input.target === undefined ? {} : { target: input.target }),\n });\n });\n\n const disconnect: Interface[\"disconnect\"] = (connectionId) =>\n Effect.gen(function* () {\n yield* recover;\n const connection = yield* storage.connections.get(connectionId);\n const attachments = yield* storage.attachments.list(connectionId);\n yield* Effect.forEach(attachments, (attachment) => storage.attachments.remove(attachment.id));\n yield* storage.connections.remove(connectionId);\n const remaining = (yield* storage.connections.list()).some(\n (candidate) => candidate.authorizationId === connection.authorizationId,\n );\n if (remaining) return;\n const authorization = yield* storage.authorizations.get(connection.authorizationId);\n yield* storage.authorizations.revoke(authorization.id, revokeAt(authorization));\n });\n\n const session: Interface[\"session\"] = (attachmentId) =>\n Effect.gen(function* () {\n const attachment = yield* storage.attachments.get(attachmentId);\n const connection = yield* storage.connections.get(attachment.connectionId);\n const authorization = yield* storage.authorizations.get(connection.authorizationId);\n const definition = yield* providers.get(authorization.provider);\n const stored = yield* Errors.decode(Target, attachment.target, \"target\");\n const target: Provider.Target = {\n ...stored,\n context: yield* Provider.decodeContext(definition, stored.context),\n };\n const built = yield* sessionFor(connection);\n // The zone must still be reachable by this credential: it may have moved account, been\n // deleted, or become a kind the provider cannot host records in.\n const reachable = yield* built.listTargets();\n if (!reachable.some((candidate) => candidate.zone === target.zone)) {\n return yield* Errors.fail(new Reason.NotFound({ entity: \"zone\", id: target.zone }));\n }\n return { session: built, target };\n });\n\n return {\n inspect,\n discover,\n start,\n complete,\n attach,\n detach: (attachmentId) => storage.attachments.remove(attachmentId),\n disconnect,\n refresh,\n session,\n };\n});\n\nexport const layer: Layer.Layer<\n Service,\n never,\n Storage.Service | Custody.Service | Providers.Service | Resolver.Service\n> = Layer.effect(Service)(make);\n\nconst accessor =\n <Args extends ReadonlyArray<unknown>, A>(\n pick: (service: Interface) => (...args: Args) => Fx<A>,\n ): ((...args: Args) => Effect.Effect<A, Errors.DomainKitError, Principal.Service | Service>) =>\n (...args) =>\n Effect.flatMap(Service, (service) => pick(service)(...args));\n\nexport const inspect = accessor((service) => service.inspect);\nexport const discover = accessor((service) => service.discover);\nexport const start = accessor((service) => service.start);\nexport const complete = accessor((service) => service.complete);\nexport const attach = accessor((service) => service.attach);\nexport const detach = accessor((service) => service.detach);\nexport const disconnect = accessor((service) => service.disconnect);\nexport const refresh = accessor((service) => service.refresh);\nexport const session = accessor((service) => service.session);\n"],"mappings":"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"}
1
+ {"version":3,"file":"Connect.mjs","names":["Errors.fail","Reason.InvalidInput","Storage.Service","Custody.Service","Providers.Service","Resolver.Service","Storage.Credential","Provider.decodeContext","Reason.Reconnect","Principal.Service","Reason.Forbidden","Storage.Authorization","Provider.encodeContext","DomainName.decode","Reason.NotFound","Storage.Attachment","candidates","DomainName.candidates","Provider.describeMethods","pkce","OAuth.pkce","Storage.Continuation","Errors.DomainKitError","Reason.Unauthenticated","Errors.decode"],"sources":["../src/Connect.ts"],"sourcesContent":["/**\n * Connect a provider, attach domains to it, and keep the credential alive. Interactive methods\n * (OAuth, integration) return a redirect and complete on callback; token methods connect in one\n * call. Reuse of an existing connection for a new domain is `attach`.\n */\nimport { Context, Data, DateTime, Duration, Effect, Layer, Option, Redacted, Schema } from \"effect\";\n\nimport * as Custody from \"./Custody.ts\";\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as DomainName from \"./DomainName.ts\";\nimport { fresh } from \"./internal/ids.ts\";\nimport * as OAuth from \"./internal/oauth.ts\";\nimport * as Principal from \"./Principal.ts\";\nimport * as Provider from \"./Provider.ts\";\nimport * as Providers from \"./Providers.ts\";\nimport * as Resolver from \"./Resolver.ts\";\nimport * as Storage from \"./Storage.ts\";\n\ntype Fx<A> = Effect.Effect<A, Errors.DomainKitError, Principal.Service>;\n\nexport type Method = Data.TaggedEnum<{\n /** Values keyed by the provider's declared `auth.token.fields`. */\n Token: { readonly values: Provider.TokenValues };\n OAuth: { readonly returnTo?: string };\n Integration: { readonly returnTo?: string };\n}>;\nconst MethodEnum = Data.taggedEnum<Method>();\n/** `token` accepts a lone string (`{ token }`) or a record; every value is wrapped in `Redacted`. */\nexport const Method = {\n ...MethodEnum,\n token: (\n values: string | Readonly<Record<string, Redacted.Redacted<string> | string | undefined>>,\n ): Method =>\n MethodEnum.Token({\n values:\n typeof values === \"string\"\n ? { token: Redacted.make(values) }\n : Object.fromEntries(\n Object.entries(values).map(([name, value]) => [\n name,\n typeof value === \"string\" ? Redacted.make(value) : value,\n ]),\n ),\n }),\n oauth: (options: { readonly returnTo?: string } = {}): Method => MethodEnum.OAuth(options),\n integration: (options: { readonly returnTo?: string } = {}): Method =>\n MethodEnum.Integration(options),\n} as const;\n\n/** Everything the UI needs about a domain's provider state. */\nexport interface Snapshot {\n readonly domain: string;\n readonly attachment: Storage.Attachment | null;\n readonly connection: Storage.Connection | null;\n readonly authorization: Pick<\n Storage.Authorization,\n \"id\" | \"provider\" | \"method\" | \"capabilities\" | \"revocation\"\n > | null;\n readonly lastReceiptId: string | null;\n /** How many domains this domain's connection serves, including this one; `0` without one. */\n readonly connectionDomains: number;\n /** Other connections of this owner that could serve this domain. */\n readonly reusable: ReadonlyArray<{\n readonly connection: Storage.Connection;\n readonly provider: string;\n readonly method: Storage.AuthMethod;\n }>;\n readonly providers: ReadonlyArray<{\n readonly id: string;\n readonly name: string;\n readonly methods: ReadonlyArray<Provider.MethodDescriptor>;\n }>;\n}\n\nexport type Started = Data.TaggedEnum<{\n Connected: {\n readonly connection: Storage.Connection;\n readonly attachment: Storage.Attachment | null;\n };\n Redirect: { readonly authorizationUrl: string; readonly continuationId: string };\n SelectionRequired: {\n readonly connection: Storage.Connection;\n readonly candidates: ReadonlyArray<Provider.Target>;\n };\n}>;\nexport const Started = Data.taggedEnum<Started>();\n\n/** `attach` either attached the domain or needs the caller to pick a zone. */\nexport type Selection = Data.TaggedEnum<{\n SelectionRequired: { readonly candidates: ReadonlyArray<Provider.Target> };\n}>;\nexport const Selection = Data.taggedEnum<Selection>();\n\nexport type Attached = Storage.Attachment | Selection;\n\nexport type Refreshed = \"current\" | \"refreshed\" | \"reconnect\";\n\n/** Which of the principal's connections serves a domain, from nameserver evidence and zone lists. */\nexport type Discovery = Data.TaggedEnum<{\n Resolved: { readonly connectionId: string; readonly target: Provider.Target };\n SelectionRequired: {\n readonly candidates: ReadonlyArray<{\n readonly connectionId: string;\n readonly target: Provider.Target;\n }>;\n };\n /**\n * No connection reaches the domain. `host` names the one registered provider whose declared\n * nameserver suffixes cover every authoritative nameserver; `null` when none or several do.\n */\n NotFound: {\n readonly nameservers: ReadonlyArray<string>;\n readonly host: { readonly provider: string } | null;\n };\n}>;\nexport const Discovery = Data.taggedEnum<Discovery>();\n\nexport interface Interface {\n readonly inspect: (domain: string) => Fx<Snapshot>;\n /**\n * Resolve the domain's authoritative nameservers, then match them against the zones each of\n * the principal's connections can reach: the closest zone wins, a decisive nameserver match\n * breaks ties, and anything else needs a selection.\n */\n readonly discover: (domain: string) => Fx<Discovery>;\n /** Connect a provider and, when `domain` is given, attach it. Token methods finish here. */\n readonly start: (input: {\n readonly provider: string;\n readonly method: Method;\n readonly domain?: string;\n readonly callbackUrl?: string;\n }) => Fx<Started>;\n /** Finish an interactive connection from the provider callback (the full callback URL). */\n readonly complete: (input: {\n readonly continuationId: string;\n readonly callbackUrl: string;\n }) => Fx<Started>;\n readonly attach: (input: {\n readonly connectionId: string;\n readonly domain: string;\n readonly target?: Provider.Target;\n }) => Fx<Attached>;\n /** Records stay in DNS; DomainKit forgets the domain. Use `Cleanup` first to remove records. */\n readonly detach: (attachmentId: string) => Fx<void>;\n /** Detach every domain and revoke the credential at the provider when the method supports it. */\n readonly disconnect: (connectionId: string) => Fx<void>;\n /** Refresh a near-expiry credential, single-flighted. Called automatically before provider use. */\n readonly refresh: (connectionId: string) => Fx<Refreshed>;\n /** Live provider session for an attachment; used by Provision, Cleanup, Verify. */\n readonly session: (\n attachmentId: string,\n ) => Fx<{ readonly session: Provider.Session; readonly target: Provider.Target }>;\n}\n\nexport class Service extends Context.Service<Service, Interface>()(\"@domainkit/Connect\") {}\n\n/** Which owner-scoped reuse is allowed. Default: any connection of the same owner may serve any of its domains. */\nexport interface PolicyShape {\n readonly allowReuse: (input: {\n readonly connection: Storage.Connection;\n readonly domain: string;\n }) => Effect.Effect<boolean, never, Principal.Service>;\n /** Refresh this long before expiry. Default 10 minutes. */\n readonly refreshBeforeMs: number;\n /** Continuation lifetime. Default 15 minutes. */\n readonly continuationTtlMs: number;\n}\nexport const defaults: PolicyShape = {\n allowReuse: () => Effect.succeed(true),\n refreshBeforeMs: 10 * 60_000,\n continuationTtlMs: 15 * 60_000,\n};\nexport class Policy extends Context.Reference<PolicyShape>(\"@domainkit/Connect/Policy\", {\n defaultValue: () => defaults,\n}) {}\n\n// ---------------------------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------------------------\n\nconst Payload = Schema.Struct({\n method: Schema.Literals([\"oauth\", \"integration\"]),\n codeVerifier: Schema.NullOr(Schema.String),\n callbackUrl: Schema.String,\n domain: Schema.NullOr(Schema.String),\n});\n\nconst Target = Schema.Struct({\n zone: Schema.String,\n context: Schema.Unknown,\n label: Schema.String,\n});\n\nconst invalid = (message: string, field?: string) =>\n Errors.fail(new Reason.InvalidInput({ message, ...(field === undefined ? {} : { field }) }));\n\n/** Nameserver hostnames compare case-insensitively and without a trailing dot. */\nconst normalize = (name: string) => name.toLowerCase().replace(/\\.$/, \"\");\n\nexport const make: Effect.Effect<\n Interface,\n never,\n Storage.Service | Custody.Service | Providers.Service | Resolver.Service\n> = Effect.gen(function* () {\n const storage = yield* Storage.Service;\n const custody = yield* Custody.Service;\n const providers = yield* Providers.Service;\n const resolver = yield* Resolver.Service;\n\n const seal = (issued: Provider.IssuedCredential) =>\n Effect.gen(function* () {\n const ciphertext = yield* custody.seal(issued.secret);\n return new Storage.Credential({\n ciphertext,\n expiresAt: issued.expiresAt,\n rotatedAt: yield* DateTime.now,\n });\n });\n\n /** Plaintext credential with the context decoded from its stored envelope. */\n const open = (authorization: Storage.Authorization): Fx<Provider.Credential> =>\n Effect.gen(function* () {\n const definition = yield* providers.get(authorization.provider);\n const row = yield* storage.authorizations.credential(authorization.id);\n const secret = yield* custody.open(row.ciphertext);\n const context = yield* Provider.decodeContext(definition, authorization.context);\n return { secret, context };\n });\n\n const refresherFor = (definition: Provider.Definition, method: Storage.AuthMethod) =>\n method === \"oauth\"\n ? definition.auth.oauth?.refresh\n : method === \"integration\"\n ? definition.auth.integration?.refresh\n : undefined;\n\n const revokerFor = (definition: Provider.Definition, method: Storage.AuthMethod) =>\n method === \"oauth\"\n ? definition.auth.oauth?.revoke\n : method === \"integration\"\n ? definition.auth.integration?.revoke\n : undefined;\n\n const revokeAt = (authorization: Storage.Authorization): Fx<void> =>\n Effect.gen(function* () {\n const definition = yield* providers.get(authorization.provider);\n const revoke = revokerFor(definition, authorization.method);\n if (revoke === undefined) return;\n const credential = yield* open(authorization);\n yield* revoke(credential);\n });\n\n /** Finish revocations a crash interrupted; best effort, never blocks the caller. */\n const recover = storage.authorizations.recoverRevocations(revokeAt).pipe(Effect.ignore);\n\n const refresh: Interface[\"refresh\"] = (connectionId) =>\n Effect.gen(function* () {\n const policy = yield* Policy;\n const connection = yield* storage.connections.get(connectionId);\n const authorization = yield* storage.authorizations.get(connection.authorizationId);\n const definition = yield* providers.get(authorization.provider);\n const refresher = refresherFor(definition, authorization.method);\n const dueAt = (row: Storage.Credential, now: DateTime.Utc) =>\n row.expiresAt !== null &&\n DateTime.toEpochMillis(row.expiresAt) - DateTime.toEpochMillis(now) <=\n policy.refreshBeforeMs;\n const expired = (row: Storage.Credential, now: DateTime.Utc) =>\n row.expiresAt !== null &&\n DateTime.toEpochMillis(row.expiresAt) <= DateTime.toEpochMillis(now);\n const current = yield* storage.authorizations.credential(authorization.id);\n const now = yield* DateTime.now;\n if (!dueAt(current, now)) return \"current\" as const;\n if (refresher === undefined)\n return expired(current, now) ? (\"reconnect\" as const) : (\"current\" as const);\n return yield* storage\n .withLock(\n `refresh:${authorization.id}`,\n Effect.gen(function* () {\n // Disconnect's two-phase revocation does not take this lock: re-read the row here\n // so a revocation that started meanwhile skips the refresh instead of racing it.\n const live = yield* storage.authorizations.get(authorization.id).pipe(\n Effect.map(Option.some),\n Effect.catchIf(\n (error) => error.reason._tag === \"NotFound\",\n () => Effect.succeed(Option.none<Storage.Authorization>()),\n ),\n );\n if (Option.isNone(live) || live.value.revocation !== \"active\") {\n return \"reconnect\" as const;\n }\n const latest = yield* storage.authorizations.credential(authorization.id);\n if (!dueAt(latest, yield* DateTime.now)) return \"current\" as const;\n const secret = yield* custody.open(latest.ciphertext);\n const context = yield* Provider.decodeContext(definition, live.value.context);\n const issued = yield* refresher({ secret, context });\n // A revocation that completed while the provider was issuing leaves no row to\n // rotate; revoke the fresh credential so it does not stay live without a home.\n yield* storage.authorizations\n .rotate(authorization.id, yield* seal(issued))\n .pipe(\n Effect.tapError((error) =>\n error.reason._tag === \"NotFound\"\n ? (revokerFor(definition, authorization.method)?.(issued) ?? Effect.void).pipe(\n Effect.ignore,\n )\n : Effect.void,\n ),\n );\n return \"refreshed\" as const;\n }),\n )\n .pipe(\n Effect.catchIf(\n (error) => error.reason._tag === \"Busy\" && !expired(current, now),\n () => Effect.succeed(\"current\" as const),\n ),\n Effect.catchIf(\n (error) => error.reason._tag === \"Unauthenticated\",\n () => Effect.succeed(\"reconnect\" as const),\n ),\n );\n });\n\n const sessionFor = (connection: Storage.Connection): Fx<Provider.Session> =>\n Effect.gen(function* () {\n const authorization = yield* storage.authorizations.get(connection.authorizationId);\n const reconnect = Errors.fail(\n new Reason.Reconnect({\n provider: authorization.provider,\n connectionId: connection.id,\n }),\n );\n if (authorization.revocation !== \"active\") return yield* reconnect;\n if ((yield* refresh(connection.id)) === \"reconnect\") return yield* reconnect;\n const definition = yield* providers.get(authorization.provider);\n return definition.session(yield* open(authorization));\n });\n\n const connectWith = (input: {\n readonly definition: Provider.Definition;\n readonly method: Storage.AuthMethod;\n readonly issued: Provider.IssuedCredential;\n readonly requiredCapabilities: ReadonlyArray<Storage.Capability>;\n }): Fx<{ readonly connection: Storage.Connection; readonly session: Provider.Session }> =>\n Effect.gen(function* () {\n const principal = yield* Principal.Service;\n const session = input.definition.session(input.issued);\n const held = yield* session.capabilities();\n const missing = input.requiredCapabilities.filter((capability) => !held.includes(capability));\n if (missing.length > 0) {\n return yield* Errors.fail(\n new Reason.Forbidden({\n message: `${input.definition.name} credential lacks ${missing.join(\", \")}`,\n }),\n );\n }\n const authorization = new Storage.Authorization({\n id: yield* fresh(\"auth\"),\n ownerId: principal.ownerId,\n provider: input.definition.id,\n method: input.method,\n capabilities: held,\n context: yield* Provider.encodeContext(input.definition, input.issued.context),\n revocation: \"active\",\n createdBy: principal.actorId,\n createdAt: yield* DateTime.now,\n });\n yield* storage.authorizations.upsert({\n authorization,\n credential: yield* seal(input.issued),\n });\n const connection = yield* storage.connections.create(authorization.id);\n return { connection, session };\n });\n\n const attachWith = (input: {\n readonly definition: Provider.Definition;\n readonly connection: Storage.Connection;\n readonly session: Provider.Session;\n readonly domain: string;\n readonly target?: Provider.Target;\n }): Fx<Attached> =>\n Effect.gen(function* () {\n const domain = yield* DomainName.decode(input.domain);\n const existing = yield* storage.attachments.byDomain(domain);\n if (Option.isSome(existing)) {\n if (existing.value.connectionId === input.connection.id) return existing.value;\n return yield* invalid(`${domain} is already attached to another connection`, \"domain\");\n }\n const resolution: Provider.Resolution =\n input.target === undefined\n ? yield* input.session.resolveTarget(domain)\n : Provider.Resolution.Resolved({ target: input.target });\n switch (resolution._tag) {\n case \"SelectionRequired\":\n return Selection.SelectionRequired({ candidates: resolution.candidates });\n case \"NotFound\":\n return yield* Errors.fail(new Reason.NotFound({ entity: \"zone\", id: domain }));\n case \"Resolved\":\n return yield* storage.attachments.create({\n connectionId: input.connection.id,\n domain,\n zone: resolution.target.zone,\n target: {\n ...resolution.target,\n context: yield* Provider.encodeContext(input.definition, resolution.target.context),\n },\n });\n }\n });\n\n const started = (connection: Storage.Connection, attached: Attached | null): Started =>\n attached === null\n ? Started.Connected({ connection, attachment: null })\n : attached instanceof Storage.Attachment\n ? Started.Connected({ connection, attachment: attached })\n : Started.SelectionRequired({ connection, candidates: attached.candidates });\n\n /** Authoritative nameservers for the closest zone that answers an NS query. */\n const authoritative = (candidates: ReadonlyArray<DomainName.Model>) =>\n Effect.gen(function* () {\n for (const zone of candidates) {\n const outcomes = yield* resolver.resolve(zone, \"NS\");\n const names = outcomes.flatMap((outcome) =>\n outcome._tag === \"Answered\"\n ? outcome.answer.records.flatMap((record) =>\n record._tag === \"NS\" ? [record.nameserver] : [],\n )\n : [],\n );\n if (names.length > 0) return [...new Set(names)].sort();\n }\n return [] as ReadonlyArray<string>;\n });\n\n const discover: Interface[\"discover\"] = (input) =>\n Effect.gen(function* () {\n const domain = yield* DomainName.decode(input);\n const candidates = DomainName.candidates(domain);\n const nameservers = yield* authoritative(candidates);\n const reachable: Array<{\n readonly connectionId: string;\n readonly target: Provider.Target;\n }> = [];\n for (const connection of yield* storage.connections.list()) {\n const targets = yield* sessionFor(connection).pipe(\n Effect.flatMap((session) => session.listTargets()),\n Effect.catch(() => Effect.succeed([] as ReadonlyArray<Provider.Target>)),\n );\n for (const target of targets) {\n if (candidates.includes(target.zone as DomainName.Model)) {\n reachable.push({ connectionId: connection.id, target });\n }\n }\n }\n for (const zone of candidates) {\n const matches = reachable\n .filter(({ target }) => target.zone === zone)\n .sort((left, right) => left.connectionId.localeCompare(right.connectionId));\n if (matches.length === 0) continue;\n const [only] = matches;\n if (matches.length === 1 && only !== undefined) return Discovery.Resolved(only);\n const decisive = matches.filter(({ target }) => {\n const configured = new Set((target.nameservers ?? []).map(normalize));\n return nameservers.length > 0 && nameservers.every((name) => configured.has(name));\n });\n const [winner] = decisive;\n if (decisive.length === 1 && winner !== undefined) return Discovery.Resolved(winner);\n return Discovery.SelectionRequired({ candidates: matches });\n }\n return Discovery.NotFound({ nameservers, host: hostOf(nameservers) });\n });\n\n /** The one registered provider whose declared suffixes cover every nameserver, else `null`. */\n const hostOf = (nameservers: ReadonlyArray<string>): { readonly provider: string } | null => {\n if (nameservers.length === 0) return null;\n const names = nameservers.map(normalize);\n const hosts = providers.list().filter((definition) => {\n const suffixes = (definition.nameservers ?? []).map(normalize);\n return (\n suffixes.length > 0 &&\n names.every((name) =>\n suffixes.some((suffix) => name === suffix || name.endsWith(`.${suffix}`)),\n )\n );\n });\n const [host] = hosts;\n return hosts.length === 1 && host !== undefined ? { provider: host.id } : null;\n };\n\n const inspect: Interface[\"inspect\"] = (input) =>\n Effect.gen(function* () {\n const domain = yield* DomainName.decode(input);\n const policy = yield* Policy;\n const attachment = Option.getOrNull(yield* storage.attachments.byDomain(domain));\n const connection =\n attachment === null ? null : yield* storage.connections.get(attachment.connectionId);\n const authorization =\n connection === null ? null : yield* storage.authorizations.get(connection.authorizationId);\n const latest =\n attachment === null\n ? Option.none()\n : yield* storage.attempts.latest(attachment.id, \"provisioning\");\n // Letting a connection go takes every domain on it, so the UI has to be able to say how many.\n const attached = connection === null ? [] : yield* storage.attachments.list(connection.id);\n const reusable: Array<Snapshot[\"reusable\"][number]> = [];\n for (const candidate of yield* storage.connections.list()) {\n if (candidate.id === connection?.id) continue;\n if (!(yield* policy.allowReuse({ connection: candidate, domain }))) continue;\n const candidateAuthorization = yield* storage.authorizations.get(candidate.authorizationId);\n if (candidateAuthorization.revocation !== \"active\") continue;\n reusable.push({\n connection: candidate,\n provider: candidateAuthorization.provider,\n method: candidateAuthorization.method,\n });\n }\n return {\n domain,\n attachment,\n connection,\n authorization:\n authorization === null\n ? null\n : {\n id: authorization.id,\n provider: authorization.provider,\n method: authorization.method,\n capabilities: authorization.capabilities,\n revocation: authorization.revocation,\n },\n lastReceiptId: Option.isSome(latest) ? (latest.value.receipt?.id ?? null) : null,\n connectionDomains: attached.length,\n reusable,\n providers: providers.list().map((definition) => ({\n id: definition.id,\n name: definition.name,\n methods: Provider.describeMethods(definition),\n })),\n };\n });\n\n const start: Interface[\"start\"] = (input) =>\n Effect.gen(function* () {\n yield* recover;\n const principal = yield* Principal.Service;\n const policy = yield* Policy;\n const definition = yield* providers.get(input.provider);\n if (input.domain !== undefined) yield* DomainName.decode(input.domain);\n switch (input.method._tag) {\n case \"Token\": {\n const token = definition.auth.token;\n if (token === undefined) {\n return yield* invalid(`${definition.name} does not accept tokens`, \"method\");\n }\n const issued = yield* token.authenticate(input.method.values);\n const { connection, session } = yield* connectWith({\n definition,\n method: \"token\",\n issued,\n requiredCapabilities: token.requiredCapabilities,\n });\n const attached =\n input.domain === undefined\n ? null\n : yield* attachWith({ definition, connection, session, domain: input.domain });\n return started(connection, attached);\n }\n case \"OAuth\":\n case \"Integration\": {\n const interactive =\n input.method._tag === \"OAuth\" ? definition.auth.oauth : definition.auth.integration;\n if (interactive === undefined) {\n return yield* invalid(\n `${definition.name} does not offer ${input.method._tag.toLowerCase()} connections`,\n \"method\",\n );\n }\n if (input.callbackUrl === undefined) {\n return yield* invalid(\"Interactive connections need a callbackUrl\", \"callbackUrl\");\n }\n const continuationId = yield* fresh(\"cont\");\n const pkce = input.method._tag === \"OAuth\" ? yield* OAuth.pkce() : null;\n const redirect =\n input.method._tag === \"OAuth\"\n ? yield* (interactive as Provider.OAuthAuth).start({\n state: continuationId,\n callbackUrl: input.callbackUrl,\n codeChallenge: pkce?.codeChallenge ?? \"\",\n })\n : yield* (interactive as Provider.IntegrationAuth).start({\n state: continuationId,\n callbackUrl: input.callbackUrl,\n });\n yield* storage.continuations.put(\n new Storage.Continuation({\n id: continuationId,\n ownerId: principal.ownerId,\n actorId: principal.actorId,\n provider: definition.id,\n payload: Schema.encodeSync(Payload)({\n method: input.method._tag === \"OAuth\" ? \"oauth\" : \"integration\",\n codeVerifier: pkce?.codeVerifier ?? null,\n callbackUrl: input.callbackUrl,\n domain: input.domain ?? null,\n }),\n returnTo: input.method.returnTo ?? null,\n expiresAt: DateTime.addDuration(\n yield* DateTime.now,\n Duration.millis(policy.continuationTtlMs),\n ),\n }),\n );\n return Started.Redirect({\n authorizationUrl: redirect.authorizationUrl,\n continuationId,\n });\n }\n }\n });\n\n const complete: Interface[\"complete\"] = (input) =>\n Effect.gen(function* () {\n // The continuation is spent only after the credential and connection are persisted, so a\n // failure before that (bad callback, provider outage, storage outage) leaves the flow\n // retryable. Concurrent callbacks for the same continuation serialize on a lock; the\n // loser sees Busy or NotFound. A provider code is single-use, so a retry after the\n // provider already redeemed it fails Unauthenticated and the customer starts over.\n const callback = yield* Effect.try({\n try: () => new URL(input.callbackUrl),\n catch: () =>\n new Errors.DomainKitError({\n reason: new Reason.InvalidInput({\n message: \"callbackUrl is not a URL\",\n field: \"callbackUrl\",\n }),\n }),\n });\n const params = Object.fromEntries(callback.searchParams);\n const unauthenticated = (message: string) =>\n Errors.fail(new Reason.Unauthenticated({ message }));\n if (params.error !== undefined) {\n return yield* unauthenticated(`Provider returned ${params.error}`);\n }\n if (params.state !== input.continuationId) {\n return yield* unauthenticated(\"Callback state does not match the continuation\");\n }\n const code = params.code;\n if (code === undefined) return yield* unauthenticated(\"Callback has no code\");\n return yield* storage.withLock(\n `continuation:${input.continuationId}`,\n Effect.gen(function* () {\n const continuation = yield* storage.continuations.get(input.continuationId);\n const payload = yield* Errors.decode(Payload, continuation.payload, \"continuation\");\n const definition = yield* providers.get(continuation.provider);\n const issued =\n payload.method === \"oauth\"\n ? yield* Effect.gen(function* () {\n const oauth = definition.auth.oauth;\n if (oauth === undefined || payload.codeVerifier === null) {\n return yield* invalid(\n `${definition.name} does not offer oauth connections`,\n \"method\",\n );\n }\n return yield* oauth.complete({\n code,\n callbackUrl: payload.callbackUrl,\n codeVerifier: payload.codeVerifier,\n params,\n });\n })\n : yield* Effect.gen(function* () {\n const integration = definition.auth.integration;\n if (integration === undefined) {\n return yield* invalid(\n `${definition.name} does not offer integration connections`,\n \"method\",\n );\n }\n return yield* integration.complete({\n code,\n callbackUrl: payload.callbackUrl,\n params,\n });\n });\n const { connection, session } = yield* connectWith({\n definition,\n method: payload.method,\n issued,\n requiredCapabilities: [],\n });\n const attached =\n payload.domain === null\n ? null\n : yield* attachWith({ definition, connection, session, domain: payload.domain });\n // The connection is durable at this point. Whatever happens to the continuation\n // now (expired, already spent, or a storage failure while spending it) no longer\n // changes the outcome: the row expires on its own and a replayed callback fails at\n // the provider, so the persisted result is returned.\n yield* storage.continuations.consume(input.continuationId).pipe(Effect.ignore);\n return started(connection, attached);\n }),\n );\n });\n\n const attach: Interface[\"attach\"] = (input) =>\n Effect.gen(function* () {\n const connection = yield* storage.connections.get(input.connectionId);\n const authorization = yield* storage.authorizations.get(connection.authorizationId);\n const definition = yield* providers.get(authorization.provider);\n const session = yield* sessionFor(connection);\n return yield* attachWith({\n definition,\n connection,\n session,\n domain: input.domain,\n ...(input.target === undefined ? {} : { target: input.target }),\n });\n });\n\n const disconnect: Interface[\"disconnect\"] = (connectionId) =>\n Effect.gen(function* () {\n yield* recover;\n const connection = yield* storage.connections.get(connectionId);\n const attachments = yield* storage.attachments.list(connectionId);\n yield* Effect.forEach(attachments, (attachment) => storage.attachments.remove(attachment.id));\n yield* storage.connections.remove(connectionId);\n const remaining = (yield* storage.connections.list()).some(\n (candidate) => candidate.authorizationId === connection.authorizationId,\n );\n if (remaining) return;\n const authorization = yield* storage.authorizations.get(connection.authorizationId);\n yield* storage.authorizations.revoke(authorization.id, revokeAt(authorization));\n });\n\n const session: Interface[\"session\"] = (attachmentId) =>\n Effect.gen(function* () {\n const attachment = yield* storage.attachments.get(attachmentId);\n const connection = yield* storage.connections.get(attachment.connectionId);\n const authorization = yield* storage.authorizations.get(connection.authorizationId);\n const definition = yield* providers.get(authorization.provider);\n const stored = yield* Errors.decode(Target, attachment.target, \"target\");\n const target: Provider.Target = {\n ...stored,\n context: yield* Provider.decodeContext(definition, stored.context),\n };\n const built = yield* sessionFor(connection);\n // The zone must still be reachable by this credential: it may have moved account, been\n // deleted, or become a kind the provider cannot host records in.\n const reachable = yield* built.listTargets();\n if (!reachable.some((candidate) => candidate.zone === target.zone)) {\n return yield* Errors.fail(new Reason.NotFound({ entity: \"zone\", id: target.zone }));\n }\n return { session: built, target };\n });\n\n return {\n inspect,\n discover,\n start,\n complete,\n attach,\n detach: (attachmentId) => storage.attachments.remove(attachmentId),\n disconnect,\n refresh,\n session,\n };\n});\n\nexport const layer: Layer.Layer<\n Service,\n never,\n Storage.Service | Custody.Service | Providers.Service | Resolver.Service\n> = Layer.effect(Service)(make);\n\nconst accessor =\n <Args extends ReadonlyArray<unknown>, A>(\n pick: (service: Interface) => (...args: Args) => Fx<A>,\n ): ((...args: Args) => Effect.Effect<A, Errors.DomainKitError, Principal.Service | Service>) =>\n (...args) =>\n Effect.flatMap(Service, (service) => pick(service)(...args));\n\nexport const inspect = accessor((service) => service.inspect);\nexport const discover = accessor((service) => service.discover);\nexport const start = accessor((service) => service.start);\nexport const complete = accessor((service) => service.complete);\nexport const attach = accessor((service) => service.attach);\nexport const detach = accessor((service) => service.detach);\nexport const disconnect = accessor((service) => service.disconnect);\nexport const refresh = accessor((service) => service.refresh);\nexport const session = accessor((service) => service.session);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,MAAM,aAAa,KAAK,WAAmB;;AAE3C,MAAa,SAAS;CACpB,GAAG;CACH,QACE,WAEA,WAAW,MAAM,EACf,QACE,OAAO,WAAW,WACd,EAAE,OAAO,SAAS,KAAK,MAAM,EAAE,IAC/B,OAAO,YACL,OAAO,QAAQ,MAAM,CAAC,CAAC,KAAK,CAAC,MAAM,WAAW,CAC5C,MACA,OAAO,UAAU,WAAW,SAAS,KAAK,KAAK,IAAI,KACrD,CAAC,CACH,EACR,CAAC;CACH,QAAQ,UAA0C,CAAC,MAAc,WAAW,MAAM,OAAO;CACzF,cAAc,UAA0C,CAAC,MACvD,WAAW,YAAY,OAAO;AAClC;AAsCA,MAAa,UAAU,KAAK,WAAoB;AAMhD,MAAa,YAAY,KAAK,WAAsB;AAwBpD,MAAa,YAAY,KAAK,WAAsB;AAuCpD,IAAa,UAAb,cAA6B,QAAQ,QAA4B,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC;AAa1F,MAAa,WAAwB;CACnC,kBAAkB,OAAO,QAAQ,IAAI;CACrC,iBAAiB;CACjB,mBAAmB;AACrB;AACA,IAAa,SAAb,cAA4B,QAAQ,UAAuB,6BAA6B,EACtF,oBAAoB,SACtB,CAAC,CAAC,CAAC,CAAC;AAMJ,MAAM,UAAU,OAAO,OAAO;CAC5B,QAAQ,OAAO,SAAS,CAAC,SAAS,aAAa,CAAC;CAChD,cAAc,OAAO,OAAO,OAAO,MAAM;CACzC,aAAa,OAAO;CACpB,QAAQ,OAAO,OAAO,OAAO,MAAM;AACrC,CAAC;AAED,MAAM,SAAS,OAAO,OAAO;CAC3B,MAAM,OAAO;CACb,SAAS,OAAO;CAChB,OAAO,OAAO;AAChB,CAAC;AAED,MAAM,WAAW,SAAiB,UAChCA,KAAY,IAAIC,aAAoB;CAAE;CAAS,GAAI,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM;AAAG,CAAC,CAAC;;AAG7F,MAAM,aAAa,SAAiB,KAAK,YAAY,CAAC,CAAC,QAAQ,OAAO,EAAE;AAExE,MAAa,OAIT,OAAO,IAAI,aAAa;CAC1B,MAAM,UAAU,OAAOC;CACvB,MAAM,UAAU,OAAOC;CACvB,MAAM,YAAY,OAAOC;CACzB,MAAM,WAAW,OAAOC;CAExB,MAAM,QAAQ,WACZ,OAAO,IAAI,aAAa;EACtB,MAAM,aAAa,OAAO,QAAQ,KAAK,OAAO,MAAM;EACpD,OAAO,IAAIC,WAAmB;GAC5B;GACA,WAAW,OAAO;GAClB,WAAW,OAAO,SAAS;EAC7B,CAAC;CACH,CAAC;;CAGH,MAAM,QAAQ,kBACZ,OAAO,IAAI,aAAa;EACtB,MAAM,aAAa,OAAO,UAAU,IAAI,cAAc,QAAQ;EAC9D,MAAM,MAAM,OAAO,QAAQ,eAAe,WAAW,cAAc,EAAE;EAGrE,OAAO;GAAE,QAAA,OAFa,QAAQ,KAAK,IAAI,UAAU;GAEhC,SAAA,OADMC,cAAuB,YAAY,cAAc,OAAO;EACtD;CAC3B,CAAC;CAEH,MAAM,gBAAgB,YAAiC,WACrD,WAAW,UACP,WAAW,KAAK,OAAO,UACvB,WAAW,gBACT,WAAW,KAAK,aAAa,UAC7B,KAAA;CAER,MAAM,cAAc,YAAiC,WACnD,WAAW,UACP,WAAW,KAAK,OAAO,SACvB,WAAW,gBACT,WAAW,KAAK,aAAa,SAC7B,KAAA;CAER,MAAM,YAAY,kBAChB,OAAO,IAAI,aAAa;EACtB,MAAM,aAAa,OAAO,UAAU,IAAI,cAAc,QAAQ;EAC9D,MAAM,SAAS,WAAW,YAAY,cAAc,MAAM;EAC1D,IAAI,WAAW,KAAA,GAAW;EAE1B,OAAO,OAAO,OADY,KAAK,aAAa,CACpB;CAC1B,CAAC;;CAGH,MAAM,UAAU,QAAQ,eAAe,mBAAmB,QAAQ,CAAC,CAAC,KAAK,OAAO,MAAM;CAEtF,MAAM,WAAiC,iBACrC,OAAO,IAAI,aAAa;EACtB,MAAM,SAAS,OAAO;EACtB,MAAM,aAAa,OAAO,QAAQ,YAAY,IAAI,YAAY;EAC9D,MAAM,gBAAgB,OAAO,QAAQ,eAAe,IAAI,WAAW,eAAe;EAClF,MAAM,aAAa,OAAO,UAAU,IAAI,cAAc,QAAQ;EAC9D,MAAM,YAAY,aAAa,YAAY,cAAc,MAAM;EAC/D,MAAM,SAAS,KAAyB,QACtC,IAAI,cAAc,QAClB,SAAS,cAAc,IAAI,SAAS,IAAI,SAAS,cAAc,GAAG,KAChE,OAAO;EACX,MAAM,WAAW,KAAyB,QACxC,IAAI,cAAc,QAClB,SAAS,cAAc,IAAI,SAAS,KAAK,SAAS,cAAc,GAAG;EACrE,MAAM,UAAU,OAAO,QAAQ,eAAe,WAAW,cAAc,EAAE;EACzE,MAAM,MAAM,OAAO,SAAS;EAC5B,IAAI,CAAC,MAAM,SAAS,GAAG,GAAG,OAAO;EACjC,IAAI,cAAc,KAAA,GAChB,OAAO,QAAQ,SAAS,GAAG,IAAK,cAAyB;EAC3D,OAAO,OAAO,QACX,SACC,WAAW,cAAc,MACzB,OAAO,IAAI,aAAa;GAGtB,MAAM,OAAO,OAAO,QAAQ,eAAe,IAAI,cAAc,EAAE,CAAC,CAAC,KAC/D,OAAO,IAAI,OAAO,IAAI,GACtB,OAAO,SACJ,UAAU,MAAM,OAAO,SAAS,kBAC3B,OAAO,QAAQ,OAAO,KAA4B,CAAC,CAC3D,CACF;GACA,IAAI,OAAO,OAAO,IAAI,KAAK,KAAK,MAAM,eAAe,UACnD,OAAO;GAET,MAAM,SAAS,OAAO,QAAQ,eAAe,WAAW,cAAc,EAAE;GACxE,IAAI,CAAC,MAAM,QAAQ,OAAO,SAAS,GAAG,GAAG,OAAO;GAChD,MAAM,SAAS,OAAO,QAAQ,KAAK,OAAO,UAAU;GACpD,MAAM,UAAU,OAAOA,cAAuB,YAAY,KAAK,MAAM,OAAO;GAC5E,MAAM,SAAS,OAAO,UAAU;IAAE;IAAQ;GAAQ,CAAC;GAGnD,OAAO,QAAQ,eACZ,OAAO,cAAc,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,CAC7C,KACC,OAAO,UAAU,UACf,MAAM,OAAO,SAAS,cACjB,WAAW,YAAY,cAAc,MAAM,CAAC,GAAG,MAAM,KAAK,OAAO,KAAA,CAAM,KACtE,OAAO,MACT,IACA,OAAO,IACb,CACF;GACF,OAAO;EACT,CAAC,CACH,CAAC,CACA,KACC,OAAO,SACJ,UAAU,MAAM,OAAO,SAAS,UAAU,CAAC,QAAQ,SAAS,GAAG,SAC1D,OAAO,QAAQ,SAAkB,CACzC,GACA,OAAO,SACJ,UAAU,MAAM,OAAO,SAAS,yBAC3B,OAAO,QAAQ,WAAoB,CAC3C,CACF;CACJ,CAAC;CAEH,MAAM,cAAc,eAClB,OAAO,IAAI,aAAa;EACtB,MAAM,gBAAgB,OAAO,QAAQ,eAAe,IAAI,WAAW,eAAe;EAClF,MAAM,YAAYP,KAChB,IAAIQ,UAAiB;GACnB,UAAU,cAAc;GACxB,cAAc,WAAW;EAC3B,CAAC,CACH;EACA,IAAI,cAAc,eAAe,UAAU,OAAO,OAAO;EACzD,KAAK,OAAO,QAAQ,WAAW,EAAE,OAAO,aAAa,OAAO,OAAO;EAEnE,QAAO,OADmB,UAAU,IAAI,cAAc,QAAQ,EAAA,CAC5C,QAAQ,OAAO,KAAK,aAAa,CAAC;CACtD,CAAC;CAEH,MAAM,eAAe,UAMnB,OAAO,IAAI,aAAa;EACtB,MAAM,YAAY,OAAOC;EACzB,MAAM,UAAU,MAAM,WAAW,QAAQ,MAAM,MAAM;EACrD,MAAM,OAAO,OAAO,QAAQ,aAAa;EACzC,MAAM,UAAU,MAAM,qBAAqB,QAAQ,eAAe,CAAC,KAAK,SAAS,UAAU,CAAC;EAC5F,IAAI,QAAQ,SAAS,GACnB,OAAO,OAAOT,KACZ,IAAIU,UAAiB,EACnB,SAAS,GAAG,MAAM,WAAW,KAAK,oBAAoB,QAAQ,KAAK,IAAI,IACzE,CAAC,CACH;EAEF,MAAM,gBAAgB,IAAIC,cAAsB;GAC9C,IAAI,OAAO,MAAM,MAAM;GACvB,SAAS,UAAU;GACnB,UAAU,MAAM,WAAW;GAC3B,QAAQ,MAAM;GACd,cAAc;GACd,SAAS,OAAOC,cAAuB,MAAM,YAAY,MAAM,OAAO,OAAO;GAC7E,YAAY;GACZ,WAAW,UAAU;GACrB,WAAW,OAAO,SAAS;EAC7B,CAAC;EACD,OAAO,QAAQ,eAAe,OAAO;GACnC;GACA,YAAY,OAAO,KAAK,MAAM,MAAM;EACtC,CAAC;EAED,OAAO;GAAE,YAAA,OADiB,QAAQ,YAAY,OAAO,cAAc,EAAE;GAChD;EAAQ;CAC/B,CAAC;CAEH,MAAM,cAAc,UAOlB,OAAO,IAAI,aAAa;EACtB,MAAM,SAAS,OAAOC,OAAkB,MAAM,MAAM;EACpD,MAAM,WAAW,OAAO,QAAQ,YAAY,SAAS,MAAM;EAC3D,IAAI,OAAO,OAAO,QAAQ,GAAG;GAC3B,IAAI,SAAS,MAAM,iBAAiB,MAAM,WAAW,IAAI,OAAO,SAAS;GACzE,OAAO,OAAO,QAAQ,GAAG,OAAO,6CAA6C,QAAQ;EACvF;EACA,MAAM,aACJ,MAAM,WAAW,KAAA,IACb,OAAO,MAAM,QAAQ,cAAc,MAAM,IAAA,WACrB,SAAS,EAAE,QAAQ,MAAM,OAAO,CAAC;EAC3D,QAAQ,WAAW,MAAnB;GACE,KAAK,qBACH,OAAO,UAAU,kBAAkB,EAAE,YAAY,WAAW,WAAW,CAAC;GAC1E,KAAK,YACH,OAAO,OAAOb,KAAY,IAAIc,SAAgB;IAAE,QAAQ;IAAQ,IAAI;GAAO,CAAC,CAAC;GAC/E,KAAK,YACH,OAAO,OAAO,QAAQ,YAAY,OAAO;IACvC,cAAc,MAAM,WAAW;IAC/B;IACA,MAAM,WAAW,OAAO;IACxB,QAAQ;KACN,GAAG,WAAW;KACd,SAAS,OAAOF,cAAuB,MAAM,YAAY,WAAW,OAAO,OAAO;IACpF;GACF,CAAC;EACL;CACF,CAAC;CAEH,MAAM,WAAW,YAAgC,aAC/C,aAAa,OACT,QAAQ,UAAU;EAAE;EAAY,YAAY;CAAK,CAAC,IAClD,oBAAoBG,aAClB,QAAQ,UAAU;EAAE;EAAY,YAAY;CAAS,CAAC,IACtD,QAAQ,kBAAkB;EAAE;EAAY,YAAY,SAAS;CAAW,CAAC;;CAGjF,MAAM,iBAAiB,eACrB,OAAO,IAAI,aAAa;EACtB,KAAK,MAAM,QAAQ,YAAY;GAE7B,MAAM,SAAQ,OADU,SAAS,QAAQ,MAAM,IAAI,EAAA,CAC5B,SAAS,YAC9B,QAAQ,SAAS,aACb,QAAQ,OAAO,QAAQ,SAAS,WAC9B,OAAO,SAAS,OAAO,CAAC,OAAO,UAAU,IAAI,CAAC,CAChD,IACA,CAAC,CACP;GACA,IAAI,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK;EACxD;EACA,OAAO,CAAC;CACV,CAAC;CAEH,MAAM,YAAmC,UACvC,OAAO,IAAI,aAAa;EACtB,MAAM,SAAS,OAAOF,OAAkB,KAAK;EAC7C,MAAMG,eAAaC,WAAsB,MAAM;EAC/C,MAAM,cAAc,OAAO,cAAcD,YAAU;EACnD,MAAM,YAGD,CAAC;EACN,KAAK,MAAM,cAAc,OAAO,QAAQ,YAAY,KAAK,GAAG;GAC1D,MAAM,UAAU,OAAO,WAAW,UAAU,CAAC,CAAC,KAC5C,OAAO,SAAS,YAAY,QAAQ,YAAY,CAAC,GACjD,OAAO,YAAY,OAAO,QAAQ,CAAC,CAAmC,CAAC,CACzE;GACA,KAAK,MAAM,UAAU,SACnB,IAAIA,aAAW,SAAS,OAAO,IAAwB,GACrD,UAAU,KAAK;IAAE,cAAc,WAAW;IAAI;GAAO,CAAC;EAG5D;EACA,KAAK,MAAM,QAAQA,cAAY;GAC7B,MAAM,UAAU,UACb,QAAQ,EAAE,aAAa,OAAO,SAAS,IAAI,CAAC,CAC5C,MAAM,MAAM,UAAU,KAAK,aAAa,cAAc,MAAM,YAAY,CAAC;GAC5E,IAAI,QAAQ,WAAW,GAAG;GAC1B,MAAM,CAAC,QAAQ;GACf,IAAI,QAAQ,WAAW,KAAK,SAAS,KAAA,GAAW,OAAO,UAAU,SAAS,IAAI;GAC9E,MAAM,WAAW,QAAQ,QAAQ,EAAE,aAAa;IAC9C,MAAM,aAAa,IAAI,KAAK,OAAO,eAAe,CAAC,EAAA,CAAG,IAAI,SAAS,CAAC;IACpE,OAAO,YAAY,SAAS,KAAK,YAAY,OAAO,SAAS,WAAW,IAAI,IAAI,CAAC;GACnF,CAAC;GACD,MAAM,CAAC,UAAU;GACjB,IAAI,SAAS,WAAW,KAAK,WAAW,KAAA,GAAW,OAAO,UAAU,SAAS,MAAM;GACnF,OAAO,UAAU,kBAAkB,EAAE,YAAY,QAAQ,CAAC;EAC5D;EACA,OAAO,UAAU,SAAS;GAAE;GAAa,MAAM,OAAO,WAAW;EAAE,CAAC;CACtE,CAAC;;CAGH,MAAM,UAAU,gBAA6E;EAC3F,IAAI,YAAY,WAAW,GAAG,OAAO;EACrC,MAAM,QAAQ,YAAY,IAAI,SAAS;EACvC,MAAM,QAAQ,UAAU,KAAK,CAAC,CAAC,QAAQ,eAAe;GACpD,MAAM,YAAY,WAAW,eAAe,CAAC,EAAA,CAAG,IAAI,SAAS;GAC7D,OACE,SAAS,SAAS,KAClB,MAAM,OAAO,SACX,SAAS,MAAM,WAAW,SAAS,UAAU,KAAK,SAAS,IAAI,QAAQ,CAAC,CAC1E;EAEJ,CAAC;EACD,MAAM,CAAC,QAAQ;EACf,OAAO,MAAM,WAAW,KAAK,SAAS,KAAA,IAAY,EAAE,UAAU,KAAK,GAAG,IAAI;CAC5E;CAEA,MAAM,WAAiC,UACrC,OAAO,IAAI,aAAa;EACtB,MAAM,SAAS,OAAOH,OAAkB,KAAK;EAC7C,MAAM,SAAS,OAAO;EACtB,MAAM,aAAa,OAAO,UAAU,OAAO,QAAQ,YAAY,SAAS,MAAM,CAAC;EAC/E,MAAM,aACJ,eAAe,OAAO,OAAO,OAAO,QAAQ,YAAY,IAAI,WAAW,YAAY;EACrF,MAAM,gBACJ,eAAe,OAAO,OAAO,OAAO,QAAQ,eAAe,IAAI,WAAW,eAAe;EAC3F,MAAM,SACJ,eAAe,OACX,OAAO,KAAK,IACZ,OAAO,QAAQ,SAAS,OAAO,WAAW,IAAI,cAAc;EAElE,MAAM,WAAW,eAAe,OAAO,CAAC,IAAI,OAAO,QAAQ,YAAY,KAAK,WAAW,EAAE;EACzF,MAAM,WAAgD,CAAC;EACvD,KAAK,MAAM,aAAa,OAAO,QAAQ,YAAY,KAAK,GAAG;GACzD,IAAI,UAAU,OAAO,YAAY,IAAI;GACrC,IAAI,EAAE,OAAO,OAAO,WAAW;IAAE,YAAY;IAAW;GAAO,CAAC,IAAI;GACpE,MAAM,yBAAyB,OAAO,QAAQ,eAAe,IAAI,UAAU,eAAe;GAC1F,IAAI,uBAAuB,eAAe,UAAU;GACpD,SAAS,KAAK;IACZ,YAAY;IACZ,UAAU,uBAAuB;IACjC,QAAQ,uBAAuB;GACjC,CAAC;EACH;EACA,OAAO;GACL;GACA;GACA;GACA,eACE,kBAAkB,OACd,OACA;IACE,IAAI,cAAc;IAClB,UAAU,cAAc;IACxB,QAAQ,cAAc;IACtB,cAAc,cAAc;IAC5B,YAAY,cAAc;GAC5B;GACN,eAAe,OAAO,OAAO,MAAM,IAAK,OAAO,MAAM,SAAS,MAAM,OAAQ;GAC5E,mBAAmB,SAAS;GAC5B;GACA,WAAW,UAAU,KAAK,CAAC,CAAC,KAAK,gBAAgB;IAC/C,IAAI,WAAW;IACf,MAAM,WAAW;IACjB,SAASK,gBAAyB,UAAU;GAC9C,EAAE;EACJ;CACF,CAAC;CAEH,MAAM,SAA6B,UACjC,OAAO,IAAI,aAAa;EACtB,OAAO;EACP,MAAM,YAAY,OAAOT;EACzB,MAAM,SAAS,OAAO;EACtB,MAAM,aAAa,OAAO,UAAU,IAAI,MAAM,QAAQ;EACtD,IAAI,MAAM,WAAW,KAAA,GAAW,OAAOI,OAAkB,MAAM,MAAM;EACrE,QAAQ,MAAM,OAAO,MAArB;GACE,KAAK,SAAS;IACZ,MAAM,QAAQ,WAAW,KAAK;IAC9B,IAAI,UAAU,KAAA,GACZ,OAAO,OAAO,QAAQ,GAAG,WAAW,KAAK,0BAA0B,QAAQ;IAE7E,MAAM,SAAS,OAAO,MAAM,aAAa,MAAM,OAAO,MAAM;IAC5D,MAAM,EAAE,YAAY,YAAY,OAAO,YAAY;KACjD;KACA,QAAQ;KACR;KACA,sBAAsB,MAAM;IAC9B,CAAC;IACD,MAAM,WACJ,MAAM,WAAW,KAAA,IACb,OACA,OAAO,WAAW;KAAE;KAAY;KAAY;KAAS,QAAQ,MAAM;IAAO,CAAC;IACjF,OAAO,QAAQ,YAAY,QAAQ;GACrC;GACA,KAAK;GACL,KAAK,eAAe;IAClB,MAAM,cACJ,MAAM,OAAO,SAAS,UAAU,WAAW,KAAK,QAAQ,WAAW,KAAK;IAC1E,IAAI,gBAAgB,KAAA,GAClB,OAAO,OAAO,QACZ,GAAG,WAAW,KAAK,kBAAkB,MAAM,OAAO,KAAK,YAAY,EAAE,eACrE,QACF;IAEF,IAAI,MAAM,gBAAgB,KAAA,GACxB,OAAO,OAAO,QAAQ,8CAA8C,aAAa;IAEnF,MAAM,iBAAiB,OAAO,MAAM,MAAM;IAC1C,MAAMM,SAAO,MAAM,OAAO,SAAS,UAAU,OAAOC,KAAW,IAAI;IACnE,MAAM,WACJ,MAAM,OAAO,SAAS,UAClB,OAAQ,YAAmC,MAAM;KAC/C,OAAO;KACP,aAAa,MAAM;KACnB,eAAeD,QAAM,iBAAiB;IACxC,CAAC,IACD,OAAQ,YAAyC,MAAM;KACrD,OAAO;KACP,aAAa,MAAM;IACrB,CAAC;IACP,OAAO,QAAQ,cAAc,IAC3B,IAAIE,aAAqB;KACvB,IAAI;KACJ,SAAS,UAAU;KACnB,SAAS,UAAU;KACnB,UAAU,WAAW;KACrB,SAAS,OAAO,WAAW,OAAO,CAAC,CAAC;MAClC,QAAQ,MAAM,OAAO,SAAS,UAAU,UAAU;MAClD,cAAcF,QAAM,gBAAgB;MACpC,aAAa,MAAM;MACnB,QAAQ,MAAM,UAAU;KAC1B,CAAC;KACD,UAAU,MAAM,OAAO,YAAY;KACnC,WAAW,SAAS,YAClB,OAAO,SAAS,KAChB,SAAS,OAAO,OAAO,iBAAiB,CAC1C;IACF,CAAC,CACH;IACA,OAAO,QAAQ,SAAS;KACtB,kBAAkB,SAAS;KAC3B;IACF,CAAC;GACH;EACF;CACF,CAAC;CAEH,MAAM,YAAmC,UACvC,OAAO,IAAI,aAAa;EAMtB,MAAM,WAAW,OAAO,OAAO,IAAI;GACjC,WAAW,IAAI,IAAI,MAAM,WAAW;GACpC,aACE,IAAIG,eAAsB,EACxB,QAAQ,IAAIrB,aAAoB;IAC9B,SAAS;IACT,OAAO;GACT,CAAC,EACH,CAAC;EACL,CAAC;EACD,MAAM,SAAS,OAAO,YAAY,SAAS,YAAY;EACvD,MAAM,mBAAmB,YACvBD,KAAY,IAAIuB,gBAAuB,EAAE,QAAQ,CAAC,CAAC;EACrD,IAAI,OAAO,UAAU,KAAA,GACnB,OAAO,OAAO,gBAAgB,qBAAqB,OAAO,OAAO;EAEnE,IAAI,OAAO,UAAU,MAAM,gBACzB,OAAO,OAAO,gBAAgB,gDAAgD;EAEhF,MAAM,OAAO,OAAO;EACpB,IAAI,SAAS,KAAA,GAAW,OAAO,OAAO,gBAAgB,sBAAsB;EAC5E,OAAO,OAAO,QAAQ,SACpB,gBAAgB,MAAM,kBACtB,OAAO,IAAI,aAAa;GACtB,MAAM,eAAe,OAAO,QAAQ,cAAc,IAAI,MAAM,cAAc;GAC1E,MAAM,UAAU,OAAOC,SAAc,SAAS,aAAa,SAAS,cAAc;GAClF,MAAM,aAAa,OAAO,UAAU,IAAI,aAAa,QAAQ;GAC7D,MAAM,SACJ,QAAQ,WAAW,UACf,OAAO,OAAO,IAAI,aAAa;IAC7B,MAAM,QAAQ,WAAW,KAAK;IAC9B,IAAI,UAAU,KAAA,KAAa,QAAQ,iBAAiB,MAClD,OAAO,OAAO,QACZ,GAAG,WAAW,KAAK,oCACnB,QACF;IAEF,OAAO,OAAO,MAAM,SAAS;KAC3B;KACA,aAAa,QAAQ;KACrB,cAAc,QAAQ;KACtB;IACF,CAAC;GACH,CAAC,IACD,OAAO,OAAO,IAAI,aAAa;IAC7B,MAAM,cAAc,WAAW,KAAK;IACpC,IAAI,gBAAgB,KAAA,GAClB,OAAO,OAAO,QACZ,GAAG,WAAW,KAAK,0CACnB,QACF;IAEF,OAAO,OAAO,YAAY,SAAS;KACjC;KACA,aAAa,QAAQ;KACrB;IACF,CAAC;GACH,CAAC;GACP,MAAM,EAAE,YAAY,YAAY,OAAO,YAAY;IACjD;IACA,QAAQ,QAAQ;IAChB;IACA,sBAAsB,CAAC;GACzB,CAAC;GACD,MAAM,WACJ,QAAQ,WAAW,OACf,OACA,OAAO,WAAW;IAAE;IAAY;IAAY;IAAS,QAAQ,QAAQ;GAAO,CAAC;GAKnF,OAAO,QAAQ,cAAc,QAAQ,MAAM,cAAc,CAAC,CAAC,KAAK,OAAO,MAAM;GAC7E,OAAO,QAAQ,YAAY,QAAQ;EACrC,CAAC,CACH;CACF,CAAC;CAEH,MAAM,UAA+B,UACnC,OAAO,IAAI,aAAa;EACtB,MAAM,aAAa,OAAO,QAAQ,YAAY,IAAI,MAAM,YAAY;EACpE,MAAM,gBAAgB,OAAO,QAAQ,eAAe,IAAI,WAAW,eAAe;EAClF,MAAM,aAAa,OAAO,UAAU,IAAI,cAAc,QAAQ;EAC9D,MAAM,UAAU,OAAO,WAAW,UAAU;EAC5C,OAAO,OAAO,WAAW;GACvB;GACA;GACA;GACA,QAAQ,MAAM;GACd,GAAI,MAAM,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,MAAM,OAAO;EAC/D,CAAC;CACH,CAAC;CAEH,MAAM,cAAuC,iBAC3C,OAAO,IAAI,aAAa;EACtB,OAAO;EACP,MAAM,aAAa,OAAO,QAAQ,YAAY,IAAI,YAAY;EAC9D,MAAM,cAAc,OAAO,QAAQ,YAAY,KAAK,YAAY;EAChE,OAAO,OAAO,QAAQ,cAAc,eAAe,QAAQ,YAAY,OAAO,WAAW,EAAE,CAAC;EAC5F,OAAO,QAAQ,YAAY,OAAO,YAAY;EAI9C,KAHmB,OAAO,QAAQ,YAAY,KAAK,EAAA,CAAG,MACnD,cAAc,UAAU,oBAAoB,WAAW,eAE9C,GAAG;EACf,MAAM,gBAAgB,OAAO,QAAQ,eAAe,IAAI,WAAW,eAAe;EAClF,OAAO,QAAQ,eAAe,OAAO,cAAc,IAAI,SAAS,aAAa,CAAC;CAChF,CAAC;CAEH,MAAM,WAAiC,iBACrC,OAAO,IAAI,aAAa;EACtB,MAAM,aAAa,OAAO,QAAQ,YAAY,IAAI,YAAY;EAC9D,MAAM,aAAa,OAAO,QAAQ,YAAY,IAAI,WAAW,YAAY;EACzE,MAAM,gBAAgB,OAAO,QAAQ,eAAe,IAAI,WAAW,eAAe;EAClF,MAAM,aAAa,OAAO,UAAU,IAAI,cAAc,QAAQ;EAC9D,MAAM,SAAS,OAAOA,SAAc,QAAQ,WAAW,QAAQ,QAAQ;EACvE,MAAM,SAA0B;GAC9B,GAAG;GACH,SAAS,OAAOjB,cAAuB,YAAY,OAAO,OAAO;EACnE;EACA,MAAM,QAAQ,OAAO,WAAW,UAAU;EAI1C,IAAI,EAAC,OADoB,MAAM,YAAY,EAAA,CAC5B,MAAM,cAAc,UAAU,SAAS,OAAO,IAAI,GAC/D,OAAO,OAAOP,KAAY,IAAIc,SAAgB;GAAE,QAAQ;GAAQ,IAAI,OAAO;EAAK,CAAC,CAAC;EAEpF,OAAO;GAAE,SAAS;GAAO;EAAO;CAClC,CAAC;CAEH,OAAO;EACL;EACA;EACA;EACA;EACA;EACA,SAAS,iBAAiB,QAAQ,YAAY,OAAO,YAAY;EACjE;EACA;EACA;CACF;AACF,CAAC;AAED,MAAa,QAIT,MAAM,OAAO,OAAO,CAAC,CAAC,IAAI;AAE9B,MAAM,YAEF,UAED,GAAG,SACF,OAAO,QAAQ,UAAU,YAAY,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;AAE/D,MAAa,UAAU,UAAU,YAAY,QAAQ,OAAO;AAC5D,MAAa,WAAW,UAAU,YAAY,QAAQ,QAAQ;AAC9D,MAAa,QAAQ,UAAU,YAAY,QAAQ,KAAK;AACxD,MAAa,WAAW,UAAU,YAAY,QAAQ,QAAQ;AAC9D,MAAa,SAAS,UAAU,YAAY,QAAQ,MAAM;AAC1D,MAAa,SAAS,UAAU,YAAY,QAAQ,MAAM;AAC1D,MAAa,aAAa,UAAU,YAAY,QAAQ,UAAU;AAClE,MAAa,UAAU,UAAU,YAAY,QAAQ,OAAO;AAC5D,MAAa,UAAU,UAAU,YAAY,QAAQ,OAAO"}
package/dist/Provider.mjs CHANGED
@@ -45,14 +45,12 @@ const tokenFields = (auth) => Object.entries(auth.fields.fields).map(([name, fie
45
45
  secret: ast._tag === "Declaration"
46
46
  };
47
47
  });
48
+ /**
49
+ * Every method a provider offers, in the order a UI should present them: the interactive ones a
50
+ * customer clicks through first, then the token they have to go and fetch themselves.
51
+ */
48
52
  const describeMethods = (definition) => {
49
53
  const found = [];
50
- if (definition.auth.token !== void 0) found.push({
51
- kind: "token",
52
- label: definition.auth.token.label,
53
- docsUrl: definition.auth.token.docsUrl ?? null,
54
- fields: tokenFields(definition.auth.token)
55
- });
56
54
  if (definition.auth.oauth !== void 0) found.push({
57
55
  kind: "oauth",
58
56
  label: definition.auth.oauth.label,
@@ -65,6 +63,12 @@ const describeMethods = (definition) => {
65
63
  docsUrl: null,
66
64
  fields: null
67
65
  });
66
+ if (definition.auth.token !== void 0) found.push({
67
+ kind: "token",
68
+ label: definition.auth.token.label,
69
+ docsUrl: definition.auth.token.docsUrl ?? null,
70
+ fields: tokenFields(definition.auth.token)
71
+ });
68
72
  return found;
69
73
  };
70
74
  /** How provider context is persisted: the definition's version tag beside the encoded value. */
@@ -91,13 +95,8 @@ const make = (definition) => {
91
95
  if (definition.contextVersion.length === 0) throw invalid(`Provider ${definition.id} needs a contextVersion`, "contextVersion");
92
96
  return definition;
93
97
  };
94
- const methods = (definition) => {
95
- const found = [];
96
- if (definition.auth.token !== void 0) found.push("token");
97
- if (definition.auth.oauth !== void 0) found.push("oauth");
98
- if (definition.auth.integration !== void 0) found.push("integration");
99
- return found;
100
- };
98
+ /** The kinds alone, in the same order `describeMethods` puts them: interactive first, token last. */
99
+ const methods = (definition) => describeMethods(definition).map((method) => method.kind);
101
100
  /**
102
101
  * Pick the most specific zone among `targets` for `domain`: exactly one match resolves, several
103
102
  * matches at the same depth need a selection, none is `NotFound`.
@@ -1 +1 @@
1
- {"version":3,"file":"Provider.mjs","names":["Errors.decode","Errors.DomainKitError","Reason.InvalidInput","DomainName.candidates","Errors.fail","Reason.Unsupported"],"sources":["../src/Provider.ts"],"sourcesContent":["/**\n * One declarative definition per DNS provider. Auth methods are optional cases, so a provider that\n * only issues personal tokens declares `token` and nothing else. The definition drives connection\n * routes, refresh, the UI method catalog, and rebuilding a session from stored context.\n */\nimport { Data, type DateTime, Effect, Redacted, Schema } from \"effect\";\n\nimport type * as DnsRecord from \"./DnsRecord.ts\";\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as DomainName from \"./DomainName.ts\";\nimport type { Capability } from \"./Storage.ts\";\n\nexport type Fx<A> = Effect.Effect<A, Errors.DomainKitError>;\n\n/** A record as the provider reports it, with the id needed to read or delete it later. */\nexport interface ObservedRecord {\n readonly record: DnsRecord.Observed;\n readonly providerRecordId: string | null;\n}\n\n/** What a session can do inside one zone. The only thing planning and applying touch. */\nexport interface Dns {\n readonly list: (zone: string) => Fx<ReadonlyArray<ObservedRecord>>;\n /** Must return the provider's id for the new record; cleanup deletes by that id. */\n readonly create: (\n zone: string,\n record: DnsRecord.Model,\n ) => Fx<{ readonly providerRecordId: string }>;\n readonly get: (zone: string, providerRecordId: string) => Fx<DnsRecord.Observed | null>;\n readonly delete: (zone: string, providerRecordId: string) => Fx<void>;\n}\n\n/** A zone the credential can reach, with the provider identity needed to address it later. */\nexport interface Target {\n readonly zone: string;\n readonly context: unknown;\n readonly label: string;\n /** The zone's nameservers as the provider reports them; `Connect.discover` matches on them. */\n readonly nameservers?: ReadonlyArray<string>;\n}\n\nexport type Resolution = Data.TaggedEnum<{\n Resolved: { readonly target: Target };\n SelectionRequired: { readonly candidates: ReadonlyArray<Target> };\n NotFound: {};\n}>;\nexport const Resolution = Data.taggedEnum<Resolution>();\n\n/** A credential bound to provider account context. */\nexport interface Session {\n readonly listTargets: () => Fx<ReadonlyArray<Target>>;\n readonly resolveTarget: (domain: string) => Fx<Resolution>;\n readonly dns: (target: Target) => Dns;\n /** Capabilities actually held; promoted onto the authorization after first use. */\n readonly capabilities: () => Fx<ReadonlyArray<Capability>>;\n}\n\n/**\n * Secret material plus the account context needed to rebuild a session. `secret` is opaque to\n * DomainKit: a provider may pack an access token and a refresh token into it.\n */\nexport interface Credential {\n readonly secret: Redacted.Redacted<string>;\n readonly context: unknown;\n}\n\n/** A credential the provider just issued or refreshed; `null` expiry means it does not expire. */\nexport interface IssuedCredential extends Credential {\n readonly expiresAt: DateTime.Utc | null;\n}\n\n/** Raw token-method input keyed by the provider's declared field names. */\nexport type TokenValues = Readonly<Record<string, Redacted.Redacted<string> | undefined>>;\n\nexport interface TokenAuth {\n readonly label: string;\n /** Where the customer creates the token; shown in the UI. */\n readonly docsUrl?: string;\n readonly requiredCapabilities: ReadonlyArray<Capability>;\n /**\n * The values this method needs, declared once so forms render from it: secrets as\n * `Schema.RedactedFromValue(Schema.String)`, optional ones through `Schema.optionalKey`.\n */\n readonly fields: Schema.Struct<Schema.Struct.Fields>;\n /** Validate the values (already decoded with `fields`) and return account context. */\n readonly authenticate: (values: TokenValues) => Fx<IssuedCredential>;\n}\n\n/** Build a `TokenAuth` whose `authenticate` receives the record decoded by `fields`. */\n/** Field schemas whose decoding needs no services, so `authenticate` stays a plain `Fx`. */\nexport type TokenFieldSchemas = { readonly [name: string]: Schema.ConstraintDecoder<unknown> };\n\nexport const tokenAuth = <const Fields extends TokenFieldSchemas>(input: {\n readonly label: string;\n readonly docsUrl?: string;\n readonly requiredCapabilities: ReadonlyArray<Capability>;\n readonly fields: Schema.Struct<Fields>;\n readonly authenticate: (values: Schema.Struct<Fields>[\"Type\"]) => Fx<IssuedCredential>;\n}): TokenAuth => ({\n label: input.label,\n ...(input.docsUrl === undefined ? {} : { docsUrl: input.docsUrl }),\n requiredCapabilities: input.requiredCapabilities,\n fields: input.fields as Schema.Struct<Schema.Struct.Fields>,\n authenticate: (values) =>\n decodeTokenValues(input.fields, values).pipe(Effect.flatMap(input.authenticate)),\n});\n\nconst decodeTokenValues = <Fields extends TokenFieldSchemas>(\n fields: Schema.Struct<Fields>,\n values: TokenValues,\n): Effect.Effect<Schema.Struct<Fields>[\"Type\"], Errors.DomainKitError> => {\n const raw: Record<string, string> = {};\n for (const [name, value] of Object.entries(values)) {\n if (value !== undefined) raw[name] = Redacted.value(value);\n }\n return Errors.decode(fields, raw, \"method\");\n};\n\nexport interface FieldDescriptor {\n readonly name: string;\n readonly required: boolean;\n readonly secret: boolean;\n}\n\n/** What a UI needs to render one auth method; `fields` is `null` for interactive methods. */\nexport interface MethodDescriptor {\n readonly kind: AuthMethod;\n readonly label: string;\n readonly docsUrl: string | null;\n readonly fields: ReadonlyArray<FieldDescriptor> | null;\n}\n\n/** Field descriptors derived from `TokenAuth.fields`: optional keys are not required, `Redacted` values are secret. */\nexport const tokenFields = (auth: TokenAuth): ReadonlyArray<FieldDescriptor> =>\n Object.entries(auth.fields.fields).map(([name, field]) => {\n const ast = (field as { readonly ast: FieldAst }).ast;\n return {\n name,\n required: ast.context?.isOptional !== true,\n secret: ast._tag === \"Declaration\",\n };\n });\n\ninterface FieldAst {\n readonly _tag: string;\n readonly context?: { readonly isOptional?: boolean } | undefined;\n}\n\nexport const describeMethods = (definition: Definition): ReadonlyArray<MethodDescriptor> => {\n const found: Array<MethodDescriptor> = [];\n if (definition.auth.token !== undefined) {\n found.push({\n kind: \"token\",\n label: definition.auth.token.label,\n docsUrl: definition.auth.token.docsUrl ?? null,\n fields: tokenFields(definition.auth.token),\n });\n }\n if (definition.auth.oauth !== undefined) {\n found.push({ kind: \"oauth\", label: definition.auth.oauth.label, docsUrl: null, fields: null });\n }\n if (definition.auth.integration !== undefined) {\n found.push({\n kind: \"integration\",\n label: definition.auth.integration.label,\n docsUrl: null,\n fields: null,\n });\n }\n return found;\n};\n\nexport interface CallbackInput {\n readonly code: string;\n /** The redirect URI registered at start; token exchanges echo it. */\n readonly callbackUrl: string;\n /** Every query parameter the provider appended to the callback. */\n readonly params: Readonly<Record<string, string>>;\n}\n\nexport interface OAuthAuth {\n readonly label: string;\n readonly scopes: ReadonlyArray<string>;\n readonly start: (input: {\n readonly state: string;\n readonly callbackUrl: string;\n readonly codeChallenge: string;\n }) => Fx<{ readonly authorizationUrl: string }>;\n readonly complete: (\n input: CallbackInput & { readonly codeVerifier: string },\n ) => Fx<IssuedCredential>;\n readonly refresh: (credential: Credential) => Fx<IssuedCredential>;\n readonly revoke?: (credential: Credential) => Fx<void>;\n}\n\n/** Marketplace-style installs (Vercel integrations) that redirect but are not OAuth. */\nexport interface IntegrationAuth {\n readonly label: string;\n readonly start: (input: {\n readonly state: string;\n readonly callbackUrl: string;\n }) => Fx<{ readonly authorizationUrl: string }>;\n readonly complete: (input: CallbackInput) => Fx<IssuedCredential>;\n readonly refresh?: (credential: Credential) => Fx<IssuedCredential>;\n readonly revoke?: (credential: Credential) => Fx<void>;\n}\n\nexport interface Definition<Context = unknown> {\n readonly id: string;\n readonly name: string;\n /**\n * Hostname suffixes of the nameservers this provider operates, e.g. `ns.cloudflare.com`.\n * `Connect.discover` names the provider as a domain's host when every authoritative nameserver\n * ends in one of them.\n */\n readonly nameservers?: ReadonlyArray<string>;\n readonly auth: {\n readonly token?: TokenAuth;\n readonly oauth?: OAuthAuth;\n readonly integration?: IntegrationAuth;\n };\n /** Schema for the account/zone context stored on authorizations and attachments. */\n readonly context: Schema.Codec<Context, unknown, never, never>;\n /** Tag written into every stored context envelope, e.g. `cloudflare.v1`; bump it with the schema. */\n readonly contextVersion: string;\n /** Upgrade an envelope written under an older `contextVersion`; absent means `Unsupported`. */\n readonly migrateContext?: (envelope: Envelope) => Fx<Context>;\n readonly session: (credential: Credential) => Session;\n}\n\n/** How provider context is persisted: the definition's version tag beside the encoded value. */\nexport const Envelope = Schema.Struct({ version: Schema.String, value: Schema.Unknown });\nexport type Envelope = typeof Envelope.Type;\n\n/** Wrap a context value for storage; a value the schema rejects fails `InvalidInput`. */\nexport const encodeContext = <Context>(\n definition: Definition<Context>,\n value: Context,\n): Effect.Effect<Envelope, Errors.DomainKitError> =>\n Schema.encodeEffect(definition.context)(value).pipe(\n Effect.map((encoded) => ({ version: definition.contextVersion, value: encoded })),\n Effect.mapError(\n (cause) =>\n new Errors.DomainKitError({\n reason: new Reason.InvalidInput({ message: cause.message, field: \"context\" }),\n }),\n ),\n );\n\nexport type AuthMethod = \"token\" | \"oauth\" | \"integration\";\n\n/** Validates a definition: an id and at least one auth method. Throws `DomainKit.Error` otherwise. */\nexport const make = <Context>(definition: Definition<Context>): Definition<Context> => {\n const invalid = (message: string, field: string) =>\n new Errors.DomainKitError({\n reason: new Reason.InvalidInput({ message, field }),\n });\n if (!/^[a-z0-9][a-z0-9-]*$/.test(definition.id)) {\n throw invalid(\n `Provider id ${JSON.stringify(definition.id)} must be lowercase kebab-case`,\n \"id\",\n );\n }\n if (methods(definition).length === 0) {\n throw invalid(`Provider ${definition.id} declares no auth method`, \"auth\");\n }\n if (definition.contextVersion.length === 0) {\n throw invalid(`Provider ${definition.id} needs a contextVersion`, \"contextVersion\");\n }\n return definition;\n};\n\nexport const methods = (definition: Definition): ReadonlyArray<AuthMethod> => {\n const found: Array<AuthMethod> = [];\n if (definition.auth.token !== undefined) found.push(\"token\");\n if (definition.auth.oauth !== undefined) found.push(\"oauth\");\n if (definition.auth.integration !== undefined) found.push(\"integration\");\n return found;\n};\n\n/**\n * Pick the most specific zone among `targets` for `domain`: exactly one match resolves, several\n * matches at the same depth need a selection, none is `NotFound`.\n */\nexport const resolveAmong = (\n domain: DomainName.Model,\n targets: ReadonlyArray<Target>,\n): Resolution => {\n for (const candidate of DomainName.candidates(domain)) {\n const matches = targets.filter((target) => target.zone === candidate);\n const [only] = matches;\n if (matches.length === 1 && only !== undefined) return Resolution.Resolved({ target: only });\n if (matches.length > 1) return Resolution.SelectionRequired({ candidates: matches });\n }\n return Resolution.NotFound();\n};\n\n/**\n * Decode a stored envelope: the current version decodes with `context`, an older one goes\n * through `migrateContext`, anything else fails `Unsupported`.\n */\nexport const decodeContext = <Context>(\n definition: Definition<Context>,\n stored: unknown,\n): Effect.Effect<Context, Errors.DomainKitError> =>\n Errors.decode(Envelope, stored, \"context\").pipe(\n Effect.flatMap((envelope) => {\n if (envelope.version === definition.contextVersion) {\n return Errors.decode(definition.context, envelope.value, \"context\");\n }\n if (definition.migrateContext !== undefined) return definition.migrateContext(envelope);\n return Errors.fail(\n new Reason.Unsupported({\n provider: definition.id,\n operation: \"context\",\n message: `${definition.id} context version ${envelope.version} is not supported (current ${definition.contextVersion})`,\n }),\n );\n }),\n );\n\nexport {\n type AsyncCredential,\n type AsyncDefinition,\n type AsyncDns,\n type AsyncIntegrationAuth,\n type AsyncIssuedCredential,\n type AsyncOAuthAuth,\n type AsyncSession,\n type AsyncTokenAuth,\n fromAsync,\n} from \"./internal/provider-async.ts\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"Provider.mjs","names":["Errors.decode","Errors.DomainKitError","Reason.InvalidInput","DomainName.candidates","Errors.fail","Reason.Unsupported"],"sources":["../src/Provider.ts"],"sourcesContent":["/**\n * One declarative definition per DNS provider. Auth methods are optional cases, so a provider that\n * only issues personal tokens declares `token` and nothing else. The definition drives connection\n * routes, refresh, the UI method catalog, and rebuilding a session from stored context.\n */\nimport { Data, type DateTime, Effect, Redacted, Schema } from \"effect\";\n\nimport type * as DnsRecord from \"./DnsRecord.ts\";\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as DomainName from \"./DomainName.ts\";\nimport type { Capability } from \"./Storage.ts\";\n\nexport type Fx<A> = Effect.Effect<A, Errors.DomainKitError>;\n\n/** A record as the provider reports it, with the id needed to read or delete it later. */\nexport interface ObservedRecord {\n readonly record: DnsRecord.Observed;\n readonly providerRecordId: string | null;\n}\n\n/** What a session can do inside one zone. The only thing planning and applying touch. */\nexport interface Dns {\n readonly list: (zone: string) => Fx<ReadonlyArray<ObservedRecord>>;\n /** Must return the provider's id for the new record; cleanup deletes by that id. */\n readonly create: (\n zone: string,\n record: DnsRecord.Model,\n ) => Fx<{ readonly providerRecordId: string }>;\n readonly get: (zone: string, providerRecordId: string) => Fx<DnsRecord.Observed | null>;\n readonly delete: (zone: string, providerRecordId: string) => Fx<void>;\n}\n\n/** A zone the credential can reach, with the provider identity needed to address it later. */\nexport interface Target {\n readonly zone: string;\n readonly context: unknown;\n readonly label: string;\n /** The zone's nameservers as the provider reports them; `Connect.discover` matches on them. */\n readonly nameservers?: ReadonlyArray<string>;\n}\n\nexport type Resolution = Data.TaggedEnum<{\n Resolved: { readonly target: Target };\n SelectionRequired: { readonly candidates: ReadonlyArray<Target> };\n NotFound: {};\n}>;\nexport const Resolution = Data.taggedEnum<Resolution>();\n\n/** A credential bound to provider account context. */\nexport interface Session {\n readonly listTargets: () => Fx<ReadonlyArray<Target>>;\n readonly resolveTarget: (domain: string) => Fx<Resolution>;\n readonly dns: (target: Target) => Dns;\n /** Capabilities actually held; promoted onto the authorization after first use. */\n readonly capabilities: () => Fx<ReadonlyArray<Capability>>;\n}\n\n/**\n * Secret material plus the account context needed to rebuild a session. `secret` is opaque to\n * DomainKit: a provider may pack an access token and a refresh token into it.\n */\nexport interface Credential {\n readonly secret: Redacted.Redacted<string>;\n readonly context: unknown;\n}\n\n/** A credential the provider just issued or refreshed; `null` expiry means it does not expire. */\nexport interface IssuedCredential extends Credential {\n readonly expiresAt: DateTime.Utc | null;\n}\n\n/** Raw token-method input keyed by the provider's declared field names. */\nexport type TokenValues = Readonly<Record<string, Redacted.Redacted<string> | undefined>>;\n\nexport interface TokenAuth {\n readonly label: string;\n /** Where the customer creates the token; shown in the UI. */\n readonly docsUrl?: string;\n readonly requiredCapabilities: ReadonlyArray<Capability>;\n /**\n * The values this method needs, declared once so forms render from it: secrets as\n * `Schema.RedactedFromValue(Schema.String)`, optional ones through `Schema.optionalKey`.\n */\n readonly fields: Schema.Struct<Schema.Struct.Fields>;\n /** Validate the values (already decoded with `fields`) and return account context. */\n readonly authenticate: (values: TokenValues) => Fx<IssuedCredential>;\n}\n\n/** Build a `TokenAuth` whose `authenticate` receives the record decoded by `fields`. */\n/** Field schemas whose decoding needs no services, so `authenticate` stays a plain `Fx`. */\nexport type TokenFieldSchemas = { readonly [name: string]: Schema.ConstraintDecoder<unknown> };\n\nexport const tokenAuth = <const Fields extends TokenFieldSchemas>(input: {\n readonly label: string;\n readonly docsUrl?: string;\n readonly requiredCapabilities: ReadonlyArray<Capability>;\n readonly fields: Schema.Struct<Fields>;\n readonly authenticate: (values: Schema.Struct<Fields>[\"Type\"]) => Fx<IssuedCredential>;\n}): TokenAuth => ({\n label: input.label,\n ...(input.docsUrl === undefined ? {} : { docsUrl: input.docsUrl }),\n requiredCapabilities: input.requiredCapabilities,\n fields: input.fields as Schema.Struct<Schema.Struct.Fields>,\n authenticate: (values) =>\n decodeTokenValues(input.fields, values).pipe(Effect.flatMap(input.authenticate)),\n});\n\nconst decodeTokenValues = <Fields extends TokenFieldSchemas>(\n fields: Schema.Struct<Fields>,\n values: TokenValues,\n): Effect.Effect<Schema.Struct<Fields>[\"Type\"], Errors.DomainKitError> => {\n const raw: Record<string, string> = {};\n for (const [name, value] of Object.entries(values)) {\n if (value !== undefined) raw[name] = Redacted.value(value);\n }\n return Errors.decode(fields, raw, \"method\");\n};\n\nexport interface FieldDescriptor {\n readonly name: string;\n readonly required: boolean;\n readonly secret: boolean;\n}\n\n/** What a UI needs to render one auth method; `fields` is `null` for interactive methods. */\nexport interface MethodDescriptor {\n readonly kind: AuthMethod;\n readonly label: string;\n readonly docsUrl: string | null;\n readonly fields: ReadonlyArray<FieldDescriptor> | null;\n}\n\n/** Field descriptors derived from `TokenAuth.fields`: optional keys are not required, `Redacted` values are secret. */\nexport const tokenFields = (auth: TokenAuth): ReadonlyArray<FieldDescriptor> =>\n Object.entries(auth.fields.fields).map(([name, field]) => {\n const ast = (field as { readonly ast: FieldAst }).ast;\n return {\n name,\n required: ast.context?.isOptional !== true,\n secret: ast._tag === \"Declaration\",\n };\n });\n\ninterface FieldAst {\n readonly _tag: string;\n readonly context?: { readonly isOptional?: boolean } | undefined;\n}\n\n/**\n * Every method a provider offers, in the order a UI should present them: the interactive ones a\n * customer clicks through first, then the token they have to go and fetch themselves.\n */\nexport const describeMethods = (definition: Definition): ReadonlyArray<MethodDescriptor> => {\n const found: Array<MethodDescriptor> = [];\n if (definition.auth.oauth !== undefined) {\n found.push({ kind: \"oauth\", label: definition.auth.oauth.label, docsUrl: null, fields: null });\n }\n if (definition.auth.integration !== undefined) {\n found.push({\n kind: \"integration\",\n label: definition.auth.integration.label,\n docsUrl: null,\n fields: null,\n });\n }\n if (definition.auth.token !== undefined) {\n found.push({\n kind: \"token\",\n label: definition.auth.token.label,\n docsUrl: definition.auth.token.docsUrl ?? null,\n fields: tokenFields(definition.auth.token),\n });\n }\n return found;\n};\n\nexport interface CallbackInput {\n readonly code: string;\n /** The redirect URI registered at start; token exchanges echo it. */\n readonly callbackUrl: string;\n /** Every query parameter the provider appended to the callback. */\n readonly params: Readonly<Record<string, string>>;\n}\n\nexport interface OAuthAuth {\n readonly label: string;\n readonly scopes: ReadonlyArray<string>;\n readonly start: (input: {\n readonly state: string;\n readonly callbackUrl: string;\n readonly codeChallenge: string;\n }) => Fx<{ readonly authorizationUrl: string }>;\n readonly complete: (\n input: CallbackInput & { readonly codeVerifier: string },\n ) => Fx<IssuedCredential>;\n readonly refresh: (credential: Credential) => Fx<IssuedCredential>;\n readonly revoke?: (credential: Credential) => Fx<void>;\n}\n\n/** Marketplace-style installs (Vercel integrations) that redirect but are not OAuth. */\nexport interface IntegrationAuth {\n readonly label: string;\n readonly start: (input: {\n readonly state: string;\n readonly callbackUrl: string;\n }) => Fx<{ readonly authorizationUrl: string }>;\n readonly complete: (input: CallbackInput) => Fx<IssuedCredential>;\n readonly refresh?: (credential: Credential) => Fx<IssuedCredential>;\n readonly revoke?: (credential: Credential) => Fx<void>;\n}\n\nexport interface Definition<Context = unknown> {\n readonly id: string;\n readonly name: string;\n /**\n * Hostname suffixes of the nameservers this provider operates, e.g. `ns.cloudflare.com`.\n * `Connect.discover` names the provider as a domain's host when every authoritative nameserver\n * ends in one of them.\n */\n readonly nameservers?: ReadonlyArray<string>;\n readonly auth: {\n readonly token?: TokenAuth;\n readonly oauth?: OAuthAuth;\n readonly integration?: IntegrationAuth;\n };\n /** Schema for the account/zone context stored on authorizations and attachments. */\n readonly context: Schema.Codec<Context, unknown, never, never>;\n /** Tag written into every stored context envelope, e.g. `cloudflare.v1`; bump it with the schema. */\n readonly contextVersion: string;\n /** Upgrade an envelope written under an older `contextVersion`; absent means `Unsupported`. */\n readonly migrateContext?: (envelope: Envelope) => Fx<Context>;\n readonly session: (credential: Credential) => Session;\n}\n\n/** How provider context is persisted: the definition's version tag beside the encoded value. */\nexport const Envelope = Schema.Struct({ version: Schema.String, value: Schema.Unknown });\nexport type Envelope = typeof Envelope.Type;\n\n/** Wrap a context value for storage; a value the schema rejects fails `InvalidInput`. */\nexport const encodeContext = <Context>(\n definition: Definition<Context>,\n value: Context,\n): Effect.Effect<Envelope, Errors.DomainKitError> =>\n Schema.encodeEffect(definition.context)(value).pipe(\n Effect.map((encoded) => ({ version: definition.contextVersion, value: encoded })),\n Effect.mapError(\n (cause) =>\n new Errors.DomainKitError({\n reason: new Reason.InvalidInput({ message: cause.message, field: \"context\" }),\n }),\n ),\n );\n\nexport type AuthMethod = \"token\" | \"oauth\" | \"integration\";\n\n/** Validates a definition: an id and at least one auth method. Throws `DomainKit.Error` otherwise. */\nexport const make = <Context>(definition: Definition<Context>): Definition<Context> => {\n const invalid = (message: string, field: string) =>\n new Errors.DomainKitError({\n reason: new Reason.InvalidInput({ message, field }),\n });\n if (!/^[a-z0-9][a-z0-9-]*$/.test(definition.id)) {\n throw invalid(\n `Provider id ${JSON.stringify(definition.id)} must be lowercase kebab-case`,\n \"id\",\n );\n }\n if (methods(definition).length === 0) {\n throw invalid(`Provider ${definition.id} declares no auth method`, \"auth\");\n }\n if (definition.contextVersion.length === 0) {\n throw invalid(`Provider ${definition.id} needs a contextVersion`, \"contextVersion\");\n }\n return definition;\n};\n\n/** The kinds alone, in the same order `describeMethods` puts them: interactive first, token last. */\nexport const methods = (definition: Definition): ReadonlyArray<AuthMethod> =>\n describeMethods(definition).map((method) => method.kind);\n\n/**\n * Pick the most specific zone among `targets` for `domain`: exactly one match resolves, several\n * matches at the same depth need a selection, none is `NotFound`.\n */\nexport const resolveAmong = (\n domain: DomainName.Model,\n targets: ReadonlyArray<Target>,\n): Resolution => {\n for (const candidate of DomainName.candidates(domain)) {\n const matches = targets.filter((target) => target.zone === candidate);\n const [only] = matches;\n if (matches.length === 1 && only !== undefined) return Resolution.Resolved({ target: only });\n if (matches.length > 1) return Resolution.SelectionRequired({ candidates: matches });\n }\n return Resolution.NotFound();\n};\n\n/**\n * Decode a stored envelope: the current version decodes with `context`, an older one goes\n * through `migrateContext`, anything else fails `Unsupported`.\n */\nexport const decodeContext = <Context>(\n definition: Definition<Context>,\n stored: unknown,\n): Effect.Effect<Context, Errors.DomainKitError> =>\n Errors.decode(Envelope, stored, \"context\").pipe(\n Effect.flatMap((envelope) => {\n if (envelope.version === definition.contextVersion) {\n return Errors.decode(definition.context, envelope.value, \"context\");\n }\n if (definition.migrateContext !== undefined) return definition.migrateContext(envelope);\n return Errors.fail(\n new Reason.Unsupported({\n provider: definition.id,\n operation: \"context\",\n message: `${definition.id} context version ${envelope.version} is not supported (current ${definition.contextVersion})`,\n }),\n );\n }),\n );\n\nexport {\n type AsyncCredential,\n type AsyncDefinition,\n type AsyncDns,\n type AsyncIntegrationAuth,\n type AsyncIssuedCredential,\n type AsyncOAuthAuth,\n type AsyncSession,\n type AsyncTokenAuth,\n fromAsync,\n} from \"./internal/provider-async.ts\";\n"],"mappings":"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"}
package/dist/Server.mjs CHANGED
@@ -94,6 +94,8 @@ const Snapshot = Schema.Struct({
94
94
  method: Schema.NullOr(AuthMethod),
95
95
  status: ConnectionStatus,
96
96
  lastReceiptId: Schema.NullOr(Schema.String),
97
+ /** How many domains the connection serves, including this one; `0` without a connection. */
98
+ connectionDomains: Schema.Number,
97
99
  reusable: Schema.Array(Schema.Struct({
98
100
  connectionId: Schema.String,
99
101
  provider: Schema.String,
@@ -345,6 +347,7 @@ const snapshotOf = (snapshot) => ({
345
347
  method: snapshot.authorization?.method ?? null,
346
348
  status: snapshot.connection === null ? "disconnected" : snapshot.authorization?.revocation === "active" ? "connected" : "reconnect",
347
349
  lastReceiptId: snapshot.lastReceiptId,
350
+ connectionDomains: snapshot.connectionDomains,
348
351
  reusable: snapshot.reusable.map(({ connection, provider, method }) => ({
349
352
  connectionId: connection.id,
350
353
  provider,