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.
- package/dist/Cloudflare.mjs +35 -12
- package/dist/Cloudflare.mjs.map +1 -1
- package/dist/Connect.mjs +2 -0
- package/dist/Connect.mjs.map +1 -1
- package/dist/Provider.mjs +12 -13
- package/dist/Provider.mjs.map +1 -1
- package/dist/Server.mjs +3 -0
- package/dist/Server.mjs.map +1 -1
- package/dist/Testing.mjs +6 -6
- package/dist/Testing.mjs.map +1 -1
- package/dist/internal/oauth.mjs +35 -7
- package/dist/internal/oauth.mjs.map +1 -1
- package/dist/package.mjs +1 -1
- package/dist/types/Cloudflare.d.ts +21 -0
- package/dist/types/Connect.d.ts +2 -0
- package/dist/types/Provider.d.ts +5 -0
- package/dist/types/Server.d.ts +6 -0
- package/dist/types/Testing.d.ts +10 -3
- package/dist/types/internal/oauth.d.ts +4 -0
- package/package.json +1 -1
package/dist/Server.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Server.mjs","names":["Storage.AuthMethod","DnsRecord.Model","Plan.OperationId","Plan.Model","Storage.AttemptStatus","Approval.Model","Receipt.Model","Storage.Rejection","Storage.Overall","Storage.RequirementStatus","Verify.Evidence","Verify.HostEvidence","Plan.PlanId","Approval.ApprovalId","Receipt.ReceiptId","Errors.fail","Reason.InvalidInput","Errors.DomainKitError","Connect.Service","Provision.Service","Cleanup.Service","Verify.Service","Storage.Service","Principal.Service","Reason.NotFound"],"sources":["../src/Server.ts"],"sourcesContent":["/**\n * domainkit/server — the whole lifecycle as one HttpApi group the host mounts.\n *\n * const Api = HttpApi.make(\"app\").add(Server.group)\n * const ApiLive = HttpApiBuilder.layer(Api).pipe(\n * Layer.provide([Server.layer(Api), DomainKitLive, IdentityLive]),\n * )\n *\n * The host provides `Identity` (request -> `Principal.Interface`). Everything else comes from\n * `DomainKit.layer`. Non-HttpApi hosts use `toWebHandler`. The group carries no prefix of its\n * own, so `group.prefix(\"/internal/dns\")` or a host router mount is all a different base path\n * takes; callback URLs follow the mount.\n */\nimport { Context, Effect, Layer, Option, Schema } from \"effect\";\nimport * as HttpRouter from \"effect/unstable/http/HttpRouter\";\nimport * as HttpServer from \"effect/unstable/http/HttpServer\";\nimport * as HttpServerRequest from \"effect/unstable/http/HttpServerRequest\";\nimport * as HttpApi from \"effect/unstable/httpapi/HttpApi\";\nimport * as HttpApiBuilder from \"effect/unstable/httpapi/HttpApiBuilder\";\nimport * as HttpApiEndpoint from \"effect/unstable/httpapi/HttpApiEndpoint\";\nimport * as HttpApiGroup from \"effect/unstable/httpapi/HttpApiGroup\";\nimport * as HttpApiSchema from \"effect/unstable/httpapi/HttpApiSchema\";\n\nimport * as Approval from \"./Approval.ts\";\nimport * as Cleanup from \"./Cleanup.ts\";\nimport * as Connect from \"./Connect.ts\";\nimport * as DnsRecord from \"./DnsRecord.ts\";\nimport * as Errors from \"./internal/error.ts\";\nimport * as Plan from \"./Plan.ts\";\nimport * as Principal from \"./Principal.ts\";\nimport * as Providers from \"./Providers.ts\";\nimport * as Provision from \"./Provision.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as Receipt from \"./Receipt.ts\";\nimport * as Storage from \"./Storage.ts\";\nimport * as Verify from \"./Verify.ts\";\n\n/** The only service a host must implement for the server. */\nexport interface IdentityService {\n readonly principal: (\n request: HttpServerRequest.HttpServerRequest,\n ) => Effect.Effect<Principal.Interface, Errors.DomainKitError>;\n /**\n * Which routes this principal may reach, checked after `principal` on every request. Fail with\n * reason `Forbidden` for the 403 a UI expects. Without it every authenticated principal reaches\n * every route, which is the right default for a host whose own middleware already gates the\n * mount.\n */\n readonly authorize?: (\n principal: Principal.Interface,\n endpoint: EndpointName,\n ) => Effect.Effect<void, Errors.DomainKitError>;\n}\n\nexport class Identity extends Context.Service<Identity, IdentityService>()(\n \"@domainkit/server/Identity\",\n) {}\n\n// ---------------------------------------------------------------------------------------------\n// Wire schemas (the transport contract `domainkit/client` and `@domainkit/react` consume)\n// ---------------------------------------------------------------------------------------------\n\nexport const ConnectionStatus = Schema.Literals([\"disconnected\", \"connected\", \"reconnect\"]);\nexport type ConnectionStatus = typeof ConnectionStatus.Type;\n\n/** One value a token method needs, so a form renders without knowing the provider. */\nexport const Field = Schema.Struct({\n name: Schema.String,\n required: Schema.Boolean,\n secret: Schema.Boolean,\n});\nexport type Field = typeof Field.Type;\n\n/** How a provider can be connected; `fields` is `null` for the interactive methods. */\nexport const MethodDescriptor = Schema.Struct({\n kind: Storage.AuthMethod,\n label: Schema.String,\n docsUrl: Schema.NullOr(Schema.String),\n fields: Schema.NullOr(Schema.Array(Field)),\n});\nexport type MethodDescriptor = typeof MethodDescriptor.Type;\n\n/** Everything the UI needs about a domain, flattened; provider context never crosses the wire. */\nexport const Snapshot = Schema.Struct({\n domain: Schema.String,\n attachmentId: Schema.NullOr(Schema.String),\n connectionId: Schema.NullOr(Schema.String),\n provider: Schema.NullOr(Schema.String),\n method: Schema.NullOr(Storage.AuthMethod),\n status: ConnectionStatus,\n lastReceiptId: Schema.NullOr(Schema.String),\n reusable: Schema.Array(\n Schema.Struct({\n connectionId: Schema.String,\n provider: Schema.String,\n method: Storage.AuthMethod,\n }),\n ),\n providers: Schema.Array(\n Schema.Struct({\n id: Schema.String,\n name: Schema.String,\n methods: Schema.Array(MethodDescriptor),\n }),\n ),\n});\nexport type Snapshot = typeof Snapshot.Type;\n\n/**\n * How the client asks to connect. `values` is keyed by the provider's declared token fields, as\n * `Snapshot.providers[].methods[].fields` names them. Secrets are plaintext in transit and never\n * come back out.\n */\nexport class Token extends Schema.TaggedClass<Token>(\"@domainkit/server/Method/Token\")(\"Token\", {\n values: Schema.Record(Schema.String, Schema.String),\n}) {}\nexport class OAuth extends Schema.TaggedClass<OAuth>(\"@domainkit/server/Method/OAuth\")(\"OAuth\", {\n returnTo: Schema.optionalKey(Schema.String),\n}) {}\nexport class Integration extends Schema.TaggedClass<Integration>(\n \"@domainkit/server/Method/Integration\",\n)(\"Integration\", { returnTo: Schema.optionalKey(Schema.String) }) {}\nexport const Method = Schema.Union([Token, OAuth, Integration]);\nexport type Method = typeof Method.Type;\n\nexport const StartPayload = Schema.Struct({\n domain: Schema.String,\n provider: Schema.String,\n method: Method,\n});\nexport type StartPayload = typeof StartPayload.Type;\n\n/** A zone the connection can serve, named for a customer to pick from. */\nexport class Candidate extends Schema.Class<Candidate>(\"@domainkit/server/Candidate\")({\n zone: Schema.String,\n label: Schema.String,\n}) {}\n\nexport class Connected extends Schema.TaggedClass<Connected>(\"@domainkit/server/Started/Connected\")(\n \"Connected\",\n { snapshot: Snapshot },\n) {}\nexport class Redirect extends Schema.TaggedClass<Redirect>(\"@domainkit/server/Started/Redirect\")(\n \"Redirect\",\n { authorizationUrl: Schema.String },\n) {}\nexport class SelectionRequired extends Schema.TaggedClass<SelectionRequired>(\n \"@domainkit/server/Started/SelectionRequired\",\n)(\"SelectionRequired\", { connectionId: Schema.String, candidates: Schema.Array(Candidate) }) {}\nexport const Started = Schema.Union([Connected, Redirect, SelectionRequired]);\nexport type Started = typeof Started.Type;\n\n/**\n * Which of the principal's connections already reaches a domain. The class names carry a\n * `Discovery` prefix because `Started` owns the unprefixed `SelectionRequired`; the wire tags are\n * the ones `Connect.Discovery` uses.\n */\nexport class DiscoveryResolved extends Schema.TaggedClass<DiscoveryResolved>(\n \"@domainkit/server/Discovery/Resolved\",\n)(\"Resolved\", { connectionId: Schema.String, zone: Schema.String, label: Schema.String }) {}\nexport class DiscoverySelectionRequired extends Schema.TaggedClass<DiscoverySelectionRequired>(\n \"@domainkit/server/Discovery/SelectionRequired\",\n)(\"SelectionRequired\", {\n candidates: Schema.Array(\n Schema.Struct({ connectionId: Schema.String, zone: Schema.String, label: Schema.String }),\n ),\n}) {}\n/** `host` names the registered provider whose nameservers serve the domain, by definition id. */\nexport class DiscoveryNotFound extends Schema.TaggedClass<DiscoveryNotFound>(\n \"@domainkit/server/Discovery/NotFound\",\n)(\"NotFound\", {\n nameservers: Schema.Array(Schema.String),\n host: Schema.NullOr(Schema.Struct({ provider: Schema.String })),\n}) {}\nexport const Discovery = Schema.Union([\n DiscoveryResolved,\n DiscoverySelectionRequired,\n DiscoveryNotFound,\n]);\nexport type Discovery = typeof Discovery.Type;\n\nexport const AttachPayload = Schema.Struct({\n domain: Schema.String,\n /** Which candidate zone to attach when `SelectionRequired` came back. */\n zone: Schema.optionalKey(Schema.String),\n});\nexport type AttachPayload = typeof AttachPayload.Type;\n\nexport const PlanPayload = Schema.Struct({\n requirements: Schema.Array(DnsRecord.Model),\n});\nexport type PlanPayload = typeof PlanPayload.Type;\n\nexport const ObservePayload = Schema.Struct({\n /** Records to observe instead of the attachment's receipt; required for an unattached domain. */\n requirements: Schema.optionalKey(Schema.Array(DnsRecord.Model)),\n});\nexport type ObservePayload = typeof ObservePayload.Type;\n\nexport const ApprovePayload = Schema.Struct({\n /** A subset of the plan's writes; omit to approve every write. */\n operationIds: Schema.optionalKey(Schema.Array(Plan.OperationId)),\n});\nexport type ApprovePayload = typeof ApprovePayload.Type;\n\nexport const RejectPayload = Schema.Struct({\n /** Why the customer declined, for the audit trail. */\n reason: Schema.optionalKey(Schema.String),\n});\nexport type RejectPayload = typeof RejectPayload.Type;\n\n/** One durable plan -> approval -> receipt lifecycle, as `GET /plans/:planId` returns it. */\nexport const Attempt = Schema.Struct({\n plan: Plan.Model,\n status: Storage.AttemptStatus,\n approval: Schema.NullOr(Approval.Model),\n receipt: Schema.NullOr(Receipt.Model),\n rejection: Schema.NullOr(Storage.Rejection),\n});\nexport type Attempt = typeof Attempt.Type;\n\n/** `Verify.Readiness` on the wire; timestamps encode as ISO strings. */\nexport const Readiness = Schema.Struct({\n domain: Schema.String,\n attachmentId: Schema.NullOr(Schema.String),\n overall: Storage.Overall,\n requirements: Schema.Array(\n Schema.Struct({\n operationId: Schema.NullOr(Plan.OperationId),\n record: DnsRecord.Model,\n status: Storage.RequirementStatus,\n evidence: Schema.Array(Verify.Evidence),\n }),\n ),\n host: Schema.Array(Verify.HostEvidence),\n checkedAt: Schema.DateTimeUtcFromString,\n nextCheckAt: Schema.NullOr(Schema.DateTimeUtcFromString),\n});\nexport type Readiness = typeof Readiness.Type;\n\nconst Redirected = HttpApiSchema.WithHeaders(HttpApiSchema.Empty(302), {\n location: Schema.String,\n});\n\n// ---------------------------------------------------------------------------------------------\n// Errors: one wire body, the status from `DomainKit.Error.httpStatus`\n// ---------------------------------------------------------------------------------------------\n\n/** `DomainKit.Error` narrowed to the reasons that answer with `status`. */\nconst errorAt = (status: number) =>\n Errors.DomainKitError.check(\n Schema.makeFilter((error: Errors.DomainKitError) => error.httpStatus === status),\n ).pipe(HttpApiSchema.status(status));\n\nconst errors = [400, 401, 403, 404, 409, 500, 501, 502, 503].map(errorAt);\n\n// ---------------------------------------------------------------------------------------------\n// The group\n// ---------------------------------------------------------------------------------------------\n\nexport const group = HttpApiGroup.make(\"domainkit\")\n .add(\n HttpApiEndpoint.get(\"inspect\", \"/domains/:domain\", {\n params: { domain: Schema.String },\n success: Snapshot,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.get(\"discover\", \"/domains/:domain/discovery\", {\n params: { domain: Schema.String },\n success: Discovery,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.post(\"start\", \"/connections\", {\n payload: StartPayload,\n success: Started,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.get(\"callback\", \"/callback/:provider\", {\n params: { provider: Schema.String },\n // `Connect.complete` reads every callback parameter off the request URL, because a provider\n // may return `error`, `teamId`, or `configurationId` too. These two are declared so the\n // OpenAPI document names what a provider is expected to send back.\n query: { state: Schema.String, code: Schema.optionalKey(Schema.String) },\n success: Redirected,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.post(\"attach\", \"/connections/:connectionId/attachments\", {\n params: { connectionId: Schema.String },\n payload: AttachPayload,\n success: Started,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.delete(\"detach\", \"/attachments/:attachmentId\", {\n params: { attachmentId: Schema.String },\n success: Schema.Void,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.delete(\"disconnect\", \"/connections/:connectionId\", {\n params: { connectionId: Schema.String },\n success: Schema.Void,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.post(\"createPlan\", \"/domains/:domain/plans\", {\n params: { domain: Schema.String },\n payload: PlanPayload,\n success: Plan.Model,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.post(\"approve\", \"/plans/:planId/approvals\", {\n params: { planId: Plan.PlanId },\n payload: ApprovePayload,\n success: Approval.Model,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.post(\"reject\", \"/plans/:planId/rejections\", {\n params: { planId: Plan.PlanId },\n payload: RejectPayload,\n success: Attempt,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.post(\"apply\", \"/approvals/:approvalId/apply\", {\n params: { approvalId: Approval.ApprovalId },\n success: Receipt.Model,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.get(\"plan\", \"/plans/:planId\", {\n params: { planId: Plan.PlanId },\n success: Attempt,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.get(\"receipt\", \"/receipts/:receiptId\", {\n params: { receiptId: Receipt.ReceiptId },\n success: Receipt.Model,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.post(\"observe\", \"/domains/:domain/observations\", {\n params: { domain: Schema.String },\n payload: ObservePayload,\n success: Readiness,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.post(\"cleanupPlan\", \"/receipts/:receiptId/cleanup-plans\", {\n params: { receiptId: Receipt.ReceiptId },\n success: Plan.Model,\n error: errors,\n }),\n );\n// Cleanup approve, reject, and apply reuse those routes: the attempt knows its kind.\n\nexport type Group = typeof group;\n\n/** Every route in the group, by name; what `Identity.authorize` is asked about. */\nexport type EndpointName = HttpApiGroup.Endpoints<Group>[\"identifier\"];\n\nexport interface Options {\n /**\n * Where the callback route is reachable from the provider, without the `/callback/:provider`\n * suffix. Defaults to the incoming request's origin and mount path.\n */\n readonly callbackBaseUrl?: string;\n /** Where interactive flows return when the callback carries no `returnTo`. */\n readonly defaultReturnTo?: string;\n}\n\n/** Everything the handlers need: the lifecycle services, Storage, and the host's `Identity`. */\nexport type Services =\n | Provision.Service\n | Cleanup.Service\n | Connect.Service\n | Verify.Service\n | Providers.Service\n | Storage.Service\n | Identity;\n\n// ---------------------------------------------------------------------------------------------\n// Handlers\n// ---------------------------------------------------------------------------------------------\n\nconst START_ROUTE = \"/connections\";\nconst callbackRoute = (provider: string) => `/callback/${encodeURIComponent(provider)}`;\n\nconst invalid = (message: string, field?: string) =>\n Errors.fail(new Reason.InvalidInput({ message, ...(field === undefined ? {} : { field }) }));\n\n/** The request as an absolute URL: the web original when there is one, else scheme plus `Host`. */\nconst absoluteUrl = (\n request: HttpServerRequest.HttpServerRequest,\n): Effect.Effect<URL, Errors.DomainKitError> =>\n Effect.suspend(() => {\n try {\n return Effect.succeed(new URL(request.originalUrl));\n } catch {\n const url = HttpServerRequest.toURL(request);\n return Option.isNone(url)\n ? invalid(\"The request has no usable origin; set callbackBaseUrl\", \"callbackBaseUrl\")\n : Effect.succeed(url.value);\n }\n });\n\n/**\n * The absolute URL the provider redirects back to. `callbackBaseUrl` wins when the host set it,\n * because a proxy that rewrites `Host` leaves the request pointing at an origin the browser never\n * sees; otherwise it follows the prefix the group is mounted at, derived by stripping the handling\n * route's own path off the request.\n */\nconst callbackUrlFor = (input: {\n readonly request: HttpServerRequest.HttpServerRequest;\n readonly provider: string;\n readonly options: Options;\n /** The path of the route serving this request, stripped to find the mount. */\n readonly route: string;\n}): Effect.Effect<URL, Errors.DomainKitError> =>\n Effect.suspend(() => {\n const suffix = `/callback/${encodeURIComponent(input.provider)}`;\n const configured = input.options.callbackBaseUrl;\n if (configured !== undefined) {\n return Effect.try({\n try: () => new URL(`${configured.replace(/\\/+$/, \"\")}${suffix}`),\n catch: () => callbackConfigurationError(`${configured} is not a URL`),\n });\n }\n return Effect.flatMap(absoluteUrl(input.request), (url) => {\n const path = url.pathname;\n if (!path.endsWith(input.route)) {\n return invalid(\n `The ${input.route} route is not mounted where it says; set callbackBaseUrl`,\n \"callbackBaseUrl\",\n );\n }\n return Effect.succeed(\n new URL(`${url.origin}${path.slice(0, path.length - input.route.length)}${suffix}`),\n );\n });\n });\n\nconst callbackConfigurationError = (message: string) =>\n new Errors.DomainKitError({\n reason: new Reason.InvalidInput({ message, field: \"callbackBaseUrl\" }),\n });\n\n/**\n * Where the callback may send the customer: a path under the callback's own base, or an absolute\n * URL on its origin. A destination that leaves the application is an open redirect, whoever\n * supplied it.\n */\nconst sameOrigin = (destination: string, callback: URL): string | null => {\n try {\n // Resolving against the callback is what makes this safe: the URL parser normalizes the forms\n // a browser would follow off-origin (`//host`, `/\\host`, `\\/host`, encoded hosts) into a real\n // origin, so comparing origins catches every one of them.\n const url = new URL(destination, callback);\n return url.origin === callback.origin ? url.toString() : null;\n } catch {\n return null;\n }\n};\n\nconst snapshotOf = (snapshot: Connect.Snapshot): Snapshot => ({\n domain: snapshot.domain,\n attachmentId: snapshot.attachment?.id ?? null,\n connectionId: snapshot.connection?.id ?? null,\n provider: snapshot.authorization?.provider ?? null,\n method: snapshot.authorization?.method ?? null,\n status:\n snapshot.connection === null\n ? \"disconnected\"\n : snapshot.authorization?.revocation === \"active\"\n ? \"connected\"\n : \"reconnect\",\n lastReceiptId: snapshot.lastReceiptId,\n reusable: snapshot.reusable.map(({ connection, provider, method }) => ({\n connectionId: connection.id,\n provider,\n method,\n })),\n providers: snapshot.providers.map(({ id, name, methods }) => ({ id, name, methods })),\n});\n\n/** The wire method as `Connect` takes it; interactive cases carry the caller's return destination. */\nconst methodOf = (method: Method): Connect.Method => {\n switch (method._tag) {\n case \"Token\":\n return Connect.Method.token(method.values);\n case \"OAuth\":\n return Connect.Method.oauth(\n method.returnTo === undefined ? {} : { returnTo: method.returnTo },\n );\n case \"Integration\":\n return Connect.Method.integration(\n method.returnTo === undefined ? {} : { returnTo: method.returnTo },\n );\n }\n};\n\nconst candidatesOf = (\n candidates: ReadonlyArray<{ readonly zone: string; readonly label: string }>,\n): ReadonlyArray<Candidate> => candidates.map(({ zone, label }) => new Candidate({ zone, label }));\n\n/**\n * Handlers for `group` inside the host's API. Every handler derives `Principal` from `Identity`\n * for the request it is serving and provides it to the lifecycle services.\n */\nexport const layer = <ApiId extends string, Groups extends HttpApiGroup.Constraint>(\n api: HttpApi.HttpApi<ApiId, Groups>,\n options: Options = {},\n): Layer.Layer<HttpApiGroup.Service<ApiId, \"domainkit\">, never, Services> =>\n // The host's API carries its own groups beside this one; only `\"domainkit\"` is implemented here,\n // and `HttpApiBuilder.group` needs the group's own endpoint types to check the handlers.\n HttpApiBuilder.group(api as unknown as HttpApi.HttpApi<ApiId, Group>, \"domainkit\", (handlers) =>\n Effect.gen(function* () {\n const identity = yield* Identity;\n const connect = yield* Connect.Service;\n const provision = yield* Provision.Service;\n const cleanup = yield* Cleanup.Service;\n const verify = yield* Verify.Service;\n const storage = yield* Storage.Service;\n\n /**\n * Run a lifecycle effect as the principal the host derives from this request, once the host\n * has allowed that principal onto this route.\n */\n const as = <A>(\n endpoint: EndpointName,\n request: HttpServerRequest.HttpServerRequest,\n effect: Effect.Effect<A, Errors.DomainKitError, Principal.Service>,\n ): Effect.Effect<A, Errors.DomainKitError> =>\n Effect.flatMap(identity.principal(request), (principal) =>\n Effect.flatMap(identity.authorize?.(principal, endpoint) ?? Effect.void, () =>\n Effect.provideService(effect, Principal.Service, principal),\n ),\n );\n\n const snapshot = (domain: string) => Effect.map(connect.inspect(domain), snapshotOf);\n\n const discovered = (discovery: Connect.Discovery): Discovery => {\n switch (discovery._tag) {\n case \"Resolved\":\n return new DiscoveryResolved({\n connectionId: discovery.connectionId,\n zone: discovery.target.zone,\n label: discovery.target.label,\n });\n case \"SelectionRequired\":\n return new DiscoverySelectionRequired({\n candidates: discovery.candidates.map(({ connectionId, target }) => ({\n connectionId,\n zone: target.zone,\n label: target.label,\n })),\n });\n case \"NotFound\":\n return new DiscoveryNotFound({\n nameservers: discovery.nameservers,\n host: discovery.host,\n });\n }\n };\n\n const connected = (domain: string) =>\n Effect.map(snapshot(domain), (value) => new Connected({ snapshot: value }));\n\n const startedOf = (\n started: Connect.Started,\n domain: string,\n ): Effect.Effect<Started, Errors.DomainKitError, Principal.Service> => {\n switch (started._tag) {\n case \"Connected\":\n return connected(domain);\n case \"Redirect\":\n return Effect.succeed(new Redirect({ authorizationUrl: started.authorizationUrl }));\n case \"SelectionRequired\":\n return Effect.succeed(\n new SelectionRequired({\n connectionId: started.connection.id,\n candidates: candidatesOf(started.candidates),\n }),\n );\n }\n };\n\n /**\n * Attach with a zone the client picked out of a `SelectionRequired`. The provider target\n * behind a zone never crosses the wire, so resolve the candidates again and match by zone.\n */\n const attachAt = (input: {\n readonly connectionId: string;\n readonly domain: string;\n readonly zone: string | undefined;\n }): Effect.Effect<Started, Errors.DomainKitError, Principal.Service> =>\n Effect.gen(function* () {\n const first = yield* connect.attach({\n connectionId: input.connectionId,\n domain: input.domain,\n });\n if (!(\"_tag\" in first)) return yield* connected(input.domain);\n if (input.zone === undefined) {\n return new SelectionRequired({\n connectionId: input.connectionId,\n candidates: candidatesOf(first.candidates),\n });\n }\n const target = first.candidates.find(({ zone }) => zone === input.zone);\n if (target === undefined) {\n return yield* Errors.fail(new Reason.NotFound({ entity: \"zone\", id: input.zone }));\n }\n yield* connect.attach({\n connectionId: input.connectionId,\n domain: input.domain,\n target,\n });\n return yield* connected(input.domain);\n });\n\n /** Provisioning and cleanup share the approve and apply routes; the attempt knows its kind. */\n const kindOf = (attempt: Storage.Attempt) =>\n attempt.kind === \"cleanup\" ? cleanup : provision;\n\n return handlers\n .handle(\"inspect\", ({ params, request }) => as(\"inspect\", request, snapshot(params.domain)))\n .handle(\"discover\", ({ params, request }) =>\n as(\"discover\", request, Effect.map(connect.discover(params.domain), discovered)),\n )\n .handle(\"start\", ({ payload, request }) =>\n as(\n \"start\",\n request,\n Effect.gen(function* () {\n // Token methods connect in one call; the interactive ones need somewhere to land.\n const callbackUrl =\n payload.method._tag === \"Token\"\n ? undefined\n : (yield* callbackUrlFor({\n request,\n provider: payload.provider,\n options,\n route: START_ROUTE,\n })).toString();\n const started = yield* connect.start({\n provider: payload.provider,\n domain: payload.domain,\n method: methodOf(payload.method),\n ...(callbackUrl === undefined ? {} : { callbackUrl }),\n });\n return yield* startedOf(started, payload.domain);\n }),\n ),\n )\n .handle(\"callback\", ({ params, query, request }) =>\n as(\n \"callback\",\n request,\n Effect.gen(function* () {\n const url = yield* absoluteUrl(request);\n // Resolve the destination against the callback's public base, not the request: a\n // proxy that rewrites `Host` leaves the request on an origin the browser never sees.\n const base = yield* callbackUrlFor({\n request,\n provider: params.provider,\n options,\n route: callbackRoute(params.provider),\n });\n // Where the customer lands comes from the continuation this owner started, never\n // from the provider's query string, and it is resolved before the callback is spent:\n // a server with nowhere to send them must not connect the provider and then fail.\n const continuation = yield* storage.continuations.get(query.state);\n const requested = continuation.returnTo ?? options.defaultReturnTo;\n if (requested === undefined) {\n return yield* invalid(\n \"The flow carried no returnTo and the server has no defaultReturnTo\",\n \"returnTo\",\n );\n }\n const destination = sameOrigin(requested, base);\n if (destination === null) {\n return yield* invalid(`${requested} leaves this application`, \"returnTo\");\n }\n yield* connect.complete({\n continuationId: query.state,\n callbackUrl: url.toString(),\n });\n return HttpApiSchema.withHeaders({\n body: undefined as void,\n headers: { location: destination },\n });\n }),\n ),\n )\n .handle(\"attach\", ({ params, payload, request }) =>\n as(\n \"attach\",\n request,\n attachAt({\n connectionId: params.connectionId,\n domain: payload.domain,\n zone: payload.zone,\n }),\n ),\n )\n .handle(\"detach\", ({ params, request }) =>\n as(\"detach\", request, connect.detach(params.attachmentId)),\n )\n .handle(\"disconnect\", ({ params, request }) =>\n as(\"disconnect\", request, connect.disconnect(params.connectionId)),\n )\n .handle(\"createPlan\", ({ params, payload, request }) =>\n as(\n \"createPlan\",\n request,\n provision.plan({ domain: params.domain, requirements: payload.requirements }),\n ),\n )\n .handle(\"approve\", ({ params, payload, request }) =>\n as(\n \"approve\",\n request,\n Effect.gen(function* () {\n const attempt = yield* storage.attempts.get(params.planId);\n return yield* kindOf(attempt).approve(params.planId, {\n ...(payload.operationIds === undefined\n ? {}\n : { operationIds: payload.operationIds, allowPartial: true }),\n });\n }),\n ),\n )\n .handle(\"reject\", ({ params, payload, request }) =>\n as(\n \"reject\",\n request,\n Effect.gen(function* () {\n const attempt = yield* storage.attempts.get(params.planId);\n return yield* kindOf(attempt).reject(params.planId, {\n ...(payload.reason === undefined ? {} : { reason: payload.reason }),\n });\n }),\n ),\n )\n .handle(\"apply\", ({ params, request }) =>\n as(\n \"apply\",\n request,\n Effect.gen(function* () {\n const attempt = yield* storage.attempts.byApproval(params.approvalId);\n return yield* kindOf(attempt).apply(params.approvalId);\n }),\n ),\n )\n .handle(\"plan\", ({ params, request }) =>\n as(\n \"plan\",\n request,\n Effect.map(storage.attempts.get(params.planId), (attempt) => ({\n plan: attempt.plan,\n status: attempt.status,\n approval: attempt.approval,\n receipt: attempt.receipt,\n rejection: attempt.rejection,\n })),\n ),\n )\n .handle(\"receipt\", ({ params, request }) =>\n as(\n \"receipt\",\n request,\n Effect.flatMap(storage.attempts.byReceipt(params.receiptId), (attempt) =>\n attempt.receipt === null\n ? Errors.fail(new Reason.NotFound({ entity: \"receipt\", id: params.receiptId }))\n : Effect.succeed(attempt.receipt),\n ),\n ),\n )\n .handle(\"observe\", ({ params, payload, request }) =>\n as(\n \"observe\",\n request,\n verify.observe({\n domain: params.domain,\n ...(payload.requirements === undefined ? {} : { requirements: payload.requirements }),\n }),\n ),\n )\n .handle(\"cleanupPlan\", ({ params, request }) =>\n as(\"cleanupPlan\", request, cleanup.plan({ receiptId: params.receiptId })),\n );\n }),\n ) as Layer.Layer<HttpApiGroup.Service<ApiId, \"domainkit\">, never, Services>;\n\n/** A standalone API for hosts not on HttpApi. Mount at any prefix. */\nexport const api: HttpApi.HttpApi<\"domainkit\", Group> = HttpApi.make(\"domainkit\").add(group);\n\nexport interface WebHandlerOptions extends Options {\n /** Mount every route under this path, e.g. `/api/domainkit`. */\n readonly prefix?: `/${string}`;\n}\n\n/** The Promise edge: a `fetch`-shaped handler over the group for hosts that are not Effect-native. */\nexport const toWebHandler = (\n services: Layer.Layer<Services, Errors.DomainKitError>,\n options: WebHandlerOptions = {},\n): {\n readonly handler: (request: Request) => Promise<Response>;\n readonly dispose: () => Promise<void>;\n} => {\n // `group.prefix` widens the endpoint paths in the type, which `Group` states unprefixed; the\n // handlers are keyed by endpoint identifier, so the routes still line up.\n const prefixed =\n options.prefix === undefined\n ? api\n : (HttpApi.make(\"domainkit\").add(group.prefix(options.prefix)) as unknown as HttpApi.HttpApi<\n \"domainkit\",\n Group\n >);\n const app = HttpApiBuilder.layer(prefixed).pipe(\n Layer.provide(layer(prefixed, options)),\n Layer.provide(services),\n Layer.provide(HttpServer.layerServices),\n );\n return HttpRouter.toWebHandler(app, { disableLogger: true });\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Server.mjs","names":["Storage.AuthMethod","DnsRecord.Model","Plan.OperationId","Plan.Model","Storage.AttemptStatus","Approval.Model","Receipt.Model","Storage.Rejection","Storage.Overall","Storage.RequirementStatus","Verify.Evidence","Verify.HostEvidence","Plan.PlanId","Approval.ApprovalId","Receipt.ReceiptId","Errors.fail","Reason.InvalidInput","Errors.DomainKitError","Connect.Service","Provision.Service","Cleanup.Service","Verify.Service","Storage.Service","Principal.Service","Reason.NotFound"],"sources":["../src/Server.ts"],"sourcesContent":["/**\n * domainkit/server — the whole lifecycle as one HttpApi group the host mounts.\n *\n * const Api = HttpApi.make(\"app\").add(Server.group)\n * const ApiLive = HttpApiBuilder.layer(Api).pipe(\n * Layer.provide([Server.layer(Api), DomainKitLive, IdentityLive]),\n * )\n *\n * The host provides `Identity` (request -> `Principal.Interface`). Everything else comes from\n * `DomainKit.layer`. Non-HttpApi hosts use `toWebHandler`. The group carries no prefix of its\n * own, so `group.prefix(\"/internal/dns\")` or a host router mount is all a different base path\n * takes; callback URLs follow the mount.\n */\nimport { Context, Effect, Layer, Option, Schema } from \"effect\";\nimport * as HttpRouter from \"effect/unstable/http/HttpRouter\";\nimport * as HttpServer from \"effect/unstable/http/HttpServer\";\nimport * as HttpServerRequest from \"effect/unstable/http/HttpServerRequest\";\nimport * as HttpApi from \"effect/unstable/httpapi/HttpApi\";\nimport * as HttpApiBuilder from \"effect/unstable/httpapi/HttpApiBuilder\";\nimport * as HttpApiEndpoint from \"effect/unstable/httpapi/HttpApiEndpoint\";\nimport * as HttpApiGroup from \"effect/unstable/httpapi/HttpApiGroup\";\nimport * as HttpApiSchema from \"effect/unstable/httpapi/HttpApiSchema\";\n\nimport * as Approval from \"./Approval.ts\";\nimport * as Cleanup from \"./Cleanup.ts\";\nimport * as Connect from \"./Connect.ts\";\nimport * as DnsRecord from \"./DnsRecord.ts\";\nimport * as Errors from \"./internal/error.ts\";\nimport * as Plan from \"./Plan.ts\";\nimport * as Principal from \"./Principal.ts\";\nimport * as Providers from \"./Providers.ts\";\nimport * as Provision from \"./Provision.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as Receipt from \"./Receipt.ts\";\nimport * as Storage from \"./Storage.ts\";\nimport * as Verify from \"./Verify.ts\";\n\n/** The only service a host must implement for the server. */\nexport interface IdentityService {\n readonly principal: (\n request: HttpServerRequest.HttpServerRequest,\n ) => Effect.Effect<Principal.Interface, Errors.DomainKitError>;\n /**\n * Which routes this principal may reach, checked after `principal` on every request. Fail with\n * reason `Forbidden` for the 403 a UI expects. Without it every authenticated principal reaches\n * every route, which is the right default for a host whose own middleware already gates the\n * mount.\n */\n readonly authorize?: (\n principal: Principal.Interface,\n endpoint: EndpointName,\n ) => Effect.Effect<void, Errors.DomainKitError>;\n}\n\nexport class Identity extends Context.Service<Identity, IdentityService>()(\n \"@domainkit/server/Identity\",\n) {}\n\n// ---------------------------------------------------------------------------------------------\n// Wire schemas (the transport contract `domainkit/client` and `@domainkit/react` consume)\n// ---------------------------------------------------------------------------------------------\n\nexport const ConnectionStatus = Schema.Literals([\"disconnected\", \"connected\", \"reconnect\"]);\nexport type ConnectionStatus = typeof ConnectionStatus.Type;\n\n/** One value a token method needs, so a form renders without knowing the provider. */\nexport const Field = Schema.Struct({\n name: Schema.String,\n required: Schema.Boolean,\n secret: Schema.Boolean,\n});\nexport type Field = typeof Field.Type;\n\n/** How a provider can be connected; `fields` is `null` for the interactive methods. */\nexport const MethodDescriptor = Schema.Struct({\n kind: Storage.AuthMethod,\n label: Schema.String,\n docsUrl: Schema.NullOr(Schema.String),\n fields: Schema.NullOr(Schema.Array(Field)),\n});\nexport type MethodDescriptor = typeof MethodDescriptor.Type;\n\n/** Everything the UI needs about a domain, flattened; provider context never crosses the wire. */\nexport const Snapshot = Schema.Struct({\n domain: Schema.String,\n attachmentId: Schema.NullOr(Schema.String),\n connectionId: Schema.NullOr(Schema.String),\n provider: Schema.NullOr(Schema.String),\n method: Schema.NullOr(Storage.AuthMethod),\n status: ConnectionStatus,\n lastReceiptId: Schema.NullOr(Schema.String),\n /** How many domains the connection serves, including this one; `0` without a connection. */\n connectionDomains: Schema.Number,\n reusable: Schema.Array(\n Schema.Struct({\n connectionId: Schema.String,\n provider: Schema.String,\n method: Storage.AuthMethod,\n }),\n ),\n providers: Schema.Array(\n Schema.Struct({\n id: Schema.String,\n name: Schema.String,\n methods: Schema.Array(MethodDescriptor),\n }),\n ),\n});\nexport type Snapshot = typeof Snapshot.Type;\n\n/**\n * How the client asks to connect. `values` is keyed by the provider's declared token fields, as\n * `Snapshot.providers[].methods[].fields` names them. Secrets are plaintext in transit and never\n * come back out.\n */\nexport class Token extends Schema.TaggedClass<Token>(\"@domainkit/server/Method/Token\")(\"Token\", {\n values: Schema.Record(Schema.String, Schema.String),\n}) {}\nexport class OAuth extends Schema.TaggedClass<OAuth>(\"@domainkit/server/Method/OAuth\")(\"OAuth\", {\n returnTo: Schema.optionalKey(Schema.String),\n}) {}\nexport class Integration extends Schema.TaggedClass<Integration>(\n \"@domainkit/server/Method/Integration\",\n)(\"Integration\", { returnTo: Schema.optionalKey(Schema.String) }) {}\nexport const Method = Schema.Union([Token, OAuth, Integration]);\nexport type Method = typeof Method.Type;\n\nexport const StartPayload = Schema.Struct({\n domain: Schema.String,\n provider: Schema.String,\n method: Method,\n});\nexport type StartPayload = typeof StartPayload.Type;\n\n/** A zone the connection can serve, named for a customer to pick from. */\nexport class Candidate extends Schema.Class<Candidate>(\"@domainkit/server/Candidate\")({\n zone: Schema.String,\n label: Schema.String,\n}) {}\n\nexport class Connected extends Schema.TaggedClass<Connected>(\"@domainkit/server/Started/Connected\")(\n \"Connected\",\n { snapshot: Snapshot },\n) {}\nexport class Redirect extends Schema.TaggedClass<Redirect>(\"@domainkit/server/Started/Redirect\")(\n \"Redirect\",\n { authorizationUrl: Schema.String },\n) {}\nexport class SelectionRequired extends Schema.TaggedClass<SelectionRequired>(\n \"@domainkit/server/Started/SelectionRequired\",\n)(\"SelectionRequired\", { connectionId: Schema.String, candidates: Schema.Array(Candidate) }) {}\nexport const Started = Schema.Union([Connected, Redirect, SelectionRequired]);\nexport type Started = typeof Started.Type;\n\n/**\n * Which of the principal's connections already reaches a domain. The class names carry a\n * `Discovery` prefix because `Started` owns the unprefixed `SelectionRequired`; the wire tags are\n * the ones `Connect.Discovery` uses.\n */\nexport class DiscoveryResolved extends Schema.TaggedClass<DiscoveryResolved>(\n \"@domainkit/server/Discovery/Resolved\",\n)(\"Resolved\", { connectionId: Schema.String, zone: Schema.String, label: Schema.String }) {}\nexport class DiscoverySelectionRequired extends Schema.TaggedClass<DiscoverySelectionRequired>(\n \"@domainkit/server/Discovery/SelectionRequired\",\n)(\"SelectionRequired\", {\n candidates: Schema.Array(\n Schema.Struct({ connectionId: Schema.String, zone: Schema.String, label: Schema.String }),\n ),\n}) {}\n/** `host` names the registered provider whose nameservers serve the domain, by definition id. */\nexport class DiscoveryNotFound extends Schema.TaggedClass<DiscoveryNotFound>(\n \"@domainkit/server/Discovery/NotFound\",\n)(\"NotFound\", {\n nameservers: Schema.Array(Schema.String),\n host: Schema.NullOr(Schema.Struct({ provider: Schema.String })),\n}) {}\nexport const Discovery = Schema.Union([\n DiscoveryResolved,\n DiscoverySelectionRequired,\n DiscoveryNotFound,\n]);\nexport type Discovery = typeof Discovery.Type;\n\nexport const AttachPayload = Schema.Struct({\n domain: Schema.String,\n /** Which candidate zone to attach when `SelectionRequired` came back. */\n zone: Schema.optionalKey(Schema.String),\n});\nexport type AttachPayload = typeof AttachPayload.Type;\n\nexport const PlanPayload = Schema.Struct({\n requirements: Schema.Array(DnsRecord.Model),\n});\nexport type PlanPayload = typeof PlanPayload.Type;\n\nexport const ObservePayload = Schema.Struct({\n /** Records to observe instead of the attachment's receipt; required for an unattached domain. */\n requirements: Schema.optionalKey(Schema.Array(DnsRecord.Model)),\n});\nexport type ObservePayload = typeof ObservePayload.Type;\n\nexport const ApprovePayload = Schema.Struct({\n /** A subset of the plan's writes; omit to approve every write. */\n operationIds: Schema.optionalKey(Schema.Array(Plan.OperationId)),\n});\nexport type ApprovePayload = typeof ApprovePayload.Type;\n\nexport const RejectPayload = Schema.Struct({\n /** Why the customer declined, for the audit trail. */\n reason: Schema.optionalKey(Schema.String),\n});\nexport type RejectPayload = typeof RejectPayload.Type;\n\n/** One durable plan -> approval -> receipt lifecycle, as `GET /plans/:planId` returns it. */\nexport const Attempt = Schema.Struct({\n plan: Plan.Model,\n status: Storage.AttemptStatus,\n approval: Schema.NullOr(Approval.Model),\n receipt: Schema.NullOr(Receipt.Model),\n rejection: Schema.NullOr(Storage.Rejection),\n});\nexport type Attempt = typeof Attempt.Type;\n\n/** `Verify.Readiness` on the wire; timestamps encode as ISO strings. */\nexport const Readiness = Schema.Struct({\n domain: Schema.String,\n attachmentId: Schema.NullOr(Schema.String),\n overall: Storage.Overall,\n requirements: Schema.Array(\n Schema.Struct({\n operationId: Schema.NullOr(Plan.OperationId),\n record: DnsRecord.Model,\n status: Storage.RequirementStatus,\n evidence: Schema.Array(Verify.Evidence),\n }),\n ),\n host: Schema.Array(Verify.HostEvidence),\n checkedAt: Schema.DateTimeUtcFromString,\n nextCheckAt: Schema.NullOr(Schema.DateTimeUtcFromString),\n});\nexport type Readiness = typeof Readiness.Type;\n\nconst Redirected = HttpApiSchema.WithHeaders(HttpApiSchema.Empty(302), {\n location: Schema.String,\n});\n\n// ---------------------------------------------------------------------------------------------\n// Errors: one wire body, the status from `DomainKit.Error.httpStatus`\n// ---------------------------------------------------------------------------------------------\n\n/** `DomainKit.Error` narrowed to the reasons that answer with `status`. */\nconst errorAt = (status: number) =>\n Errors.DomainKitError.check(\n Schema.makeFilter((error: Errors.DomainKitError) => error.httpStatus === status),\n ).pipe(HttpApiSchema.status(status));\n\nconst errors = [400, 401, 403, 404, 409, 500, 501, 502, 503].map(errorAt);\n\n// ---------------------------------------------------------------------------------------------\n// The group\n// ---------------------------------------------------------------------------------------------\n\nexport const group = HttpApiGroup.make(\"domainkit\")\n .add(\n HttpApiEndpoint.get(\"inspect\", \"/domains/:domain\", {\n params: { domain: Schema.String },\n success: Snapshot,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.get(\"discover\", \"/domains/:domain/discovery\", {\n params: { domain: Schema.String },\n success: Discovery,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.post(\"start\", \"/connections\", {\n payload: StartPayload,\n success: Started,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.get(\"callback\", \"/callback/:provider\", {\n params: { provider: Schema.String },\n // `Connect.complete` reads every callback parameter off the request URL, because a provider\n // may return `error`, `teamId`, or `configurationId` too. These two are declared so the\n // OpenAPI document names what a provider is expected to send back.\n query: { state: Schema.String, code: Schema.optionalKey(Schema.String) },\n success: Redirected,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.post(\"attach\", \"/connections/:connectionId/attachments\", {\n params: { connectionId: Schema.String },\n payload: AttachPayload,\n success: Started,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.delete(\"detach\", \"/attachments/:attachmentId\", {\n params: { attachmentId: Schema.String },\n success: Schema.Void,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.delete(\"disconnect\", \"/connections/:connectionId\", {\n params: { connectionId: Schema.String },\n success: Schema.Void,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.post(\"createPlan\", \"/domains/:domain/plans\", {\n params: { domain: Schema.String },\n payload: PlanPayload,\n success: Plan.Model,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.post(\"approve\", \"/plans/:planId/approvals\", {\n params: { planId: Plan.PlanId },\n payload: ApprovePayload,\n success: Approval.Model,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.post(\"reject\", \"/plans/:planId/rejections\", {\n params: { planId: Plan.PlanId },\n payload: RejectPayload,\n success: Attempt,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.post(\"apply\", \"/approvals/:approvalId/apply\", {\n params: { approvalId: Approval.ApprovalId },\n success: Receipt.Model,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.get(\"plan\", \"/plans/:planId\", {\n params: { planId: Plan.PlanId },\n success: Attempt,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.get(\"receipt\", \"/receipts/:receiptId\", {\n params: { receiptId: Receipt.ReceiptId },\n success: Receipt.Model,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.post(\"observe\", \"/domains/:domain/observations\", {\n params: { domain: Schema.String },\n payload: ObservePayload,\n success: Readiness,\n error: errors,\n }),\n )\n .add(\n HttpApiEndpoint.post(\"cleanupPlan\", \"/receipts/:receiptId/cleanup-plans\", {\n params: { receiptId: Receipt.ReceiptId },\n success: Plan.Model,\n error: errors,\n }),\n );\n// Cleanup approve, reject, and apply reuse those routes: the attempt knows its kind.\n\nexport type Group = typeof group;\n\n/** Every route in the group, by name; what `Identity.authorize` is asked about. */\nexport type EndpointName = HttpApiGroup.Endpoints<Group>[\"identifier\"];\n\nexport interface Options {\n /**\n * Where the callback route is reachable from the provider, without the `/callback/:provider`\n * suffix. Defaults to the incoming request's origin and mount path.\n */\n readonly callbackBaseUrl?: string;\n /** Where interactive flows return when the callback carries no `returnTo`. */\n readonly defaultReturnTo?: string;\n}\n\n/** Everything the handlers need: the lifecycle services, Storage, and the host's `Identity`. */\nexport type Services =\n | Provision.Service\n | Cleanup.Service\n | Connect.Service\n | Verify.Service\n | Providers.Service\n | Storage.Service\n | Identity;\n\n// ---------------------------------------------------------------------------------------------\n// Handlers\n// ---------------------------------------------------------------------------------------------\n\nconst START_ROUTE = \"/connections\";\nconst callbackRoute = (provider: string) => `/callback/${encodeURIComponent(provider)}`;\n\nconst invalid = (message: string, field?: string) =>\n Errors.fail(new Reason.InvalidInput({ message, ...(field === undefined ? {} : { field }) }));\n\n/** The request as an absolute URL: the web original when there is one, else scheme plus `Host`. */\nconst absoluteUrl = (\n request: HttpServerRequest.HttpServerRequest,\n): Effect.Effect<URL, Errors.DomainKitError> =>\n Effect.suspend(() => {\n try {\n return Effect.succeed(new URL(request.originalUrl));\n } catch {\n const url = HttpServerRequest.toURL(request);\n return Option.isNone(url)\n ? invalid(\"The request has no usable origin; set callbackBaseUrl\", \"callbackBaseUrl\")\n : Effect.succeed(url.value);\n }\n });\n\n/**\n * The absolute URL the provider redirects back to. `callbackBaseUrl` wins when the host set it,\n * because a proxy that rewrites `Host` leaves the request pointing at an origin the browser never\n * sees; otherwise it follows the prefix the group is mounted at, derived by stripping the handling\n * route's own path off the request.\n */\nconst callbackUrlFor = (input: {\n readonly request: HttpServerRequest.HttpServerRequest;\n readonly provider: string;\n readonly options: Options;\n /** The path of the route serving this request, stripped to find the mount. */\n readonly route: string;\n}): Effect.Effect<URL, Errors.DomainKitError> =>\n Effect.suspend(() => {\n const suffix = `/callback/${encodeURIComponent(input.provider)}`;\n const configured = input.options.callbackBaseUrl;\n if (configured !== undefined) {\n return Effect.try({\n try: () => new URL(`${configured.replace(/\\/+$/, \"\")}${suffix}`),\n catch: () => callbackConfigurationError(`${configured} is not a URL`),\n });\n }\n return Effect.flatMap(absoluteUrl(input.request), (url) => {\n const path = url.pathname;\n if (!path.endsWith(input.route)) {\n return invalid(\n `The ${input.route} route is not mounted where it says; set callbackBaseUrl`,\n \"callbackBaseUrl\",\n );\n }\n return Effect.succeed(\n new URL(`${url.origin}${path.slice(0, path.length - input.route.length)}${suffix}`),\n );\n });\n });\n\nconst callbackConfigurationError = (message: string) =>\n new Errors.DomainKitError({\n reason: new Reason.InvalidInput({ message, field: \"callbackBaseUrl\" }),\n });\n\n/**\n * Where the callback may send the customer: a path under the callback's own base, or an absolute\n * URL on its origin. A destination that leaves the application is an open redirect, whoever\n * supplied it.\n */\nconst sameOrigin = (destination: string, callback: URL): string | null => {\n try {\n // Resolving against the callback is what makes this safe: the URL parser normalizes the forms\n // a browser would follow off-origin (`//host`, `/\\host`, `\\/host`, encoded hosts) into a real\n // origin, so comparing origins catches every one of them.\n const url = new URL(destination, callback);\n return url.origin === callback.origin ? url.toString() : null;\n } catch {\n return null;\n }\n};\n\nconst snapshotOf = (snapshot: Connect.Snapshot): Snapshot => ({\n domain: snapshot.domain,\n attachmentId: snapshot.attachment?.id ?? null,\n connectionId: snapshot.connection?.id ?? null,\n provider: snapshot.authorization?.provider ?? null,\n method: snapshot.authorization?.method ?? null,\n status:\n snapshot.connection === null\n ? \"disconnected\"\n : snapshot.authorization?.revocation === \"active\"\n ? \"connected\"\n : \"reconnect\",\n lastReceiptId: snapshot.lastReceiptId,\n connectionDomains: snapshot.connectionDomains,\n reusable: snapshot.reusable.map(({ connection, provider, method }) => ({\n connectionId: connection.id,\n provider,\n method,\n })),\n providers: snapshot.providers.map(({ id, name, methods }) => ({ id, name, methods })),\n});\n\n/** The wire method as `Connect` takes it; interactive cases carry the caller's return destination. */\nconst methodOf = (method: Method): Connect.Method => {\n switch (method._tag) {\n case \"Token\":\n return Connect.Method.token(method.values);\n case \"OAuth\":\n return Connect.Method.oauth(\n method.returnTo === undefined ? {} : { returnTo: method.returnTo },\n );\n case \"Integration\":\n return Connect.Method.integration(\n method.returnTo === undefined ? {} : { returnTo: method.returnTo },\n );\n }\n};\n\nconst candidatesOf = (\n candidates: ReadonlyArray<{ readonly zone: string; readonly label: string }>,\n): ReadonlyArray<Candidate> => candidates.map(({ zone, label }) => new Candidate({ zone, label }));\n\n/**\n * Handlers for `group` inside the host's API. Every handler derives `Principal` from `Identity`\n * for the request it is serving and provides it to the lifecycle services.\n */\nexport const layer = <ApiId extends string, Groups extends HttpApiGroup.Constraint>(\n api: HttpApi.HttpApi<ApiId, Groups>,\n options: Options = {},\n): Layer.Layer<HttpApiGroup.Service<ApiId, \"domainkit\">, never, Services> =>\n // The host's API carries its own groups beside this one; only `\"domainkit\"` is implemented here,\n // and `HttpApiBuilder.group` needs the group's own endpoint types to check the handlers.\n HttpApiBuilder.group(api as unknown as HttpApi.HttpApi<ApiId, Group>, \"domainkit\", (handlers) =>\n Effect.gen(function* () {\n const identity = yield* Identity;\n const connect = yield* Connect.Service;\n const provision = yield* Provision.Service;\n const cleanup = yield* Cleanup.Service;\n const verify = yield* Verify.Service;\n const storage = yield* Storage.Service;\n\n /**\n * Run a lifecycle effect as the principal the host derives from this request, once the host\n * has allowed that principal onto this route.\n */\n const as = <A>(\n endpoint: EndpointName,\n request: HttpServerRequest.HttpServerRequest,\n effect: Effect.Effect<A, Errors.DomainKitError, Principal.Service>,\n ): Effect.Effect<A, Errors.DomainKitError> =>\n Effect.flatMap(identity.principal(request), (principal) =>\n Effect.flatMap(identity.authorize?.(principal, endpoint) ?? Effect.void, () =>\n Effect.provideService(effect, Principal.Service, principal),\n ),\n );\n\n const snapshot = (domain: string) => Effect.map(connect.inspect(domain), snapshotOf);\n\n const discovered = (discovery: Connect.Discovery): Discovery => {\n switch (discovery._tag) {\n case \"Resolved\":\n return new DiscoveryResolved({\n connectionId: discovery.connectionId,\n zone: discovery.target.zone,\n label: discovery.target.label,\n });\n case \"SelectionRequired\":\n return new DiscoverySelectionRequired({\n candidates: discovery.candidates.map(({ connectionId, target }) => ({\n connectionId,\n zone: target.zone,\n label: target.label,\n })),\n });\n case \"NotFound\":\n return new DiscoveryNotFound({\n nameservers: discovery.nameservers,\n host: discovery.host,\n });\n }\n };\n\n const connected = (domain: string) =>\n Effect.map(snapshot(domain), (value) => new Connected({ snapshot: value }));\n\n const startedOf = (\n started: Connect.Started,\n domain: string,\n ): Effect.Effect<Started, Errors.DomainKitError, Principal.Service> => {\n switch (started._tag) {\n case \"Connected\":\n return connected(domain);\n case \"Redirect\":\n return Effect.succeed(new Redirect({ authorizationUrl: started.authorizationUrl }));\n case \"SelectionRequired\":\n return Effect.succeed(\n new SelectionRequired({\n connectionId: started.connection.id,\n candidates: candidatesOf(started.candidates),\n }),\n );\n }\n };\n\n /**\n * Attach with a zone the client picked out of a `SelectionRequired`. The provider target\n * behind a zone never crosses the wire, so resolve the candidates again and match by zone.\n */\n const attachAt = (input: {\n readonly connectionId: string;\n readonly domain: string;\n readonly zone: string | undefined;\n }): Effect.Effect<Started, Errors.DomainKitError, Principal.Service> =>\n Effect.gen(function* () {\n const first = yield* connect.attach({\n connectionId: input.connectionId,\n domain: input.domain,\n });\n if (!(\"_tag\" in first)) return yield* connected(input.domain);\n if (input.zone === undefined) {\n return new SelectionRequired({\n connectionId: input.connectionId,\n candidates: candidatesOf(first.candidates),\n });\n }\n const target = first.candidates.find(({ zone }) => zone === input.zone);\n if (target === undefined) {\n return yield* Errors.fail(new Reason.NotFound({ entity: \"zone\", id: input.zone }));\n }\n yield* connect.attach({\n connectionId: input.connectionId,\n domain: input.domain,\n target,\n });\n return yield* connected(input.domain);\n });\n\n /** Provisioning and cleanup share the approve and apply routes; the attempt knows its kind. */\n const kindOf = (attempt: Storage.Attempt) =>\n attempt.kind === \"cleanup\" ? cleanup : provision;\n\n return handlers\n .handle(\"inspect\", ({ params, request }) => as(\"inspect\", request, snapshot(params.domain)))\n .handle(\"discover\", ({ params, request }) =>\n as(\"discover\", request, Effect.map(connect.discover(params.domain), discovered)),\n )\n .handle(\"start\", ({ payload, request }) =>\n as(\n \"start\",\n request,\n Effect.gen(function* () {\n // Token methods connect in one call; the interactive ones need somewhere to land.\n const callbackUrl =\n payload.method._tag === \"Token\"\n ? undefined\n : (yield* callbackUrlFor({\n request,\n provider: payload.provider,\n options,\n route: START_ROUTE,\n })).toString();\n const started = yield* connect.start({\n provider: payload.provider,\n domain: payload.domain,\n method: methodOf(payload.method),\n ...(callbackUrl === undefined ? {} : { callbackUrl }),\n });\n return yield* startedOf(started, payload.domain);\n }),\n ),\n )\n .handle(\"callback\", ({ params, query, request }) =>\n as(\n \"callback\",\n request,\n Effect.gen(function* () {\n const url = yield* absoluteUrl(request);\n // Resolve the destination against the callback's public base, not the request: a\n // proxy that rewrites `Host` leaves the request on an origin the browser never sees.\n const base = yield* callbackUrlFor({\n request,\n provider: params.provider,\n options,\n route: callbackRoute(params.provider),\n });\n // Where the customer lands comes from the continuation this owner started, never\n // from the provider's query string, and it is resolved before the callback is spent:\n // a server with nowhere to send them must not connect the provider and then fail.\n const continuation = yield* storage.continuations.get(query.state);\n const requested = continuation.returnTo ?? options.defaultReturnTo;\n if (requested === undefined) {\n return yield* invalid(\n \"The flow carried no returnTo and the server has no defaultReturnTo\",\n \"returnTo\",\n );\n }\n const destination = sameOrigin(requested, base);\n if (destination === null) {\n return yield* invalid(`${requested} leaves this application`, \"returnTo\");\n }\n yield* connect.complete({\n continuationId: query.state,\n callbackUrl: url.toString(),\n });\n return HttpApiSchema.withHeaders({\n body: undefined as void,\n headers: { location: destination },\n });\n }),\n ),\n )\n .handle(\"attach\", ({ params, payload, request }) =>\n as(\n \"attach\",\n request,\n attachAt({\n connectionId: params.connectionId,\n domain: payload.domain,\n zone: payload.zone,\n }),\n ),\n )\n .handle(\"detach\", ({ params, request }) =>\n as(\"detach\", request, connect.detach(params.attachmentId)),\n )\n .handle(\"disconnect\", ({ params, request }) =>\n as(\"disconnect\", request, connect.disconnect(params.connectionId)),\n )\n .handle(\"createPlan\", ({ params, payload, request }) =>\n as(\n \"createPlan\",\n request,\n provision.plan({ domain: params.domain, requirements: payload.requirements }),\n ),\n )\n .handle(\"approve\", ({ params, payload, request }) =>\n as(\n \"approve\",\n request,\n Effect.gen(function* () {\n const attempt = yield* storage.attempts.get(params.planId);\n return yield* kindOf(attempt).approve(params.planId, {\n ...(payload.operationIds === undefined\n ? {}\n : { operationIds: payload.operationIds, allowPartial: true }),\n });\n }),\n ),\n )\n .handle(\"reject\", ({ params, payload, request }) =>\n as(\n \"reject\",\n request,\n Effect.gen(function* () {\n const attempt = yield* storage.attempts.get(params.planId);\n return yield* kindOf(attempt).reject(params.planId, {\n ...(payload.reason === undefined ? {} : { reason: payload.reason }),\n });\n }),\n ),\n )\n .handle(\"apply\", ({ params, request }) =>\n as(\n \"apply\",\n request,\n Effect.gen(function* () {\n const attempt = yield* storage.attempts.byApproval(params.approvalId);\n return yield* kindOf(attempt).apply(params.approvalId);\n }),\n ),\n )\n .handle(\"plan\", ({ params, request }) =>\n as(\n \"plan\",\n request,\n Effect.map(storage.attempts.get(params.planId), (attempt) => ({\n plan: attempt.plan,\n status: attempt.status,\n approval: attempt.approval,\n receipt: attempt.receipt,\n rejection: attempt.rejection,\n })),\n ),\n )\n .handle(\"receipt\", ({ params, request }) =>\n as(\n \"receipt\",\n request,\n Effect.flatMap(storage.attempts.byReceipt(params.receiptId), (attempt) =>\n attempt.receipt === null\n ? Errors.fail(new Reason.NotFound({ entity: \"receipt\", id: params.receiptId }))\n : Effect.succeed(attempt.receipt),\n ),\n ),\n )\n .handle(\"observe\", ({ params, payload, request }) =>\n as(\n \"observe\",\n request,\n verify.observe({\n domain: params.domain,\n ...(payload.requirements === undefined ? {} : { requirements: payload.requirements }),\n }),\n ),\n )\n .handle(\"cleanupPlan\", ({ params, request }) =>\n as(\"cleanupPlan\", request, cleanup.plan({ receiptId: params.receiptId })),\n );\n }),\n ) as Layer.Layer<HttpApiGroup.Service<ApiId, \"domainkit\">, never, Services>;\n\n/** A standalone API for hosts not on HttpApi. Mount at any prefix. */\nexport const api: HttpApi.HttpApi<\"domainkit\", Group> = HttpApi.make(\"domainkit\").add(group);\n\nexport interface WebHandlerOptions extends Options {\n /** Mount every route under this path, e.g. `/api/domainkit`. */\n readonly prefix?: `/${string}`;\n}\n\n/** The Promise edge: a `fetch`-shaped handler over the group for hosts that are not Effect-native. */\nexport const toWebHandler = (\n services: Layer.Layer<Services, Errors.DomainKitError>,\n options: WebHandlerOptions = {},\n): {\n readonly handler: (request: Request) => Promise<Response>;\n readonly dispose: () => Promise<void>;\n} => {\n // `group.prefix` widens the endpoint paths in the type, which `Group` states unprefixed; the\n // handlers are keyed by endpoint identifier, so the routes still line up.\n const prefixed =\n options.prefix === undefined\n ? api\n : (HttpApi.make(\"domainkit\").add(group.prefix(options.prefix)) as unknown as HttpApi.HttpApi<\n \"domainkit\",\n Group\n >);\n const app = HttpApiBuilder.layer(prefixed).pipe(\n Layer.provide(layer(prefixed, options)),\n Layer.provide(services),\n Layer.provide(HttpServer.layerServices),\n );\n return HttpRouter.toWebHandler(app, { disableLogger: true });\n};\n"],"mappings":"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"}
|
package/dist/Testing.mjs
CHANGED
|
@@ -110,7 +110,7 @@ const provider = (options = {}) => {
|
|
|
110
110
|
return {
|
|
111
111
|
...make$1({
|
|
112
112
|
id,
|
|
113
|
-
name: `Fake ${id}`,
|
|
113
|
+
name: options.name ?? `Fake ${id}`,
|
|
114
114
|
...options.nameserverSuffixes === void 0 ? {} : { nameservers: options.nameserverSuffixes },
|
|
115
115
|
context: Context$1,
|
|
116
116
|
contextVersion: "fake.v1",
|
|
@@ -176,15 +176,15 @@ const fakeNameservers = (name) => [...registry].filter((zone) => zone.name === n
|
|
|
176
176
|
/** Records every fake provider currently holds for `name`, across all fake zones. */
|
|
177
177
|
const fakeRecords = (name) => [...registry].flatMap((zone) => zone.rows.map(({ record }) => record)).filter((record) => record.name === name);
|
|
178
178
|
/**
|
|
179
|
-
* A resolver answering from the fake providers' zones, or from an explicit table. Every answer
|
|
180
|
-
*
|
|
179
|
+
* A resolver answering from the fake providers' zones, or from an explicit table. Every answer is
|
|
180
|
+
* attributed to `options.id`, which a fixture that ships screenshots names after a real resolver.
|
|
181
181
|
*/
|
|
182
|
-
const resolver = (answers) => Layer.succeed(Service)({ resolve: (name, type) => Effect.sync(() => {
|
|
182
|
+
const resolver = (answers, options = {}) => Layer.succeed(Service)({ resolve: (name, type) => Effect.sync(() => {
|
|
183
183
|
const normalized = fromString(name);
|
|
184
184
|
const lookup = normalized._tag === "Some" ? normalized.value : name;
|
|
185
185
|
const records = (answers === void 0 ? [...fakeRecords(lookup), ...fakeNameservers(lookup)] : answers.filter((entry) => entry.name === lookup).flatMap((entry) => entry.records)).filter((record) => (record._tag === "Opaque" ? record.type : record._tag) === type);
|
|
186
186
|
return [Outcome.Answered({ answer: {
|
|
187
|
-
resolver: "fake",
|
|
187
|
+
resolver: options.id ?? "fake",
|
|
188
188
|
records,
|
|
189
189
|
negative: records.length === 0,
|
|
190
190
|
ttl: 60
|
|
@@ -198,7 +198,7 @@ const transport = (options = {}) => {
|
|
|
198
198
|
const fake = provider(options.provider);
|
|
199
199
|
const { handler } = toWebHandler(layerMemory$1({
|
|
200
200
|
providers: [fake],
|
|
201
|
-
resolver: resolver()
|
|
201
|
+
resolver: resolver(void 0, options.resolver)
|
|
202
202
|
}).pipe(Layer.merge(Layer.succeed(Identity)({ principal: () => Effect.succeed(principal) }))));
|
|
203
203
|
const live = fromFetch("http://domainkit.test", {
|
|
204
204
|
fetch: (input, init) => handler(new Request(input, init)),
|
package/dist/Testing.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Testing.mjs","names":["Storage.layerMemory","Principal.make","Context","DomainName.fromStringUnsafe","Errors.fail","Reason.NotFound","Reason.Unauthenticated","Provider.make","Provider.tokenAuth","DomainName.decode","Provider.resolveAmong","Reason.ProviderUnavailable","DomainName.isWithin","Reason.ProviderRejected","DnsRecord.ns","Resolver.Service","DomainName.fromString","Server.toWebHandler","DomainKit.layerMemory","Server.Identity","Transport.fromFetch","Transport.capabilities","storageCases","providerCases"],"sources":["../src/Testing.ts"],"sourcesContent":["/**\n * domainkit/testing — fakes and conformance runners so hosts test against the seam instead of\n * stubbing global fetch.\n */\nimport { DateTime, Effect, Layer, Redacted, Schema } from \"effect\";\n\nimport * as DnsRecord from \"./DnsRecord.ts\";\nimport * as DomainKit from \"./DomainKit.ts\";\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as DomainName from \"./DomainName.ts\";\nimport { provider as providerCases } from \"./internal/conformance/provider.ts\";\nimport { storage as storageCases } from \"./internal/conformance/storage.ts\";\nimport * as Principal from \"./Principal.ts\";\nimport * as Provider from \"./Provider.ts\";\nimport * as Resolver from \"./Resolver.ts\";\nimport * as Server from \"./Server.ts\";\nimport * as Storage from \"./Storage.ts\";\nimport * as Transport from \"./Transport.ts\";\n\n/** In-memory Storage. Same as `Storage.layerMemory`; re-exported for discoverability. */\nexport const storage: Layer.Layer<Storage.Service> = Storage.layerMemory;\n\nexport const principal: Principal.Interface = Principal.make({\n ownerId: \"org_test\",\n actorId: \"user_test\",\n});\n\nexport interface FakeProviderOptions {\n readonly id?: string;\n /** Zones the fake credential can reach. Default: `example.com`. */\n readonly zones?: ReadonlyArray<string>;\n /** Pre-existing records, to produce `Noop` and `Conflict` operations. */\n readonly records?: ReadonlyArray<{ readonly zone: string; readonly record: DnsRecord.Observed }>;\n /** Offer OAuth in addition to tokens; the fake redirects to `callbackUrl` immediately. */\n readonly oauth?: boolean;\n /** Fail the write at this zero-based index, to exercise partial receipts. */\n readonly failWrite?: (index: number) => boolean;\n /** Nameservers per zone; default `ns1.<zone>`, `ns2.<zone>`. `resolver()` answers NS queries from them. */\n readonly nameservers?: Readonly<Record<string, ReadonlyArray<string>>>;\n /** Suffixes the fake declares as `Definition.nameservers`, so `Connect.discover` can name it as a host. */\n readonly nameserverSuffixes?: ReadonlyArray<string>;\n}\n\nexport interface FakeProvider extends Provider.Definition<FakeContext> {\n readonly records: (zone: string) => ReadonlyArray<DnsRecord.Observed>;\n /** Every credential the fake issued, oldest first. */\n readonly issued: () => ReadonlyArray<string>;\n /** How many credentials were revoked at the fake. */\n readonly revoked: () => number;\n}\n\ninterface Zone {\n readonly name: string;\n readonly nameservers: ReadonlyArray<string>;\n readonly rows: Array<{ readonly id: string; readonly record: DnsRecord.Observed }>;\n}\n\n/** Every fake zone created in this process, so `resolver()` can answer from them. */\nconst registry = new Set<Zone>();\n\n/** Account context carries `account`; target context carries `zone`. One schema covers both. */\nconst Context = Schema.Struct({\n account: Schema.optionalKey(Schema.String),\n zone: Schema.optionalKey(Schema.String),\n});\ntype FakeContext = typeof Context.Type;\nconst TargetContext = Schema.Struct({ zone: Schema.String });\n\n/** A provider definition with a token method (and optional OAuth) over in-memory zones. */\nexport const provider = (options: FakeProviderOptions = {}): FakeProvider => {\n const id = options.id ?? \"fake\";\n const zones = new Map<string, Zone>();\n let nextId = 1;\n let writes = 0;\n let revokedCount = 0;\n const issued: Array<string> = [];\n for (const name of options.zones ?? [\"example.com\"]) {\n const normalized = DomainName.fromStringUnsafe(name);\n const zone: Zone = {\n name: normalized,\n nameservers: options.nameservers?.[name] ??\n options.nameservers?.[normalized] ?? [`ns1.${normalized}`, `ns2.${normalized}`],\n rows: [],\n };\n zones.set(zone.name, zone);\n registry.add(zone);\n }\n for (const { zone: name, record } of options.records ?? []) {\n const zone = zones.get(DomainName.fromStringUnsafe(name));\n if (zone === undefined) throw new Error(`Fake provider ${id} has no zone ${name}`);\n zone.rows.push({ id: `${id}-${nextId++}`, record });\n }\n\n const issue = (prefix: string): Provider.IssuedCredential => {\n const secret = `${prefix}-${issued.length + 1}`;\n issued.push(secret);\n return { secret: Redacted.make(secret), context: { account: id }, expiresAt: null };\n };\n\n const zoneOf = (target: Provider.Target) =>\n Effect.suspend(() => {\n const decoded = Schema.decodeUnknownOption(TargetContext)(target.context);\n const zone = decoded._tag === \"Some\" ? zones.get(decoded.value.zone) : undefined;\n return zone === undefined\n ? Errors.fail(new Reason.NotFound({ entity: \"zone\", id: target.zone }))\n : Effect.succeed(zone);\n });\n\n const targets = () =>\n [...zones.values()].map((zone): Provider.Target => ({\n zone: zone.name,\n context: { zone: zone.name },\n label: zone.name,\n nameservers: zone.nameservers,\n }));\n\n const oauth: Provider.OAuthAuth = {\n label: \"Sign in (fake)\",\n scopes: [\"dns\"],\n start: (input) =>\n Effect.succeed({\n authorizationUrl: `${input.callbackUrl}?${new URLSearchParams({\n state: input.state,\n code: \"fake-code\",\n })}`,\n }),\n complete: (input) =>\n input.code === \"fake-code\"\n ? Effect.map(DateTime.now, (now) => ({\n ...issue(\"oauth\"),\n expiresAt: DateTime.add(now, { hours: 1 }),\n }))\n : Errors.fail(new Reason.Unauthenticated({ message: \"fake provider rejected the code\" })),\n refresh: (credential) =>\n Redacted.value(credential.secret).startsWith(\"revoked\")\n ? Errors.fail(new Reason.Unauthenticated({ message: \"fake refresh token was revoked\" }))\n : Effect.map(DateTime.now, (now) => ({\n ...issue(\"oauth\"),\n expiresAt: DateTime.add(now, { hours: 1 }),\n })),\n revoke: () => Effect.sync(() => void (revokedCount += 1)),\n };\n\n const definition = Provider.make<FakeContext>({\n id,\n name: `Fake ${id}`,\n ...(options.nameserverSuffixes === undefined\n ? {}\n : { nameservers: options.nameserverSuffixes }),\n context: Context,\n contextVersion: \"fake.v1\",\n auth: {\n token: Provider.tokenAuth({\n label: \"Token (fake)\",\n requiredCapabilities: [\"dns:read\", \"dns:write\"],\n fields: Schema.Struct({ token: Schema.RedactedFromValue(Schema.String) }),\n authenticate: ({ token }) =>\n Redacted.value(token).length === 0\n ? Errors.fail(\n new Reason.Unauthenticated({\n message: \"fake provider rejected an empty token\",\n }),\n )\n : Effect.succeed({ secret: token, context: { account: id }, expiresAt: null }),\n }),\n ...(options.oauth === true ? { oauth } : {}),\n },\n session: () => ({\n capabilities: () => Effect.succeed([\"dns:read\", \"dns:write\"]),\n listTargets: () => Effect.succeed(targets()),\n resolveTarget: (domain) =>\n Effect.map(DomainName.decode(domain), (name) => Provider.resolveAmong(name, targets())),\n dns: (target) => ({\n list: () =>\n Effect.map(zoneOf(target), (zone) =>\n zone.rows.map(({ id: providerRecordId, record }) => ({ record, providerRecordId })),\n ),\n create: (_zone, record) =>\n Effect.gen(function* () {\n const zone = yield* zoneOf(target);\n const index = writes;\n writes += 1;\n if (options.failWrite?.(index) === true) {\n return yield* Errors.fail(\n new Reason.ProviderUnavailable({\n provider: id,\n message: `fake provider failed write ${index}`,\n }),\n );\n }\n if (!DomainName.isWithin(record.name, zone.name)) {\n return yield* Errors.fail(\n new Reason.ProviderRejected({\n provider: id,\n message: `${record.name} is outside ${zone.name}`,\n }),\n );\n }\n const providerRecordId = `${id}-${nextId++}`;\n zone.rows.push({ id: providerRecordId, record });\n return { providerRecordId };\n }),\n get: (_zone, providerRecordId) =>\n Effect.map(\n zoneOf(target),\n (zone) => zone.rows.find((row) => row.id === providerRecordId)?.record ?? null,\n ),\n delete: (_zone, providerRecordId) =>\n Effect.map(zoneOf(target), (zone) => {\n const index = zone.rows.findIndex((row) => row.id === providerRecordId);\n if (index >= 0) zone.rows.splice(index, 1);\n }),\n }),\n }),\n });\n\n return {\n ...definition,\n records: (zone) =>\n zones.get(DomainName.fromStringUnsafe(zone))?.rows.map(({ record }) => record) ?? [],\n issued: () => issued,\n revoked: () => revokedCount,\n };\n};\n\n/** NS records for every fake zone named `name`, so `resolver()` can answer authority queries. */\nconst fakeNameservers = (name: string): ReadonlyArray<DnsRecord.Observed> =>\n [...registry]\n .filter((zone) => zone.name === name)\n .flatMap((zone) =>\n zone.nameservers.map((nameserver) => DnsRecord.ns({ name: zone.name, nameserver })),\n );\n\n/** Records every fake provider currently holds for `name`, across all fake zones. */\nexport const fakeRecords = (name: string): ReadonlyArray<DnsRecord.Observed> =>\n [...registry]\n .flatMap((zone) => zone.rows.map(({ record }) => record))\n .filter((record) => record.name === name);\n\n/**\n * A resolver answering from the fake providers' zones, or from an explicit table. Every answer\n * is attributed to the resolver named `fake`.\n */\nexport const resolver = (\n answers?: ReadonlyArray<{\n readonly name: string;\n readonly records: ReadonlyArray<DnsRecord.Observed>;\n }>,\n): Layer.Layer<Resolver.Service> =>\n Layer.succeed(Resolver.Service)({\n resolve: (name, type) =>\n Effect.sync(() => {\n const normalized = DomainName.fromString(name);\n const lookup = normalized._tag === \"Some\" ? normalized.value : name;\n const table =\n answers === undefined\n ? [...fakeRecords(lookup), ...fakeNameservers(lookup)]\n : answers.filter((entry) => entry.name === lookup).flatMap((entry) => entry.records);\n const records = table.filter(\n (record) => (record._tag === \"Opaque\" ? record.type : record._tag) === type,\n );\n return [\n Resolver.Outcome.Answered({\n answer: { resolver: \"fake\", records, negative: records.length === 0, ttl: 60 },\n }),\n ];\n }),\n });\n\nexport interface TransportOptions {\n /** Which groups the fake server exposes, so a UI can be tested against a partial host. */\n readonly capabilities?: ReadonlyArray<Transport.Capability>;\n readonly provider?: FakeProviderOptions;\n}\n\nexport interface RecordedCall {\n /** `connection.inspect`, `provisioning.approve`, ... */\n readonly method: string;\n /** The call's only argument, or the argument list when the method takes more than one. */\n readonly input: unknown;\n}\n\ntype TransportMethod = (\n ...args: ReadonlyArray<never>\n) => Effect.Effect<unknown, Errors.DomainKitError>;\n\nexport interface RecordingTransport extends Transport.Interface {\n readonly calls: ReadonlyArray<RecordedCall>;\n}\n\n/**\n * A transport over an in-memory `domainkit/server` and memory Storage, recording every call. What\n * `@domainkit/react` tests render against, instead of stubbing global `fetch`.\n */\nexport const transport = (options: TransportOptions = {}): RecordingTransport => {\n const fake = provider(options.provider);\n // The handler's layer holds memory Storage and a throwaway custody key, so there is nothing to\n // release; a test that wants the server disposed builds `Server.toWebHandler` itself.\n const { handler } = Server.toWebHandler(\n DomainKit.layerMemory({ providers: [fake], resolver: resolver() }).pipe(\n Layer.merge(Layer.succeed(Server.Identity)({ principal: () => Effect.succeed(principal) })),\n ),\n );\n const live = Transport.fromFetch(\"http://domainkit.test\", {\n fetch: (input, init) => handler(new Request(input, init)),\n ...(options.capabilities === undefined ? {} : { capabilities: options.capabilities }),\n });\n const calls: Array<RecordedCall> = [];\n const recorded = Object.fromEntries(\n Transport.capabilities(live).map((capability) => [\n capability,\n Object.fromEntries(\n Object.entries(live[capability] as unknown as Record<string, TransportMethod>).map(\n ([name, method]) => [\n name,\n (...args: ReadonlyArray<never>) =>\n Effect.suspend(() => {\n calls.push({\n method: `${capability}.${name}`,\n input: args.length > 1 ? args : args[0],\n });\n return method(...args);\n }),\n ],\n ),\n ),\n ]),\n ) as Transport.Interface;\n return { ...recorded, calls };\n};\n\nexport type { Case as ProviderCase } from \"./internal/conformance/provider.ts\";\nexport type { Case as StorageCase } from \"./internal/conformance/storage.ts\";\n\nexport const conformance = {\n /** Runs every Storage invariant (tenant isolation, leases, exactly-once continuations, revocation recovery). */\n storage: storageCases,\n /** Runs create/readback/cleanup, exact-noop, conflict, stale-plan, partial-apply, and rejected-token against a real provider definition. */\n provider: providerCases,\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Testing.mjs","names":["Storage.layerMemory","Principal.make","Context","DomainName.fromStringUnsafe","Errors.fail","Reason.NotFound","Reason.Unauthenticated","Provider.make","Provider.tokenAuth","DomainName.decode","Provider.resolveAmong","Reason.ProviderUnavailable","DomainName.isWithin","Reason.ProviderRejected","DnsRecord.ns","Resolver.Service","DomainName.fromString","Server.toWebHandler","DomainKit.layerMemory","Server.Identity","Transport.fromFetch","Transport.capabilities","storageCases","providerCases"],"sources":["../src/Testing.ts"],"sourcesContent":["/**\n * domainkit/testing — fakes and conformance runners so hosts test against the seam instead of\n * stubbing global fetch.\n */\nimport { DateTime, Effect, Layer, Redacted, Schema } from \"effect\";\n\nimport * as DnsRecord from \"./DnsRecord.ts\";\nimport * as DomainKit from \"./DomainKit.ts\";\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as DomainName from \"./DomainName.ts\";\nimport { provider as providerCases } from \"./internal/conformance/provider.ts\";\nimport { storage as storageCases } from \"./internal/conformance/storage.ts\";\nimport * as Principal from \"./Principal.ts\";\nimport * as Provider from \"./Provider.ts\";\nimport * as Resolver from \"./Resolver.ts\";\nimport * as Server from \"./Server.ts\";\nimport * as Storage from \"./Storage.ts\";\nimport * as Transport from \"./Transport.ts\";\n\n/** In-memory Storage. Same as `Storage.layerMemory`; re-exported for discoverability. */\nexport const storage: Layer.Layer<Storage.Service> = Storage.layerMemory;\n\nexport const principal: Principal.Interface = Principal.make({\n ownerId: \"org_test\",\n actorId: \"user_test\",\n});\n\nexport interface FakeProviderOptions {\n readonly id?: string;\n /** The name a customer reads. Default `Fake <id>`; a fixture that ships screenshots sets it. */\n readonly name?: string;\n /** Zones the fake credential can reach. Default: `example.com`. */\n readonly zones?: ReadonlyArray<string>;\n /** Pre-existing records, to produce `Noop` and `Conflict` operations. */\n readonly records?: ReadonlyArray<{ readonly zone: string; readonly record: DnsRecord.Observed }>;\n /** Offer OAuth in addition to tokens; the fake redirects to `callbackUrl` immediately. */\n readonly oauth?: boolean;\n /** Fail the write at this zero-based index, to exercise partial receipts. */\n readonly failWrite?: (index: number) => boolean;\n /** Nameservers per zone; default `ns1.<zone>`, `ns2.<zone>`. `resolver()` answers NS queries from them. */\n readonly nameservers?: Readonly<Record<string, ReadonlyArray<string>>>;\n /** Suffixes the fake declares as `Definition.nameservers`, so `Connect.discover` can name it as a host. */\n readonly nameserverSuffixes?: ReadonlyArray<string>;\n}\n\nexport interface FakeProvider extends Provider.Definition<FakeContext> {\n readonly records: (zone: string) => ReadonlyArray<DnsRecord.Observed>;\n /** Every credential the fake issued, oldest first. */\n readonly issued: () => ReadonlyArray<string>;\n /** How many credentials were revoked at the fake. */\n readonly revoked: () => number;\n}\n\ninterface Zone {\n readonly name: string;\n readonly nameservers: ReadonlyArray<string>;\n readonly rows: Array<{ readonly id: string; readonly record: DnsRecord.Observed }>;\n}\n\n/** Every fake zone created in this process, so `resolver()` can answer from them. */\nconst registry = new Set<Zone>();\n\n/** Account context carries `account`; target context carries `zone`. One schema covers both. */\nconst Context = Schema.Struct({\n account: Schema.optionalKey(Schema.String),\n zone: Schema.optionalKey(Schema.String),\n});\ntype FakeContext = typeof Context.Type;\nconst TargetContext = Schema.Struct({ zone: Schema.String });\n\n/** A provider definition with a token method (and optional OAuth) over in-memory zones. */\nexport const provider = (options: FakeProviderOptions = {}): FakeProvider => {\n const id = options.id ?? \"fake\";\n const zones = new Map<string, Zone>();\n let nextId = 1;\n let writes = 0;\n let revokedCount = 0;\n const issued: Array<string> = [];\n for (const name of options.zones ?? [\"example.com\"]) {\n const normalized = DomainName.fromStringUnsafe(name);\n const zone: Zone = {\n name: normalized,\n nameservers: options.nameservers?.[name] ??\n options.nameservers?.[normalized] ?? [`ns1.${normalized}`, `ns2.${normalized}`],\n rows: [],\n };\n zones.set(zone.name, zone);\n registry.add(zone);\n }\n for (const { zone: name, record } of options.records ?? []) {\n const zone = zones.get(DomainName.fromStringUnsafe(name));\n if (zone === undefined) throw new Error(`Fake provider ${id} has no zone ${name}`);\n zone.rows.push({ id: `${id}-${nextId++}`, record });\n }\n\n const issue = (prefix: string): Provider.IssuedCredential => {\n const secret = `${prefix}-${issued.length + 1}`;\n issued.push(secret);\n return { secret: Redacted.make(secret), context: { account: id }, expiresAt: null };\n };\n\n const zoneOf = (target: Provider.Target) =>\n Effect.suspend(() => {\n const decoded = Schema.decodeUnknownOption(TargetContext)(target.context);\n const zone = decoded._tag === \"Some\" ? zones.get(decoded.value.zone) : undefined;\n return zone === undefined\n ? Errors.fail(new Reason.NotFound({ entity: \"zone\", id: target.zone }))\n : Effect.succeed(zone);\n });\n\n const targets = () =>\n [...zones.values()].map((zone): Provider.Target => ({\n zone: zone.name,\n context: { zone: zone.name },\n label: zone.name,\n nameservers: zone.nameservers,\n }));\n\n const oauth: Provider.OAuthAuth = {\n label: \"Sign in (fake)\",\n scopes: [\"dns\"],\n start: (input) =>\n Effect.succeed({\n authorizationUrl: `${input.callbackUrl}?${new URLSearchParams({\n state: input.state,\n code: \"fake-code\",\n })}`,\n }),\n complete: (input) =>\n input.code === \"fake-code\"\n ? Effect.map(DateTime.now, (now) => ({\n ...issue(\"oauth\"),\n expiresAt: DateTime.add(now, { hours: 1 }),\n }))\n : Errors.fail(new Reason.Unauthenticated({ message: \"fake provider rejected the code\" })),\n refresh: (credential) =>\n Redacted.value(credential.secret).startsWith(\"revoked\")\n ? Errors.fail(new Reason.Unauthenticated({ message: \"fake refresh token was revoked\" }))\n : Effect.map(DateTime.now, (now) => ({\n ...issue(\"oauth\"),\n expiresAt: DateTime.add(now, { hours: 1 }),\n })),\n revoke: () => Effect.sync(() => void (revokedCount += 1)),\n };\n\n const definition = Provider.make<FakeContext>({\n id,\n name: options.name ?? `Fake ${id}`,\n ...(options.nameserverSuffixes === undefined\n ? {}\n : { nameservers: options.nameserverSuffixes }),\n context: Context,\n contextVersion: \"fake.v1\",\n auth: {\n token: Provider.tokenAuth({\n label: \"Token (fake)\",\n requiredCapabilities: [\"dns:read\", \"dns:write\"],\n fields: Schema.Struct({ token: Schema.RedactedFromValue(Schema.String) }),\n authenticate: ({ token }) =>\n Redacted.value(token).length === 0\n ? Errors.fail(\n new Reason.Unauthenticated({\n message: \"fake provider rejected an empty token\",\n }),\n )\n : Effect.succeed({ secret: token, context: { account: id }, expiresAt: null }),\n }),\n ...(options.oauth === true ? { oauth } : {}),\n },\n session: () => ({\n capabilities: () => Effect.succeed([\"dns:read\", \"dns:write\"]),\n listTargets: () => Effect.succeed(targets()),\n resolveTarget: (domain) =>\n Effect.map(DomainName.decode(domain), (name) => Provider.resolveAmong(name, targets())),\n dns: (target) => ({\n list: () =>\n Effect.map(zoneOf(target), (zone) =>\n zone.rows.map(({ id: providerRecordId, record }) => ({ record, providerRecordId })),\n ),\n create: (_zone, record) =>\n Effect.gen(function* () {\n const zone = yield* zoneOf(target);\n const index = writes;\n writes += 1;\n if (options.failWrite?.(index) === true) {\n return yield* Errors.fail(\n new Reason.ProviderUnavailable({\n provider: id,\n message: `fake provider failed write ${index}`,\n }),\n );\n }\n if (!DomainName.isWithin(record.name, zone.name)) {\n return yield* Errors.fail(\n new Reason.ProviderRejected({\n provider: id,\n message: `${record.name} is outside ${zone.name}`,\n }),\n );\n }\n const providerRecordId = `${id}-${nextId++}`;\n zone.rows.push({ id: providerRecordId, record });\n return { providerRecordId };\n }),\n get: (_zone, providerRecordId) =>\n Effect.map(\n zoneOf(target),\n (zone) => zone.rows.find((row) => row.id === providerRecordId)?.record ?? null,\n ),\n delete: (_zone, providerRecordId) =>\n Effect.map(zoneOf(target), (zone) => {\n const index = zone.rows.findIndex((row) => row.id === providerRecordId);\n if (index >= 0) zone.rows.splice(index, 1);\n }),\n }),\n }),\n });\n\n return {\n ...definition,\n records: (zone) =>\n zones.get(DomainName.fromStringUnsafe(zone))?.rows.map(({ record }) => record) ?? [],\n issued: () => issued,\n revoked: () => revokedCount,\n };\n};\n\n/** NS records for every fake zone named `name`, so `resolver()` can answer authority queries. */\nconst fakeNameservers = (name: string): ReadonlyArray<DnsRecord.Observed> =>\n [...registry]\n .filter((zone) => zone.name === name)\n .flatMap((zone) =>\n zone.nameservers.map((nameserver) => DnsRecord.ns({ name: zone.name, nameserver })),\n );\n\n/** Records every fake provider currently holds for `name`, across all fake zones. */\nexport const fakeRecords = (name: string): ReadonlyArray<DnsRecord.Observed> =>\n [...registry]\n .flatMap((zone) => zone.rows.map(({ record }) => record))\n .filter((record) => record.name === name);\n\nexport interface FakeResolverOptions {\n /** The name every answer carries. A UI reads it as the observer. Default `fake`. */\n readonly id?: string;\n}\n\n/**\n * A resolver answering from the fake providers' zones, or from an explicit table. Every answer is\n * attributed to `options.id`, which a fixture that ships screenshots names after a real resolver.\n */\nexport const resolver = (\n answers?: ReadonlyArray<{\n readonly name: string;\n readonly records: ReadonlyArray<DnsRecord.Observed>;\n }>,\n options: FakeResolverOptions = {},\n): Layer.Layer<Resolver.Service> =>\n Layer.succeed(Resolver.Service)({\n resolve: (name, type) =>\n Effect.sync(() => {\n const normalized = DomainName.fromString(name);\n const lookup = normalized._tag === \"Some\" ? normalized.value : name;\n const table =\n answers === undefined\n ? [...fakeRecords(lookup), ...fakeNameservers(lookup)]\n : answers.filter((entry) => entry.name === lookup).flatMap((entry) => entry.records);\n const records = table.filter(\n (record) => (record._tag === \"Opaque\" ? record.type : record._tag) === type,\n );\n return [\n Resolver.Outcome.Answered({\n answer: {\n resolver: options.id ?? \"fake\",\n records,\n negative: records.length === 0,\n ttl: 60,\n },\n }),\n ];\n }),\n });\n\nexport interface TransportOptions {\n /** Which groups the fake server exposes, so a UI can be tested against a partial host. */\n readonly capabilities?: ReadonlyArray<Transport.Capability>;\n readonly provider?: FakeProviderOptions;\n readonly resolver?: FakeResolverOptions;\n}\n\nexport interface RecordedCall {\n /** `connection.inspect`, `provisioning.approve`, ... */\n readonly method: string;\n /** The call's only argument, or the argument list when the method takes more than one. */\n readonly input: unknown;\n}\n\ntype TransportMethod = (\n ...args: ReadonlyArray<never>\n) => Effect.Effect<unknown, Errors.DomainKitError>;\n\nexport interface RecordingTransport extends Transport.Interface {\n readonly calls: ReadonlyArray<RecordedCall>;\n}\n\n/**\n * A transport over an in-memory `domainkit/server` and memory Storage, recording every call. What\n * `@domainkit/react` tests render against, instead of stubbing global `fetch`.\n */\nexport const transport = (options: TransportOptions = {}): RecordingTransport => {\n const fake = provider(options.provider);\n // The handler's layer holds memory Storage and a throwaway custody key, so there is nothing to\n // release; a test that wants the server disposed builds `Server.toWebHandler` itself.\n const { handler } = Server.toWebHandler(\n DomainKit.layerMemory({\n providers: [fake],\n resolver: resolver(undefined, options.resolver),\n }).pipe(\n Layer.merge(Layer.succeed(Server.Identity)({ principal: () => Effect.succeed(principal) })),\n ),\n );\n const live = Transport.fromFetch(\"http://domainkit.test\", {\n fetch: (input, init) => handler(new Request(input, init)),\n ...(options.capabilities === undefined ? {} : { capabilities: options.capabilities }),\n });\n const calls: Array<RecordedCall> = [];\n const recorded = Object.fromEntries(\n Transport.capabilities(live).map((capability) => [\n capability,\n Object.fromEntries(\n Object.entries(live[capability] as unknown as Record<string, TransportMethod>).map(\n ([name, method]) => [\n name,\n (...args: ReadonlyArray<never>) =>\n Effect.suspend(() => {\n calls.push({\n method: `${capability}.${name}`,\n input: args.length > 1 ? args : args[0],\n });\n return method(...args);\n }),\n ],\n ),\n ),\n ]),\n ) as Transport.Interface;\n return { ...recorded, calls };\n};\n\nexport type { Case as ProviderCase } from \"./internal/conformance/provider.ts\";\nexport type { Case as StorageCase } from \"./internal/conformance/storage.ts\";\n\nexport const conformance = {\n /** Runs every Storage invariant (tenant isolation, leases, exactly-once continuations, revocation recovery). */\n storage: storageCases,\n /** Runs create/readback/cleanup, exact-noop, conflict, stale-plan, partial-apply, and rejected-token against a real provider definition. */\n provider: providerCases,\n};\n"],"mappings":"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"}
|
package/dist/internal/oauth.mjs
CHANGED
|
@@ -32,13 +32,32 @@ const clientAuth = (client) => {
|
|
|
32
32
|
const secret = client.clientSecret === null ? "" : Redacted.value(client.clientSecret);
|
|
33
33
|
return client.clientAuth === "client_secret_basic" ? oauth.ClientSecretBasic(secret) : oauth.ClientSecretPost(secret);
|
|
34
34
|
};
|
|
35
|
-
const
|
|
35
|
+
const plaintext = (endpoint) => endpoint !== void 0 && URL.parse(endpoint)?.protocol === "http:";
|
|
36
|
+
/**
|
|
37
|
+
* A destination nothing can move: a loopback literal, or a name RFC 6761 reserves for one. Every
|
|
38
|
+
* other name resolves wherever DNS or a hosts file says, so it is not evidence of anything.
|
|
39
|
+
*/
|
|
40
|
+
const loopback = (endpoint) => {
|
|
41
|
+
const url = endpoint === void 0 ? null : URL.parse(endpoint);
|
|
42
|
+
if (url === null || !plaintext(endpoint)) return false;
|
|
43
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
44
|
+
return host === "localhost" || host.endsWith(".localhost") || host === "::1" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* oauth4webapi refuses plaintext endpoints, which is right for every request that carries a client
|
|
48
|
+
* secret, a code, or a token. Loopback is the one automatic exception: it goes nowhere and nothing
|
|
49
|
+
* can repoint it. Any other plaintext endpoint takes a caller that said so out loud, because a name
|
|
50
|
+
* like `host.docker.internal` resolves wherever the machine has been told to resolve it.
|
|
51
|
+
*/
|
|
52
|
+
const insecureAllowed = (endpoint, allowPlaintext) => loopback(endpoint) || allowPlaintext === true && plaintext(endpoint);
|
|
53
|
+
const requestOptions = (input, fetch, signal) => ({
|
|
36
54
|
signal,
|
|
37
|
-
[oauth.
|
|
55
|
+
...insecureAllowed(input.endpoint, input.allowPlaintext) ? { [oauth.allowInsecureRequests]: true } : {},
|
|
56
|
+
...fetch === void 0 ? {} : { [oauth.customFetch]: (url, init) => fetch(url, {
|
|
38
57
|
...init,
|
|
39
58
|
body: init.body
|
|
40
|
-
})
|
|
41
|
-
};
|
|
59
|
+
}) }
|
|
60
|
+
});
|
|
42
61
|
const tokens = (response, now) => ({
|
|
43
62
|
accessToken: response.access_token,
|
|
44
63
|
refreshToken: response.refresh_token ?? null,
|
|
@@ -74,7 +93,10 @@ const exchangeCode = (input) => Effect.gen(function* () {
|
|
|
74
93
|
code: input.code,
|
|
75
94
|
state: input.state
|
|
76
95
|
}), input.state);
|
|
77
|
-
const reply = await oauth.authorizationCodeGrantRequest(server, client, clientAuth(input.client), params, input.callbackUrl, input.codeVerifier, requestOptions(
|
|
96
|
+
const reply = await oauth.authorizationCodeGrantRequest(server, client, clientAuth(input.client), params, input.callbackUrl, input.codeVerifier, requestOptions({
|
|
97
|
+
allowPlaintext: input.allowPlaintext,
|
|
98
|
+
endpoint: input.server.token_endpoint
|
|
99
|
+
}, input.fetch, signal));
|
|
78
100
|
return oauth.processAuthorizationCodeResponse(server, client, reply);
|
|
79
101
|
},
|
|
80
102
|
catch: (cause) => failure(input.provider, cause)
|
|
@@ -87,7 +109,10 @@ const refresh = (input) => Effect.gen(function* () {
|
|
|
87
109
|
const client = { client_id: input.client.clientId };
|
|
88
110
|
const response = yield* Effect.tryPromise({
|
|
89
111
|
try: async (signal) => {
|
|
90
|
-
const reply = await oauth.refreshTokenGrantRequest(server, client, clientAuth(input.client), input.refreshToken, requestOptions(
|
|
112
|
+
const reply = await oauth.refreshTokenGrantRequest(server, client, clientAuth(input.client), input.refreshToken, requestOptions({
|
|
113
|
+
allowPlaintext: input.allowPlaintext,
|
|
114
|
+
endpoint: input.server.token_endpoint
|
|
115
|
+
}, input.fetch, signal));
|
|
91
116
|
return oauth.processRefreshTokenResponse(server, client, reply);
|
|
92
117
|
},
|
|
93
118
|
catch: (cause) => failure(input.provider, cause)
|
|
@@ -102,7 +127,10 @@ const revoke = (input) => Effect.tryPromise({
|
|
|
102
127
|
try: async (signal) => {
|
|
103
128
|
const server = input.server;
|
|
104
129
|
const client = { client_id: input.client.clientId };
|
|
105
|
-
const reply = await oauth.revocationRequest(server, client, clientAuth(input.client), input.token, requestOptions(
|
|
130
|
+
const reply = await oauth.revocationRequest(server, client, clientAuth(input.client), input.token, requestOptions({
|
|
131
|
+
allowPlaintext: input.allowPlaintext,
|
|
132
|
+
endpoint: input.server.revocation_endpoint
|
|
133
|
+
}, input.fetch, signal));
|
|
106
134
|
await oauth.processRevocationResponse(reply);
|
|
107
135
|
},
|
|
108
136
|
catch: (cause) => failure(input.provider, cause)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth.mjs","names":["Errors.DomainKitError","Reason.CryptoFailed","Reason.Unauthenticated","Reason.ProviderRejected","Reason.ProviderUnavailable"],"sources":["../../src/internal/oauth.ts"],"sourcesContent":["import { DateTime, Effect, Redacted } from \"effect\";\nimport * as oauth from \"oauth4webapi\";\n\nimport * as Errors from \"./error.ts\";\nimport * as Reason from \"../Reason.ts\";\nimport type { Fetch } from \"./http.ts\";\n\nexport interface Server {\n readonly issuer: string;\n readonly authorization_endpoint: string;\n readonly token_endpoint: string;\n readonly revocation_endpoint?: string;\n}\n\nexport type ClientAuth = \"client_secret_basic\" | \"client_secret_post\" | \"none\";\n\nexport interface Client {\n readonly clientId: string;\n readonly clientSecret: Redacted.Redacted<string> | null;\n readonly clientAuth: ClientAuth;\n}\n\n/** A PKCE pair: the verifier stays in the continuation, the challenge goes to the provider. */\nexport const pkce = (): Effect.Effect<\n { readonly codeVerifier: string; readonly codeChallenge: string },\n Errors.DomainKitError\n> =>\n Effect.tryPromise({\n try: async () => {\n const codeVerifier = oauth.generateRandomCodeVerifier();\n return { codeVerifier, codeChallenge: await oauth.calculatePKCECodeChallenge(codeVerifier) };\n },\n catch: () =>\n new Errors.DomainKitError({\n reason: new Reason.CryptoFailed({ operation: \"digest\" }),\n }),\n });\n\nexport const authorizationUrl = (input: {\n readonly server: Server;\n readonly clientId: string;\n readonly scopes: ReadonlyArray<string>;\n readonly state: string;\n readonly callbackUrl: string;\n readonly codeChallenge: string;\n}): string => {\n const url = new URL(input.server.authorization_endpoint);\n url.search = new URLSearchParams({\n client_id: input.clientId,\n code_challenge: input.codeChallenge,\n code_challenge_method: \"S256\",\n redirect_uri: input.callbackUrl,\n response_type: \"code\",\n scope: input.scopes.join(\" \"),\n state: input.state,\n }).toString();\n return url.toString();\n};\n\nexport interface Tokens {\n readonly accessToken: string;\n readonly refreshToken: string | null;\n readonly expiresAt: DateTime.Utc | null;\n readonly scope: string | null;\n}\n\nconst clientAuth = (client: Client): oauth.ClientAuth => {\n if (client.clientAuth === \"none\") return oauth.None();\n const secret = client.clientSecret === null ? \"\" : Redacted.value(client.clientSecret);\n return client.clientAuth === \"client_secret_basic\"\n ? oauth.ClientSecretBasic(secret)\n : oauth.ClientSecretPost(secret);\n};\n\nconst requestOptions = (\n fetch: Fetch | undefined,\n signal: AbortSignal,\n): oauth.TokenEndpointRequestOptions =>\n fetch === undefined\n ? { signal }\n : { signal, [oauth.customFetch]: (url, init) => fetch(url, { ...init, body: init.body }) };\n\nconst tokens = (response: oauth.TokenEndpointResponse, now: DateTime.Utc): Tokens => ({\n accessToken: response.access_token,\n refreshToken: response.refresh_token ?? null,\n expiresAt:\n typeof response.expires_in === \"number\"\n ? DateTime.add(now, { seconds: response.expires_in })\n : null,\n scope: response.scope ?? null,\n});\n\nconst failure = (provider: string, cause: unknown): Errors.DomainKitError => {\n if (cause instanceof oauth.ResponseBodyError) {\n const terminal = [\"invalid_grant\", \"invalid_client\", \"unauthorized_client\"].includes(\n cause.error,\n );\n return new Errors.DomainKitError({\n reason: terminal\n ? new Reason.Unauthenticated({\n message: `${provider} rejected the grant: ${cause.error}`,\n })\n : new Reason.ProviderRejected({\n provider,\n code: cause.error,\n message: cause.error_description ?? cause.error,\n }),\n });\n }\n if (cause instanceof oauth.AuthorizationResponseError) {\n return new Errors.DomainKitError({\n reason: new Reason.Unauthenticated({\n message: `${provider} authorization failed: ${cause.error}`,\n }),\n });\n }\n return new Errors.DomainKitError({\n reason: new Reason.ProviderUnavailable({\n provider,\n message: cause instanceof Error ? cause.message : `${provider} OAuth request failed`,\n }),\n });\n};\n\nexport const exchangeCode = (input: {\n readonly provider: string;\n readonly server: Server;\n readonly client: Client;\n readonly code: string;\n readonly state: string;\n readonly callbackUrl: string;\n readonly codeVerifier: string;\n readonly fetch?: Fetch;\n}): Effect.Effect<Tokens, Errors.DomainKitError> =>\n Effect.gen(function* () {\n const now = yield* DateTime.now;\n const server = input.server as oauth.AuthorizationServer;\n const client: oauth.Client = { client_id: input.client.clientId };\n const response = yield* Effect.tryPromise({\n try: async (signal) => {\n const params = oauth.validateAuthResponse(\n server,\n client,\n new URLSearchParams({ code: input.code, state: input.state }),\n input.state,\n );\n const reply = await oauth.authorizationCodeGrantRequest(\n server,\n client,\n clientAuth(input.client),\n params,\n input.callbackUrl,\n input.codeVerifier,\n requestOptions(input.fetch, signal),\n );\n return oauth.processAuthorizationCodeResponse(server, client, reply);\n },\n catch: (cause) => failure(input.provider, cause),\n });\n return tokens(response, now);\n });\n\nexport const refresh = (input: {\n readonly provider: string;\n readonly server: Server;\n readonly client: Client;\n readonly refreshToken: string;\n readonly fetch?: Fetch;\n}): Effect.Effect<Tokens, Errors.DomainKitError> =>\n Effect.gen(function* () {\n const now = yield* DateTime.now;\n const server = input.server as oauth.AuthorizationServer;\n const client: oauth.Client = { client_id: input.client.clientId };\n const response = yield* Effect.tryPromise({\n try: async (signal) => {\n const reply = await oauth.refreshTokenGrantRequest(\n server,\n client,\n clientAuth(input.client),\n input.refreshToken,\n requestOptions(input.fetch, signal),\n );\n return oauth.processRefreshTokenResponse(server, client, reply);\n },\n catch: (cause) => failure(input.provider, cause),\n });\n const next = tokens(response, now);\n return { ...next, refreshToken: next.refreshToken ?? input.refreshToken };\n });\n\nexport const revoke = (input: {\n readonly provider: string;\n readonly server: Server;\n readonly client: Client;\n readonly token: string;\n readonly fetch?: Fetch;\n}): Effect.Effect<void, Errors.DomainKitError> =>\n Effect.tryPromise({\n try: async (signal) => {\n const server = input.server as oauth.AuthorizationServer;\n const client: oauth.Client = { client_id: input.client.clientId };\n const reply = await oauth.revocationRequest(\n server,\n client,\n clientAuth(input.client),\n input.token,\n requestOptions(input.fetch, signal),\n );\n await oauth.processRevocationResponse(reply);\n },\n catch: (cause) => failure(input.provider, cause),\n });\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"oauth.mjs","names":["Errors.DomainKitError","Reason.CryptoFailed","Reason.Unauthenticated","Reason.ProviderRejected","Reason.ProviderUnavailable"],"sources":["../../src/internal/oauth.ts"],"sourcesContent":["import { DateTime, Effect, Redacted } from \"effect\";\nimport * as oauth from \"oauth4webapi\";\n\nimport * as Errors from \"./error.ts\";\nimport * as Reason from \"../Reason.ts\";\nimport type { Fetch } from \"./http.ts\";\n\nexport interface Server {\n readonly issuer: string;\n readonly authorization_endpoint: string;\n readonly token_endpoint: string;\n readonly revocation_endpoint?: string;\n}\n\nexport type ClientAuth = \"client_secret_basic\" | \"client_secret_post\" | \"none\";\n\nexport interface Client {\n readonly clientId: string;\n readonly clientSecret: Redacted.Redacted<string> | null;\n readonly clientAuth: ClientAuth;\n}\n\n/** A PKCE pair: the verifier stays in the continuation, the challenge goes to the provider. */\nexport const pkce = (): Effect.Effect<\n { readonly codeVerifier: string; readonly codeChallenge: string },\n Errors.DomainKitError\n> =>\n Effect.tryPromise({\n try: async () => {\n const codeVerifier = oauth.generateRandomCodeVerifier();\n return { codeVerifier, codeChallenge: await oauth.calculatePKCECodeChallenge(codeVerifier) };\n },\n catch: () =>\n new Errors.DomainKitError({\n reason: new Reason.CryptoFailed({ operation: \"digest\" }),\n }),\n });\n\nexport const authorizationUrl = (input: {\n readonly server: Server;\n readonly clientId: string;\n readonly scopes: ReadonlyArray<string>;\n readonly state: string;\n readonly callbackUrl: string;\n readonly codeChallenge: string;\n}): string => {\n const url = new URL(input.server.authorization_endpoint);\n url.search = new URLSearchParams({\n client_id: input.clientId,\n code_challenge: input.codeChallenge,\n code_challenge_method: \"S256\",\n redirect_uri: input.callbackUrl,\n response_type: \"code\",\n scope: input.scopes.join(\" \"),\n state: input.state,\n }).toString();\n return url.toString();\n};\n\nexport interface Tokens {\n readonly accessToken: string;\n readonly refreshToken: string | null;\n readonly expiresAt: DateTime.Utc | null;\n readonly scope: string | null;\n}\n\nconst clientAuth = (client: Client): oauth.ClientAuth => {\n if (client.clientAuth === \"none\") return oauth.None();\n const secret = client.clientSecret === null ? \"\" : Redacted.value(client.clientSecret);\n return client.clientAuth === \"client_secret_basic\"\n ? oauth.ClientSecretBasic(secret)\n : oauth.ClientSecretPost(secret);\n};\n\nconst plaintext = (endpoint: string | undefined): boolean =>\n endpoint !== undefined && URL.parse(endpoint)?.protocol === \"http:\";\n\n/**\n * A destination nothing can move: a loopback literal, or a name RFC 6761 reserves for one. Every\n * other name resolves wherever DNS or a hosts file says, so it is not evidence of anything.\n */\nconst loopback = (endpoint: string | undefined): boolean => {\n const url = endpoint === undefined ? null : URL.parse(endpoint);\n if (url === null || !plaintext(endpoint)) return false;\n const host = url.hostname.replace(/^\\[|\\]$/g, \"\");\n return (\n host === \"localhost\" ||\n host.endsWith(\".localhost\") ||\n host === \"::1\" ||\n /^127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/.test(host)\n );\n};\n\n/**\n * oauth4webapi refuses plaintext endpoints, which is right for every request that carries a client\n * secret, a code, or a token. Loopback is the one automatic exception: it goes nowhere and nothing\n * can repoint it. Any other plaintext endpoint takes a caller that said so out loud, because a name\n * like `host.docker.internal` resolves wherever the machine has been told to resolve it.\n */\nconst insecureAllowed = (endpoint: string | undefined, allowPlaintext: boolean | undefined) =>\n loopback(endpoint) || (allowPlaintext === true && plaintext(endpoint));\n\nconst requestOptions = (\n input: {\n readonly endpoint: string | undefined;\n readonly allowPlaintext: boolean | undefined;\n },\n fetch: Fetch | undefined,\n signal: AbortSignal,\n): oauth.TokenEndpointRequestOptions => ({\n signal,\n ...(insecureAllowed(input.endpoint, input.allowPlaintext)\n ? { [oauth.allowInsecureRequests]: true }\n : {}),\n ...(fetch === undefined\n ? {}\n : { [oauth.customFetch]: (url, init) => fetch(url, { ...init, body: init.body }) }),\n});\n\nconst tokens = (response: oauth.TokenEndpointResponse, now: DateTime.Utc): Tokens => ({\n accessToken: response.access_token,\n refreshToken: response.refresh_token ?? null,\n expiresAt:\n typeof response.expires_in === \"number\"\n ? DateTime.add(now, { seconds: response.expires_in })\n : null,\n scope: response.scope ?? null,\n});\n\nconst failure = (provider: string, cause: unknown): Errors.DomainKitError => {\n if (cause instanceof oauth.ResponseBodyError) {\n const terminal = [\"invalid_grant\", \"invalid_client\", \"unauthorized_client\"].includes(\n cause.error,\n );\n return new Errors.DomainKitError({\n reason: terminal\n ? new Reason.Unauthenticated({\n message: `${provider} rejected the grant: ${cause.error}`,\n })\n : new Reason.ProviderRejected({\n provider,\n code: cause.error,\n message: cause.error_description ?? cause.error,\n }),\n });\n }\n if (cause instanceof oauth.AuthorizationResponseError) {\n return new Errors.DomainKitError({\n reason: new Reason.Unauthenticated({\n message: `${provider} authorization failed: ${cause.error}`,\n }),\n });\n }\n return new Errors.DomainKitError({\n reason: new Reason.ProviderUnavailable({\n provider,\n message: cause instanceof Error ? cause.message : `${provider} OAuth request failed`,\n }),\n });\n};\n\nexport const exchangeCode = (input: {\n readonly provider: string;\n readonly server: Server;\n readonly client: Client;\n readonly code: string;\n readonly state: string;\n readonly callbackUrl: string;\n readonly codeVerifier: string;\n readonly fetch?: Fetch;\n /** Permit an `http:` endpoint that is not loopback. Development only; see `insecureAllowed`. */\n readonly allowPlaintext?: boolean;\n}): Effect.Effect<Tokens, Errors.DomainKitError> =>\n Effect.gen(function* () {\n const now = yield* DateTime.now;\n const server = input.server as oauth.AuthorizationServer;\n const client: oauth.Client = { client_id: input.client.clientId };\n const response = yield* Effect.tryPromise({\n try: async (signal) => {\n const params = oauth.validateAuthResponse(\n server,\n client,\n new URLSearchParams({ code: input.code, state: input.state }),\n input.state,\n );\n const reply = await oauth.authorizationCodeGrantRequest(\n server,\n client,\n clientAuth(input.client),\n params,\n input.callbackUrl,\n input.codeVerifier,\n requestOptions(\n { allowPlaintext: input.allowPlaintext, endpoint: input.server.token_endpoint },\n input.fetch,\n signal,\n ),\n );\n return oauth.processAuthorizationCodeResponse(server, client, reply);\n },\n catch: (cause) => failure(input.provider, cause),\n });\n return tokens(response, now);\n });\n\nexport const refresh = (input: {\n readonly provider: string;\n readonly server: Server;\n readonly client: Client;\n readonly refreshToken: string;\n readonly fetch?: Fetch;\n readonly allowPlaintext?: boolean;\n}): Effect.Effect<Tokens, Errors.DomainKitError> =>\n Effect.gen(function* () {\n const now = yield* DateTime.now;\n const server = input.server as oauth.AuthorizationServer;\n const client: oauth.Client = { client_id: input.client.clientId };\n const response = yield* Effect.tryPromise({\n try: async (signal) => {\n const reply = await oauth.refreshTokenGrantRequest(\n server,\n client,\n clientAuth(input.client),\n input.refreshToken,\n requestOptions(\n { allowPlaintext: input.allowPlaintext, endpoint: input.server.token_endpoint },\n input.fetch,\n signal,\n ),\n );\n return oauth.processRefreshTokenResponse(server, client, reply);\n },\n catch: (cause) => failure(input.provider, cause),\n });\n const next = tokens(response, now);\n return { ...next, refreshToken: next.refreshToken ?? input.refreshToken };\n });\n\nexport const revoke = (input: {\n readonly provider: string;\n readonly server: Server;\n readonly client: Client;\n readonly token: string;\n readonly fetch?: Fetch;\n readonly allowPlaintext?: boolean;\n}): Effect.Effect<void, Errors.DomainKitError> =>\n Effect.tryPromise({\n try: async (signal) => {\n const server = input.server as oauth.AuthorizationServer;\n const client: oauth.Client = { client_id: input.client.clientId };\n const reply = await oauth.revocationRequest(\n server,\n client,\n clientAuth(input.client),\n input.token,\n requestOptions(\n { allowPlaintext: input.allowPlaintext, endpoint: input.server.revocation_endpoint },\n input.fetch,\n signal,\n ),\n );\n await oauth.processRevocationResponse(reply);\n },\n catch: (cause) => failure(input.provider, cause),\n });\n"],"mappings":"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"}
|
package/dist/package.mjs
CHANGED
|
@@ -28,6 +28,27 @@ export interface Options {
|
|
|
28
28
|
readonly scopes?: ReadonlyArray<string>;
|
|
29
29
|
/** Default `client_secret_basic`. */
|
|
30
30
|
readonly clientAuth?: OAuth.ClientAuth;
|
|
31
|
+
/**
|
|
32
|
+
* Origin for consent, token exchange, and revocation, which Cloudflare serves at
|
|
33
|
+
* `/oauth2/auth`, `/oauth2/token`, and `/oauth2/revoke` beneath it. Default
|
|
34
|
+
* `https://dash.cloudflare.com`; a stage points it at an emulator that mounts the same three
|
|
35
|
+
* paths. It stays separate from `baseUrl` because in production these are different hosts:
|
|
36
|
+
* `dash.cloudflare.com` for OAuth, `api.cloudflare.com/client/v4` for the REST API.
|
|
37
|
+
*/
|
|
38
|
+
readonly issuer?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Where the server reaches the OAuth endpoints when that differs from where the browser does,
|
|
41
|
+
* for example an API in a container reaching an emulator through `host.docker.internal`.
|
|
42
|
+
* Defaults to `issuer`.
|
|
43
|
+
*/
|
|
44
|
+
readonly serverOrigin?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Permit `http:` for these endpoints when they are not loopback, such as an emulator reached
|
|
47
|
+
* through `host.docker.internal`. Development only: the client secret, the code, and the tokens
|
|
48
|
+
* cross the network in the clear, and a name resolves wherever the machine has been told to.
|
|
49
|
+
* Loopback needs no flag.
|
|
50
|
+
*/
|
|
51
|
+
readonly allowPlaintext?: boolean;
|
|
31
52
|
};
|
|
32
53
|
readonly fetch?: Fetch;
|
|
33
54
|
readonly baseUrl?: string;
|
package/dist/types/Connect.d.ts
CHANGED
|
@@ -113,6 +113,8 @@ export interface Snapshot {
|
|
|
113
113
|
readonly connection: Storage.Connection | null;
|
|
114
114
|
readonly authorization: Pick<Storage.Authorization, "id" | "provider" | "method" | "capabilities" | "revocation"> | null;
|
|
115
115
|
readonly lastReceiptId: string | null;
|
|
116
|
+
/** How many domains this domain's connection serves, including this one; `0` without one. */
|
|
117
|
+
readonly connectionDomains: number;
|
|
116
118
|
/** Other connections of this owner that could serve this domain. */
|
|
117
119
|
readonly reusable: ReadonlyArray<{
|
|
118
120
|
readonly connection: Storage.Connection;
|
package/dist/types/Provider.d.ts
CHANGED
|
@@ -170,6 +170,10 @@ export interface MethodDescriptor {
|
|
|
170
170
|
}
|
|
171
171
|
/** Field descriptors derived from `TokenAuth.fields`: optional keys are not required, `Redacted` values are secret. */
|
|
172
172
|
export declare const tokenFields: (auth: TokenAuth) => ReadonlyArray<FieldDescriptor>;
|
|
173
|
+
/**
|
|
174
|
+
* Every method a provider offers, in the order a UI should present them: the interactive ones a
|
|
175
|
+
* customer clicks through first, then the token they have to go and fetch themselves.
|
|
176
|
+
*/
|
|
173
177
|
export declare const describeMethods: (definition: Definition) => ReadonlyArray<MethodDescriptor>;
|
|
174
178
|
export interface CallbackInput {
|
|
175
179
|
readonly code: string;
|
|
@@ -240,6 +244,7 @@ export declare const encodeContext: <Context>(definition: Definition<Context>, v
|
|
|
240
244
|
export type AuthMethod = "token" | "oauth" | "integration";
|
|
241
245
|
/** Validates a definition: an id and at least one auth method. Throws `DomainKit.Error` otherwise. */
|
|
242
246
|
export declare const make: <Context>(definition: Definition<Context>) => Definition<Context>;
|
|
247
|
+
/** The kinds alone, in the same order `describeMethods` puts them: interactive first, token last. */
|
|
243
248
|
export declare const methods: (definition: Definition) => ReadonlyArray<AuthMethod>;
|
|
244
249
|
/**
|
|
245
250
|
* Pick the most specific zone among `targets` for `domain`: exactly one match resolves, several
|
package/dist/types/Server.d.ts
CHANGED
|
@@ -74,6 +74,8 @@ export declare const Snapshot: Schema.Struct<{
|
|
|
74
74
|
readonly method: Schema.NullOr<Schema.Literals<readonly ["token", "oauth", "integration"]>>;
|
|
75
75
|
readonly status: Schema.Literals<readonly ["disconnected", "connected", "reconnect"]>;
|
|
76
76
|
readonly lastReceiptId: Schema.NullOr<Schema.String>;
|
|
77
|
+
/** How many domains the connection serves, including this one; `0` without a connection. */
|
|
78
|
+
readonly connectionDomains: Schema.Number;
|
|
77
79
|
readonly reusable: Schema.$Array<Schema.Struct<{
|
|
78
80
|
readonly connectionId: Schema.String;
|
|
79
81
|
readonly provider: Schema.String;
|
|
@@ -139,6 +141,8 @@ declare const Connected_base: Schema.Class<Connected, Schema.TaggedStruct<"Conne
|
|
|
139
141
|
readonly method: Schema.NullOr<Schema.Literals<readonly ["token", "oauth", "integration"]>>;
|
|
140
142
|
readonly status: Schema.Literals<readonly ["disconnected", "connected", "reconnect"]>;
|
|
141
143
|
readonly lastReceiptId: Schema.NullOr<Schema.String>;
|
|
144
|
+
/** How many domains the connection serves, including this one; `0` without a connection. */
|
|
145
|
+
readonly connectionDomains: Schema.Number;
|
|
142
146
|
readonly reusable: Schema.$Array<Schema.Struct<{
|
|
143
147
|
readonly connectionId: Schema.String;
|
|
144
148
|
readonly provider: Schema.String;
|
|
@@ -3975,6 +3979,8 @@ export declare const group: HttpApiGroup.HttpApiGroup<"domainkit", HttpApiEndpoi
|
|
|
3975
3979
|
readonly method: Schema.NullOr<Schema.Literals<readonly ["token", "oauth", "integration"]>>;
|
|
3976
3980
|
readonly status: Schema.Literals<readonly ["disconnected", "connected", "reconnect"]>;
|
|
3977
3981
|
readonly lastReceiptId: Schema.NullOr<Schema.String>;
|
|
3982
|
+
/** How many domains the connection serves, including this one; `0` without a connection. */
|
|
3983
|
+
readonly connectionDomains: Schema.Number;
|
|
3978
3984
|
readonly reusable: Schema.$Array<Schema.Struct<{
|
|
3979
3985
|
readonly connectionId: Schema.String;
|
|
3980
3986
|
readonly provider: Schema.String;
|