domainkit 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/Cloudflare.mjs +1 -0
- package/dist/Cloudflare.mjs.map +1 -1
- package/dist/Connect.mjs +17 -2
- package/dist/Connect.mjs.map +1 -1
- package/dist/Provider.mjs.map +1 -1
- package/dist/Resolver.mjs +1 -1
- package/dist/Resolver.mjs.map +1 -1
- package/dist/Server.mjs +9 -2
- package/dist/Server.mjs.map +1 -1
- package/dist/Testing.mjs +20 -18
- package/dist/Testing.mjs.map +1 -1
- package/dist/Transport.mjs +1 -1
- package/dist/Transport.mjs.map +1 -1
- package/dist/Vercel.mjs +3 -1
- package/dist/Vercel.mjs.map +1 -1
- package/dist/internal/cloudflare/client.mjs +10 -5
- package/dist/internal/cloudflare/client.mjs.map +1 -1
- package/dist/internal/conformance/provider.mjs +31 -18
- package/dist/internal/conformance/provider.mjs.map +1 -1
- package/dist/internal/doh.mjs +2 -1
- package/dist/internal/doh.mjs.map +1 -1
- package/dist/internal/http.mjs +9 -3
- package/dist/internal/http.mjs.map +1 -1
- package/dist/package.mjs +1 -1
- package/dist/types/Connect.d.ts +28 -0
- package/dist/types/Provider.d.ts +6 -0
- package/dist/types/Server.d.ts +4 -0
- package/dist/types/Testing.d.ts +3 -1
- package/dist/types/Vercel.d.ts +1 -1
- package/dist/types/internal/cloudflare/client.d.ts +4 -2
- package/dist/types/internal/conformance/provider.d.ts +10 -3
- package/dist/types/internal/http.d.ts +5 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NotFound, ProviderConflict, Unauthenticated } from "../../Reason.mjs";
|
|
2
2
|
import { decode, fail } from "../error.mjs";
|
|
3
|
-
import { bearer, classify, rejected, requestJson } from "../http.mjs";
|
|
3
|
+
import { bearer, classify, rejected, rejectedToken, requestJson } from "../http.mjs";
|
|
4
4
|
import { BaseEnvelope, Envelope, Record, Token, Zone } from "./protocol.mjs";
|
|
5
5
|
import { decode as decode$1, encode, isWithinZone } from "./records.mjs";
|
|
6
6
|
import { DateTime, Effect, Schema } from "effect";
|
|
@@ -16,6 +16,8 @@ const conflictCodes = /* @__PURE__ */ new Set([
|
|
|
16
16
|
81057,
|
|
17
17
|
81058
|
|
18
18
|
]);
|
|
19
|
+
/** Cloudflare answers a malformed bearer token with HTTP 400: 6003 invalid request headers, 6111 invalid Authorization format. */
|
|
20
|
+
const rejectedTokenCodes = /* @__PURE__ */ new Set([6003, 6111]);
|
|
19
21
|
const call = (options, path, result, init) => Effect.gen(function* () {
|
|
20
22
|
const reply = yield* requestJson({
|
|
21
23
|
fetch: options.fetch,
|
|
@@ -27,6 +29,7 @@ const call = (options, path, result, init) => Effect.gen(function* () {
|
|
|
27
29
|
const base = Schema.decodeUnknownOption(BaseEnvelope)(reply.body);
|
|
28
30
|
if (!reply.ok || base._tag === "None" || !base.value.success) {
|
|
29
31
|
const detail = base._tag === "Some" ? base.value.errors[0] : void 0;
|
|
32
|
+
if (detail !== void 0 && rejectedTokenCodes.has(detail.code)) return yield* fail(new Unauthenticated({ message: detail.message }));
|
|
30
33
|
if (detail !== void 0 && conflictCodes.has(detail.code)) return yield* fail(new ProviderConflict({
|
|
31
34
|
provider,
|
|
32
35
|
code: String(detail.code),
|
|
@@ -60,11 +63,13 @@ const listZones = (options, accountId) => paginate(options, (page) => {
|
|
|
60
63
|
if (accountId !== void 0) query.set("account.id", accountId);
|
|
61
64
|
return `/zones?${query}`;
|
|
62
65
|
}, Zone, 50).pipe(Effect.map((zones) => zones.filter((zone) => zone.type !== "internal")));
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Token status and expiry from a verify endpoint; `null` expiry means the token never expires. An
|
|
68
|
+
* inactive token, or a 403 from the verify endpoint, is `Unauthenticated`.
|
|
69
|
+
*/
|
|
70
|
+
const verifyToken = (options, path) => call(options, path, Token).pipe(Effect.flatMap(({ result }) => result.status === "active" ? Effect.succeed(typeof result.expires_on === "string" ? DateTime.makeUnsafe(result.expires_on) : null) : fail(new Unauthenticated({ message: `Cloudflare token is ${result.status}` }))), rejectedToken);
|
|
66
71
|
const tokenExpiry = (options, accountId) => {
|
|
67
|
-
const verify = (path) =>
|
|
72
|
+
const verify = (path) => verifyToken(options, path);
|
|
68
73
|
return verify("/user/tokens/verify").pipe(Effect.map((expiresAt) => ({
|
|
69
74
|
expiresAt,
|
|
70
75
|
kind: "user"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.mjs","names":["Reason.NotFound","Errors.fail","S","Protocol.BaseEnvelope","Reason.ProviderConflict","Errors.decode","Protocol.Envelope","Protocol.Zone","Protocol.Token","Reason.Unauthenticated","Protocol.Record","Records.isWithinZone","Records.decode","Records.encode"],"sources":["../../../src/internal/cloudflare/client.ts"],"sourcesContent":["import { DateTime, Effect, Schema as S } from \"effect\";\n\nimport type * as DnsRecord from \"../../DnsRecord.ts\";\nimport * as Errors from \"../error.ts\";\nimport * as Reason from \"../../Reason.ts\";\nimport type * as Provider from \"../../Provider.ts\";\nimport { bearer, classify, type Fetch, rejected, requestJson } from \"../http.ts\";\nimport * as Protocol from \"./protocol.ts\";\nimport * as Records from \"./records.ts\";\n\nexport const provider = \"cloudflare\";\n\nconst notFound = new Reason.NotFound({ entity: \"zone\", id: \"cloudflare\" });\n\n/** Cloudflare's \"record already exists\" family (81056 identical, 81057 same name and content, 81058 CNAME). */\nconst conflictCodes = new Set([81056, 81057, 81058]);\n\nexport interface Options {\n readonly token: string;\n readonly fetch: Fetch;\n readonly baseUrl: string;\n}\n\ntype Fx<A> = Effect.Effect<A, Errors.DomainKitError>;\n\nconst call = <R extends S.ConstraintDecoder<unknown>>(\n options: Options,\n path: string,\n result: R,\n init?: RequestInit,\n): Fx<{ readonly result: R[\"Type\"]; readonly totalPages: number | undefined }> =>\n Effect.gen(function* () {\n const reply = yield* requestJson({\n fetch: options.fetch,\n provider,\n url: `${options.baseUrl}${path}`,\n init: bearer(options.token, init),\n });\n if (reply.status === 404) return yield* Errors.fail(notFound);\n const base = S.decodeUnknownOption(Protocol.BaseEnvelope)(reply.body);\n if (!reply.ok || base._tag === \"None\" || !base.value.success) {\n const detail = base._tag === \"Some\" ? base.value.errors[0] : undefined;\n if (detail !== undefined && conflictCodes.has(detail.code)) {\n return yield* Errors.fail(\n new Reason.ProviderConflict({\n provider,\n code: String(detail.code),\n message: detail.message,\n }),\n );\n }\n return yield* Errors.fail(\n classify(\n provider,\n reply.status,\n reply.headers,\n detail?.message ?? `Cloudflare request failed with HTTP ${reply.status}`,\n detail === undefined ? undefined : String(detail.code),\n ),\n );\n }\n const contract = () =>\n rejected(provider, \"Cloudflare response did not match its API contract\", \"response\");\n const envelope = yield* Errors.decode(Protocol.Envelope, reply.body).pipe(\n Effect.mapError(contract),\n );\n const decoded: R[\"Type\"] = yield* Errors.decode(result, envelope.result).pipe(\n Effect.mapError(contract),\n );\n return { result: decoded, totalPages: envelope.result_info?.total_pages };\n });\n\nconst paginate = <R extends S.ConstraintDecoder<unknown>>(\n options: Options,\n path: (page: number) => string,\n result: R,\n perPage: number,\n): Fx<ReadonlyArray<R[\"Type\"]>> =>\n Effect.gen(function* () {\n const rows: Array<R[\"Type\"]> = [];\n let page = 1;\n while (true) {\n const reply = yield* call(options, path(page), S.Array(result));\n rows.push(...reply.result);\n const last =\n reply.totalPages === undefined ? reply.result.length < perPage : page >= reply.totalPages;\n if (last) return rows;\n page += 1;\n }\n });\n\n/** Every zone the credential can see, across accounts; internal zones cannot host records. */\nexport const listZones = (options: Options, accountId?: string): Fx<ReadonlyArray<Protocol.Zone>> =>\n paginate(\n options,\n (page) => {\n const query = new URLSearchParams({ page: String(page), per_page: \"50\" });\n if (accountId !== undefined) query.set(\"account.id\", accountId);\n return `/zones?${query}`;\n },\n Protocol.Zone,\n 50,\n ).pipe(Effect.map((zones) => zones.filter((zone) => zone.type !== \"internal\")));\n\n/** Token expiry from the verify endpoint; `null` when the token never expires or verification is unavailable. */\n/** Token status and expiry from a verify endpoint; an inactive token is `Unauthenticated`. */\nexport const verifyToken = (options: Options, path: string): Fx<DateTime.Utc | null> =>\n call(options, path, Protocol.Token).pipe(\n Effect.flatMap(({ result }) =>\n result.status === \"active\"\n ? Effect.succeed(\n typeof result.expires_on === \"string\" ? DateTime.makeUnsafe(result.expires_on) : null,\n )\n : Errors.fail(\n new Reason.Unauthenticated({\n message: `Cloudflare token is ${result.status}`,\n }),\n ),\n ),\n );\n\nexport const tokenExpiry = (\n options: Options,\n accountId: string | null,\n): Fx<{ readonly expiresAt: DateTime.Utc | null; readonly kind: \"user\" | \"account\" }> => {\n const verify = (path: string) =>\n call(options, path, Protocol.Token).pipe(\n Effect.flatMap(({ result }) =>\n result.status === \"active\"\n ? Effect.succeed(\n typeof result.expires_on === \"string\" ? DateTime.makeUnsafe(result.expires_on) : null,\n )\n : Errors.fail(\n new Reason.Unauthenticated({\n message: `Cloudflare token is ${result.status}`,\n }),\n ),\n ),\n );\n return verify(\"/user/tokens/verify\").pipe(\n Effect.map((expiresAt) => ({ expiresAt, kind: \"user\" as const })),\n Effect.catchIf(\n (error) => error.reason._tag !== \"Unauthenticated\" || accountId !== null,\n () =>\n accountId === null\n ? Effect.succeed({ expiresAt: null, kind: \"user\" as const })\n : verify(`/accounts/${encodeURIComponent(accountId)}/tokens/verify`).pipe(\n Effect.map((expiresAt) => ({ expiresAt, kind: \"account\" as const })),\n Effect.catchIf(\n (error) => error.reason._tag !== \"Unauthenticated\",\n () => Effect.succeed({ expiresAt: null, kind: \"user\" as const }),\n ),\n ),\n ),\n );\n};\n\nexport const dns = (options: Options, zoneId: string): Provider.Dns => {\n const base = `/zones/${encodeURIComponent(zoneId)}/dns_records`;\n return {\n list: (zone) =>\n paginate(\n options,\n (page) => `${base}?${new URLSearchParams({ page: String(page), per_page: \"100\" })}`,\n Protocol.Record,\n 100,\n ).pipe(\n Effect.flatMap((records) =>\n Effect.forEach(\n records.filter((record) => Records.isWithinZone(record.name, zone)),\n Records.decode,\n ),\n ),\n ),\n create: (_zone, record: DnsRecord.Model) =>\n call(options, base, Protocol.Record, {\n method: \"POST\",\n body: JSON.stringify(Records.encode(record)),\n }).pipe(Effect.map(({ result }) => ({ providerRecordId: result.id }))),\n get: (_zone, providerRecordId) =>\n call(options, `${base}/${encodeURIComponent(providerRecordId)}`, Protocol.Record).pipe(\n Effect.flatMap(({ result }) => Records.decode(result)),\n Effect.map(({ record }) => record),\n Effect.catchIf(\n (error) => error.reason === notFound,\n () => Effect.succeed(null),\n ),\n ),\n delete: (_zone, providerRecordId) =>\n call(options, `${base}/${encodeURIComponent(providerRecordId)}`, S.Unknown, {\n method: \"DELETE\",\n }).pipe(Effect.asVoid),\n };\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.mjs","names":["Reason.NotFound","Errors.fail","S","Protocol.BaseEnvelope","Reason.Unauthenticated","Reason.ProviderConflict","Errors.decode","Protocol.Envelope","Protocol.Zone","Protocol.Token","Protocol.Record","Records.isWithinZone","Records.decode","Records.encode"],"sources":["../../../src/internal/cloudflare/client.ts"],"sourcesContent":["import { DateTime, Effect, Schema as S } from \"effect\";\n\nimport type * as DnsRecord from \"../../DnsRecord.ts\";\nimport * as Errors from \"../error.ts\";\nimport * as Reason from \"../../Reason.ts\";\nimport type * as Provider from \"../../Provider.ts\";\nimport { bearer, classify, type Fetch, rejected, rejectedToken, requestJson } from \"../http.ts\";\nimport * as Protocol from \"./protocol.ts\";\nimport * as Records from \"./records.ts\";\n\nexport const provider = \"cloudflare\";\n\nconst notFound = new Reason.NotFound({ entity: \"zone\", id: \"cloudflare\" });\n\n/** Cloudflare's \"record already exists\" family (81056 identical, 81057 same name and content, 81058 CNAME). */\nconst conflictCodes = new Set([81056, 81057, 81058]);\n\n/** Cloudflare answers a malformed bearer token with HTTP 400: 6003 invalid request headers, 6111 invalid Authorization format. */\nconst rejectedTokenCodes = new Set([6003, 6111]);\n\nexport interface Options {\n readonly token: string;\n readonly fetch: Fetch;\n readonly baseUrl: string;\n}\n\ntype Fx<A> = Effect.Effect<A, Errors.DomainKitError>;\n\nconst call = <R extends S.ConstraintDecoder<unknown>>(\n options: Options,\n path: string,\n result: R,\n init?: RequestInit,\n): Fx<{ readonly result: R[\"Type\"]; readonly totalPages: number | undefined }> =>\n Effect.gen(function* () {\n const reply = yield* requestJson({\n fetch: options.fetch,\n provider,\n url: `${options.baseUrl}${path}`,\n init: bearer(options.token, init),\n });\n if (reply.status === 404) return yield* Errors.fail(notFound);\n const base = S.decodeUnknownOption(Protocol.BaseEnvelope)(reply.body);\n if (!reply.ok || base._tag === \"None\" || !base.value.success) {\n const detail = base._tag === \"Some\" ? base.value.errors[0] : undefined;\n if (detail !== undefined && rejectedTokenCodes.has(detail.code)) {\n return yield* Errors.fail(new Reason.Unauthenticated({ message: detail.message }));\n }\n if (detail !== undefined && conflictCodes.has(detail.code)) {\n return yield* Errors.fail(\n new Reason.ProviderConflict({\n provider,\n code: String(detail.code),\n message: detail.message,\n }),\n );\n }\n return yield* Errors.fail(\n classify(\n provider,\n reply.status,\n reply.headers,\n detail?.message ?? `Cloudflare request failed with HTTP ${reply.status}`,\n detail === undefined ? undefined : String(detail.code),\n ),\n );\n }\n const contract = () =>\n rejected(provider, \"Cloudflare response did not match its API contract\", \"response\");\n const envelope = yield* Errors.decode(Protocol.Envelope, reply.body).pipe(\n Effect.mapError(contract),\n );\n const decoded: R[\"Type\"] = yield* Errors.decode(result, envelope.result).pipe(\n Effect.mapError(contract),\n );\n return { result: decoded, totalPages: envelope.result_info?.total_pages };\n });\n\nconst paginate = <R extends S.ConstraintDecoder<unknown>>(\n options: Options,\n path: (page: number) => string,\n result: R,\n perPage: number,\n): Fx<ReadonlyArray<R[\"Type\"]>> =>\n Effect.gen(function* () {\n const rows: Array<R[\"Type\"]> = [];\n let page = 1;\n while (true) {\n const reply = yield* call(options, path(page), S.Array(result));\n rows.push(...reply.result);\n const last =\n reply.totalPages === undefined ? reply.result.length < perPage : page >= reply.totalPages;\n if (last) return rows;\n page += 1;\n }\n });\n\n/** Every zone the credential can see, across accounts; internal zones cannot host records. */\nexport const listZones = (options: Options, accountId?: string): Fx<ReadonlyArray<Protocol.Zone>> =>\n paginate(\n options,\n (page) => {\n const query = new URLSearchParams({ page: String(page), per_page: \"50\" });\n if (accountId !== undefined) query.set(\"account.id\", accountId);\n return `/zones?${query}`;\n },\n Protocol.Zone,\n 50,\n ).pipe(Effect.map((zones) => zones.filter((zone) => zone.type !== \"internal\")));\n\n/**\n * Token status and expiry from a verify endpoint; `null` expiry means the token never expires. An\n * inactive token, or a 403 from the verify endpoint, is `Unauthenticated`.\n */\nexport const verifyToken = (options: Options, path: string): Fx<DateTime.Utc | null> =>\n call(options, path, Protocol.Token).pipe(\n Effect.flatMap(({ result }) =>\n result.status === \"active\"\n ? Effect.succeed(\n typeof result.expires_on === \"string\" ? DateTime.makeUnsafe(result.expires_on) : null,\n )\n : Errors.fail(\n new Reason.Unauthenticated({\n message: `Cloudflare token is ${result.status}`,\n }),\n ),\n ),\n rejectedToken,\n );\n\nexport const tokenExpiry = (\n options: Options,\n accountId: string | null,\n): Fx<{ readonly expiresAt: DateTime.Utc | null; readonly kind: \"user\" | \"account\" }> => {\n const verify = (path: string) => verifyToken(options, path);\n return verify(\"/user/tokens/verify\").pipe(\n Effect.map((expiresAt) => ({ expiresAt, kind: \"user\" as const })),\n Effect.catchIf(\n (error) => error.reason._tag !== \"Unauthenticated\" || accountId !== null,\n () =>\n accountId === null\n ? Effect.succeed({ expiresAt: null, kind: \"user\" as const })\n : verify(`/accounts/${encodeURIComponent(accountId)}/tokens/verify`).pipe(\n Effect.map((expiresAt) => ({ expiresAt, kind: \"account\" as const })),\n Effect.catchIf(\n (error) => error.reason._tag !== \"Unauthenticated\",\n () => Effect.succeed({ expiresAt: null, kind: \"user\" as const }),\n ),\n ),\n ),\n );\n};\n\nexport const dns = (options: Options, zoneId: string): Provider.Dns => {\n const base = `/zones/${encodeURIComponent(zoneId)}/dns_records`;\n return {\n list: (zone) =>\n paginate(\n options,\n (page) => `${base}?${new URLSearchParams({ page: String(page), per_page: \"100\" })}`,\n Protocol.Record,\n 100,\n ).pipe(\n Effect.flatMap((records) =>\n Effect.forEach(\n records.filter((record) => Records.isWithinZone(record.name, zone)),\n Records.decode,\n ),\n ),\n ),\n create: (_zone, record: DnsRecord.Model) =>\n call(options, base, Protocol.Record, {\n method: \"POST\",\n body: JSON.stringify(Records.encode(record)),\n }).pipe(Effect.map(({ result }) => ({ providerRecordId: result.id }))),\n get: (_zone, providerRecordId) =>\n call(options, `${base}/${encodeURIComponent(providerRecordId)}`, Protocol.Record).pipe(\n Effect.flatMap(({ result }) => Records.decode(result)),\n Effect.map(({ record }) => record),\n Effect.catchIf(\n (error) => error.reason === notFound,\n () => Effect.succeed(null),\n ),\n ),\n delete: (_zone, providerRecordId) =>\n call(options, `${base}/${encodeURIComponent(providerRecordId)}`, S.Unknown, {\n method: \"DELETE\",\n }).pipe(Effect.asVoid),\n };\n};\n"],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@ import { DomainKitError, fail } from "../error.mjs";
|
|
|
4
4
|
import { Service } from "../../Custody.mjs";
|
|
5
5
|
import { fresh } from "../ids.mjs";
|
|
6
6
|
import { Service as Service$1, make } from "../../Principal.mjs";
|
|
7
|
-
import { encodeContext } from "../../Provider.mjs";
|
|
7
|
+
import { encodeContext, tokenFields } from "../../Provider.mjs";
|
|
8
8
|
import { Service as Service$2 } from "../../Resolver.mjs";
|
|
9
9
|
import { applied } from "../../Receipt.mjs";
|
|
10
10
|
import { Attachment, Authorization, Credential, Service as Service$3 } from "../../Storage.mjs";
|
|
@@ -12,7 +12,7 @@ import { attach, session } from "../../Connect.mjs";
|
|
|
12
12
|
import { apply, approve, plan } from "../../Provision.mjs";
|
|
13
13
|
import { apply as apply$1, approve as approve$1, plan as plan$1 } from "../../Cleanup.mjs";
|
|
14
14
|
import { layerMemory } from "../../DomainKit.mjs";
|
|
15
|
-
import { DateTime, Effect, Layer } from "effect";
|
|
15
|
+
import { DateTime, Effect, Layer, Redacted } from "effect";
|
|
16
16
|
//#region src/internal/conformance/provider.ts
|
|
17
17
|
const principal = make({
|
|
18
18
|
ownerId: "conformance",
|
|
@@ -67,8 +67,9 @@ const txt = (zone, label, value) => txt$1({
|
|
|
67
67
|
});
|
|
68
68
|
const recordIds = (receipt) => applied(receipt).map(({ providerRecordId }) => providerRecordId);
|
|
69
69
|
/**
|
|
70
|
-
* Runs create/readback/cleanup, exact-noop, conflict, stale-plan,
|
|
71
|
-
* real provider definition, through the same Provision and Cleanup
|
|
70
|
+
* Runs create/readback/cleanup, exact-noop, conflict, stale-plan, partial-apply, and
|
|
71
|
+
* rejected-token against a real provider definition, through the same Provision and Cleanup
|
|
72
|
+
* services hosts use.
|
|
72
73
|
*/
|
|
73
74
|
const provider = (definition, credential, zone, options = {}) => {
|
|
74
75
|
const prefix = options.prefix ?? "domainkit-conformance";
|
|
@@ -86,12 +87,12 @@ const provider = (definition, credential, zone, options = {}) => {
|
|
|
86
87
|
const name = "create-readback-cleanup";
|
|
87
88
|
return Effect.gen(function* () {
|
|
88
89
|
const requirements = [cname(zone, `${prefix}-create`), txt(zone, `${prefix}-page`, "domainkit")];
|
|
89
|
-
const plan$
|
|
90
|
+
const plan$4 = yield* plan({
|
|
90
91
|
domain: attachment.domain,
|
|
91
92
|
requirements
|
|
92
93
|
});
|
|
93
|
-
yield* expect(name, plan$
|
|
94
|
-
const receipt = yield* apply(yield* approve(plan$
|
|
94
|
+
yield* expect(name, plan$4.operations.every(({ _tag }) => _tag === "Create"), "expected two Create operations");
|
|
95
|
+
const receipt = yield* apply(yield* approve(plan$4));
|
|
95
96
|
created = recordIds(receipt);
|
|
96
97
|
yield* expect(name, receipt.status === "complete" && created.length === 2, "apply must return a complete receipt with provider record ids");
|
|
97
98
|
const observed = yield* dns.list(zone);
|
|
@@ -111,12 +112,12 @@ const provider = (definition, credential, zone, options = {}) => {
|
|
|
111
112
|
const requirement = cname(zone, `${prefix}-exact`);
|
|
112
113
|
const { providerRecordId } = yield* dns.create(zone, requirement);
|
|
113
114
|
created = [providerRecordId];
|
|
114
|
-
const plan$
|
|
115
|
+
const plan$5 = yield* plan({
|
|
115
116
|
domain: attachment.domain,
|
|
116
117
|
requirements: [requirement]
|
|
117
118
|
});
|
|
118
|
-
yield* expect(name, plan$
|
|
119
|
-
const receipt = yield* apply(yield* approve(plan$
|
|
119
|
+
yield* expect(name, plan$5.operations[0]?._tag === "Noop", "an exact record must plan as Noop");
|
|
120
|
+
const receipt = yield* apply(yield* approve(plan$5));
|
|
120
121
|
yield* expect(name, applied(receipt).length === 0, "an exact plan must not create another record");
|
|
121
122
|
}).pipe(Effect.ensuring(cleanup(dns, () => created)));
|
|
122
123
|
});
|
|
@@ -126,12 +127,12 @@ const provider = (definition, credential, zone, options = {}) => {
|
|
|
126
127
|
return Effect.gen(function* () {
|
|
127
128
|
const { providerRecordId } = yield* dns.create(zone, txt(zone, `${prefix}-conflict`, "occupied"));
|
|
128
129
|
created = [providerRecordId];
|
|
129
|
-
const plan$
|
|
130
|
+
const plan$3 = yield* plan({
|
|
130
131
|
domain: attachment.domain,
|
|
131
132
|
requirements: [cname(zone, `${prefix}-conflict`)]
|
|
132
133
|
});
|
|
133
|
-
yield* expect(name, plan$
|
|
134
|
-
const rejected = yield* approve(plan$
|
|
134
|
+
yield* expect(name, plan$3.operations[0]?._tag === "Conflict", "a CNAME over an occupied name must plan as Conflict");
|
|
135
|
+
const rejected = yield* approve(plan$3).pipe(Effect.result);
|
|
135
136
|
yield* expect(name, rejected._tag === "Failure" && rejected.failure.reason._tag === "Conflict", "approve must fail closed on conflicts");
|
|
136
137
|
}).pipe(Effect.ensuring(cleanup(dns, () => created)));
|
|
137
138
|
});
|
|
@@ -140,11 +141,11 @@ const provider = (definition, credential, zone, options = {}) => {
|
|
|
140
141
|
const name = "stale-plan";
|
|
141
142
|
return Effect.gen(function* () {
|
|
142
143
|
const requirement = cname(zone, `${prefix}-stale`);
|
|
143
|
-
const plan$
|
|
144
|
+
const plan$6 = yield* plan({
|
|
144
145
|
domain: attachment.domain,
|
|
145
146
|
requirements: [requirement]
|
|
146
147
|
});
|
|
147
|
-
const approval = yield* approve(plan$
|
|
148
|
+
const approval = yield* approve(plan$6);
|
|
148
149
|
const { providerRecordId } = yield* dns.create(zone, requirement);
|
|
149
150
|
created = [providerRecordId];
|
|
150
151
|
const stale = yield* apply(approval).pipe(Effect.result);
|
|
@@ -177,11 +178,11 @@ const provider = (definition, credential, zone, options = {}) => {
|
|
|
177
178
|
let created = [];
|
|
178
179
|
const name = "partial-apply-cleanup";
|
|
179
180
|
return Effect.gen(function* () {
|
|
180
|
-
const plan$
|
|
181
|
+
const plan$2 = yield* plan({
|
|
181
182
|
domain: attachment.domain,
|
|
182
183
|
requirements: [cname(zone, `${prefix}-partial-one`), txt(zone, `${prefix}-partial-two`, "domainkit")]
|
|
183
184
|
});
|
|
184
|
-
const receipt = yield* apply(yield* approve(plan$
|
|
185
|
+
const receipt = yield* apply(yield* approve(plan$2));
|
|
185
186
|
created = recordIds(receipt);
|
|
186
187
|
yield* expect(name, receipt.status === "partial" && created.length === 1, "a later write failure must preserve the first confirmed write in a partial receipt");
|
|
187
188
|
const cleanupPlan = yield* plan$1({ receiptId: receipt.id });
|
|
@@ -191,12 +192,24 @@ const provider = (definition, credential, zone, options = {}) => {
|
|
|
191
192
|
created = [];
|
|
192
193
|
}).pipe(Effect.ensuring(cleanup(dns, () => created)));
|
|
193
194
|
});
|
|
195
|
+
/** A token the provider rejects must read as `Unauthenticated`, so a host tells the customer the provider refused it. */
|
|
196
|
+
const rejectedToken = Effect.gen(function* () {
|
|
197
|
+
const name = "rejected-token";
|
|
198
|
+
const auth = definition.auth.token;
|
|
199
|
+
if (auth === void 0) return;
|
|
200
|
+
const required = tokenFields(auth).filter((field) => field.required);
|
|
201
|
+
if (options.rejectedToken === void 0 && required.some((field) => !field.secret)) return yield* Effect.fail(failure(name, `the token method requires ${required.filter((field) => !field.secret).map((field) => field.name).join(", ")}; pass options.rejectedToken with values the provider rejects`));
|
|
202
|
+
const values = options.rejectedToken ?? Object.fromEntries(required.map((field) => [field.name, Redacted.make("")]));
|
|
203
|
+
const outcome = yield* auth.authenticate(values).pipe(Effect.result);
|
|
204
|
+
yield* expect(name, outcome._tag === "Failure" && outcome.failure.reason._tag === "Unauthenticated", "authenticating a rejected token must fail Unauthenticated");
|
|
205
|
+
});
|
|
194
206
|
return Effect.all([
|
|
195
207
|
createReadbackCleanup,
|
|
196
208
|
exactNoop,
|
|
197
209
|
conflict,
|
|
198
210
|
stalePlan,
|
|
199
|
-
partialApplyCleanup
|
|
211
|
+
partialApplyCleanup,
|
|
212
|
+
rejectedToken
|
|
200
213
|
], { discard: true });
|
|
201
214
|
};
|
|
202
215
|
/** Verification is out of scope for the provider contract; keep the pool quiet. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.mjs","names":["Principal.make","Errors.DomainKitError","Reason.InvalidInput","Storage.Service","Custody.Service","Storage.Authorization","Provider.encodeContext","Storage.Credential","Connect.attach","Storage.Attachment","DnsRecord.cname","DnsRecord.txt","Receipt.applied","Connect.session","session","Principal.Service","DomainKit.layerMemory","Resolver.Service","plan","Provision.plan","Provision.apply","Provision.approve","DnsRecord.equals","Cleanup.plan","Cleanup.apply","Cleanup.approve","Errors.fail","Reason.ProviderUnavailable"],"sources":["../../../src/internal/conformance/provider.ts"],"sourcesContent":["import { DateTime, Effect, Layer } from \"effect\";\n\nimport * as Cleanup from \"../../Cleanup.ts\";\nimport * as Connect from \"../../Connect.ts\";\nimport * as Custody from \"../../Custody.ts\";\nimport * as DnsRecord from \"../../DnsRecord.ts\";\nimport * as Errors from \"../error.ts\";\nimport * as Reason from \"../../Reason.ts\";\nimport * as DomainKit from \"../../DomainKit.ts\";\nimport * as Principal from \"../../Principal.ts\";\nimport * as Provider from \"../../Provider.ts\";\nimport * as Provision from \"../../Provision.ts\";\nimport * as Receipt from \"../../Receipt.ts\";\nimport * as Resolver from \"../../Resolver.ts\";\nimport * as Storage from \"../../Storage.ts\";\nimport { fresh } from \"../ids.ts\";\n\nexport const cases = [\n \"create-readback-cleanup\",\n \"exact-noop\",\n \"conflict\",\n \"stale-plan\",\n \"partial-apply-cleanup\",\n] as const;\nexport type Case = (typeof cases)[number];\n\nconst principal = Principal.make({ ownerId: \"conformance\", actorId: \"conformance\" });\n\nconst failure = (name: Case, message: string) =>\n new Errors.DomainKitError({\n reason: new Reason.InvalidInput({\n message: `provider conformance ${name}: ${message}`,\n field: \"conformance\",\n }),\n });\n\nconst expect = (name: Case, condition: boolean, message: string) =>\n condition ? Effect.void : Effect.fail(failure(name, message));\n\ntype Env = Storage.Service | Connect.Service | Provision.Service | Cleanup.Service;\n\n/** Attach `zone` to a connection built straight from `credential`, bypassing `authenticate`. */\nconst attachZone = (\n definition: Provider.Definition,\n credential: Provider.Credential,\n zone: string,\n) =>\n Effect.gen(function* () {\n const storage = yield* Storage.Service;\n const custody = yield* Custody.Service;\n const now = yield* DateTime.now;\n const authorization = new Storage.Authorization({\n id: yield* fresh(\"auth\"),\n ownerId: principal.ownerId,\n provider: definition.id,\n method: \"token\",\n capabilities: [\"dns:read\", \"dns:write\"],\n context: yield* Provider.encodeContext(definition, credential.context),\n revocation: \"active\",\n createdBy: principal.actorId,\n createdAt: now,\n });\n yield* storage.authorizations.upsert({\n authorization,\n credential: new Storage.Credential({\n ciphertext: yield* custody.seal(credential.secret),\n expiresAt: null,\n rotatedAt: now,\n }),\n });\n const connection = yield* storage.connections.create(authorization.id);\n const attached = yield* Connect.attach({ connectionId: connection.id, domain: zone });\n if (!(attached instanceof Storage.Attachment)) {\n return yield* Effect.fail(\n failure(\"create-readback-cleanup\", `zone ${zone} needs a selection; pass an exact zone`),\n );\n }\n return attached;\n });\n\nconst cname = (zone: string, label: string) =>\n DnsRecord.cname({ name: `${label}.${zone}`, target: \"target.example.net\", ttl: 300 });\nconst txt = (zone: string, label: string, value: string) =>\n DnsRecord.txt({ name: `${label}.${zone}`, value, ttl: 300 });\n\nconst recordIds = (receipt: Receipt.Model) =>\n Receipt.applied(receipt).map(({ providerRecordId }) => providerRecordId);\n\n/**\n * Runs create/readback/cleanup, exact-noop, conflict, stale-plan, and partial-apply against a\n * real provider definition, through the same Provision and Cleanup services hosts use.\n */\nexport const provider = (\n definition: Provider.Definition,\n credential: Provider.Credential,\n zone: string,\n options: { readonly prefix?: string } = {},\n): Effect.Effect<void, Errors.DomainKitError> => {\n const prefix = options.prefix ?? \"domainkit-conformance\";\n const run = <A>(\n target: Provider.Definition,\n body: (\n dns: Provider.Dns,\n attachment: Storage.Attachment,\n ) => Effect.Effect<A, Errors.DomainKitError, Env | Principal.Service>,\n ) =>\n Effect.gen(function* () {\n const attachment = yield* attachZone(target, credential, zone);\n const { session, target: resolved } = yield* Connect.session(attachment.id);\n return yield* body(session.dns(resolved), attachment);\n }).pipe(\n Effect.provideService(Principal.Service, principal),\n Effect.provide(\n DomainKit.layerMemory({\n providers: [target],\n resolver: Layer.succeed(Resolver.Service)(silentResolver),\n }),\n ),\n );\n\n const cleanup = (dns: Provider.Dns, ids: () => ReadonlyArray<string>) =>\n Effect.suspend(() =>\n Effect.forEach(ids(), (id) => dns.delete(zone, id).pipe(Effect.ignore), { discard: true }),\n );\n\n const createReadbackCleanup = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"create-readback-cleanup\";\n return Effect.gen(function* () {\n const requirements = [\n cname(zone, `${prefix}-create`),\n txt(zone, `${prefix}-page`, \"domainkit\"),\n ];\n const plan = yield* Provision.plan({ domain: attachment.domain, requirements });\n yield* expect(\n name,\n plan.operations.every(({ _tag }) => _tag === \"Create\"),\n \"expected two Create operations\",\n );\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n created = recordIds(receipt);\n yield* expect(\n name,\n receipt.status === \"complete\" && created.length === 2,\n \"apply must return a complete receipt with provider record ids\",\n );\n const observed = yield* dns.list(zone);\n yield* expect(\n name,\n requirements.every((requirement) =>\n observed.some(({ record }) => DnsRecord.equals(record, requirement)),\n ),\n \"list must return every created record, including across provider pages\",\n );\n const cleanupPlan = yield* Cleanup.plan({ receiptId: receipt.id });\n yield* expect(\n name,\n cleanupPlan.operations.every(({ _tag }) => _tag === \"Delete\"),\n \"cleanup plan must delete both records\",\n );\n const cleaned = yield* Cleanup.apply(yield* Cleanup.approve(cleanupPlan));\n const remaining = yield* Effect.forEach(created, (id) => dns.get(zone, id));\n yield* expect(\n name,\n cleaned.status === \"complete\" && remaining.every((record) => record === null),\n \"receipt-bound cleanup must remove every created record\",\n );\n created = [];\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const exactNoop = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"exact-noop\";\n return Effect.gen(function* () {\n const requirement = cname(zone, `${prefix}-exact`);\n const { providerRecordId } = yield* dns.create(zone, requirement);\n created = [providerRecordId];\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [requirement],\n });\n yield* expect(name, plan.operations[0]?._tag === \"Noop\", \"an exact record must plan as Noop\");\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n yield* expect(\n name,\n Receipt.applied(receipt).length === 0,\n \"an exact plan must not create another record\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const conflict = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"conflict\";\n return Effect.gen(function* () {\n const { providerRecordId } = yield* dns.create(\n zone,\n txt(zone, `${prefix}-conflict`, \"occupied\"),\n );\n created = [providerRecordId];\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [cname(zone, `${prefix}-conflict`)],\n });\n yield* expect(\n name,\n plan.operations[0]?._tag === \"Conflict\",\n \"a CNAME over an occupied name must plan as Conflict\",\n );\n const rejected = yield* Provision.approve(plan).pipe(Effect.result);\n yield* expect(\n name,\n rejected._tag === \"Failure\" && rejected.failure.reason._tag === \"Conflict\",\n \"approve must fail closed on conflicts\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const stalePlan = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"stale-plan\";\n return Effect.gen(function* () {\n const requirement = cname(zone, `${prefix}-stale`);\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [requirement],\n });\n const approval = yield* Provision.approve(plan);\n const { providerRecordId } = yield* dns.create(zone, requirement);\n created = [providerRecordId];\n const stale = yield* Provision.apply(approval).pipe(Effect.result);\n yield* expect(\n name,\n stale._tag === \"Failure\" && stale.failure.reason._tag === \"Stale\",\n \"apply must reject provider state that changed after approval\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const flaky: Provider.Definition = {\n ...definition,\n session: (input) => {\n const session = definition.session(input);\n return {\n ...session,\n dns: (target) => {\n const dns = session.dns(target);\n let writes = 0;\n return {\n ...dns,\n create: (targetZone, record) => {\n writes += 1;\n return writes === 2\n ? Errors.fail(\n new Reason.ProviderUnavailable({\n provider: definition.id,\n message: \"injected conformance failure\",\n }),\n )\n : dns.create(targetZone, record);\n },\n };\n },\n };\n },\n };\n\n const partialApplyCleanup = run(flaky, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"partial-apply-cleanup\";\n return Effect.gen(function* () {\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [\n cname(zone, `${prefix}-partial-one`),\n txt(zone, `${prefix}-partial-two`, \"domainkit\"),\n ],\n });\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n created = recordIds(receipt);\n yield* expect(\n name,\n receipt.status === \"partial\" && created.length === 1,\n \"a later write failure must preserve the first confirmed write in a partial receipt\",\n );\n const cleanupPlan = yield* Cleanup.plan({ receiptId: receipt.id });\n yield* Cleanup.apply(yield* Cleanup.approve(cleanupPlan));\n const remaining = yield* Effect.forEach(created, (id) => dns.get(zone, id));\n yield* expect(\n name,\n remaining.every((record) => record === null),\n \"partial receipt cleanup must remove the confirmed write\",\n );\n created = [];\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n return Effect.all([createReadbackCleanup, exactNoop, conflict, stalePlan, partialApplyCleanup], {\n discard: true,\n });\n};\n\n/** Verification is out of scope for the provider contract; keep the pool quiet. */\nconst silentResolver: Resolver.Interface = {\n resolve: () => Effect.succeed([]),\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"provider.mjs","names":["Principal.make","Errors.DomainKitError","Reason.InvalidInput","Storage.Service","Custody.Service","Storage.Authorization","Provider.encodeContext","Storage.Credential","Connect.attach","Storage.Attachment","DnsRecord.cname","DnsRecord.txt","Receipt.applied","Connect.session","session","Principal.Service","DomainKit.layerMemory","Resolver.Service","plan","Provision.plan","Provision.apply","Provision.approve","DnsRecord.equals","Cleanup.plan","Cleanup.apply","Cleanup.approve","Errors.fail","Reason.ProviderUnavailable","Provider.tokenFields"],"sources":["../../../src/internal/conformance/provider.ts"],"sourcesContent":["import { DateTime, Effect, Layer, Redacted } from \"effect\";\n\nimport * as Cleanup from \"../../Cleanup.ts\";\nimport * as Connect from \"../../Connect.ts\";\nimport * as Custody from \"../../Custody.ts\";\nimport * as DnsRecord from \"../../DnsRecord.ts\";\nimport * as Errors from \"../error.ts\";\nimport * as Reason from \"../../Reason.ts\";\nimport * as DomainKit from \"../../DomainKit.ts\";\nimport * as Principal from \"../../Principal.ts\";\nimport * as Provider from \"../../Provider.ts\";\nimport * as Provision from \"../../Provision.ts\";\nimport * as Receipt from \"../../Receipt.ts\";\nimport * as Resolver from \"../../Resolver.ts\";\nimport * as Storage from \"../../Storage.ts\";\nimport { fresh } from \"../ids.ts\";\n\nexport const cases = [\n \"create-readback-cleanup\",\n \"exact-noop\",\n \"conflict\",\n \"stale-plan\",\n \"partial-apply-cleanup\",\n \"rejected-token\",\n] as const;\nexport type Case = (typeof cases)[number];\n\nconst principal = Principal.make({ ownerId: \"conformance\", actorId: \"conformance\" });\n\nconst failure = (name: Case, message: string) =>\n new Errors.DomainKitError({\n reason: new Reason.InvalidInput({\n message: `provider conformance ${name}: ${message}`,\n field: \"conformance\",\n }),\n });\n\nconst expect = (name: Case, condition: boolean, message: string) =>\n condition ? Effect.void : Effect.fail(failure(name, message));\n\ntype Env = Storage.Service | Connect.Service | Provision.Service | Cleanup.Service;\n\n/** Attach `zone` to a connection built straight from `credential`, bypassing `authenticate`. */\nconst attachZone = (\n definition: Provider.Definition,\n credential: Provider.Credential,\n zone: string,\n) =>\n Effect.gen(function* () {\n const storage = yield* Storage.Service;\n const custody = yield* Custody.Service;\n const now = yield* DateTime.now;\n const authorization = new Storage.Authorization({\n id: yield* fresh(\"auth\"),\n ownerId: principal.ownerId,\n provider: definition.id,\n method: \"token\",\n capabilities: [\"dns:read\", \"dns:write\"],\n context: yield* Provider.encodeContext(definition, credential.context),\n revocation: \"active\",\n createdBy: principal.actorId,\n createdAt: now,\n });\n yield* storage.authorizations.upsert({\n authorization,\n credential: new Storage.Credential({\n ciphertext: yield* custody.seal(credential.secret),\n expiresAt: null,\n rotatedAt: now,\n }),\n });\n const connection = yield* storage.connections.create(authorization.id);\n const attached = yield* Connect.attach({ connectionId: connection.id, domain: zone });\n if (!(attached instanceof Storage.Attachment)) {\n return yield* Effect.fail(\n failure(\"create-readback-cleanup\", `zone ${zone} needs a selection; pass an exact zone`),\n );\n }\n return attached;\n });\n\nconst cname = (zone: string, label: string) =>\n DnsRecord.cname({ name: `${label}.${zone}`, target: \"target.example.net\", ttl: 300 });\nconst txt = (zone: string, label: string, value: string) =>\n DnsRecord.txt({ name: `${label}.${zone}`, value, ttl: 300 });\n\nconst recordIds = (receipt: Receipt.Model) =>\n Receipt.applied(receipt).map(({ providerRecordId }) => providerRecordId);\n\n/**\n * Runs create/readback/cleanup, exact-noop, conflict, stale-plan, partial-apply, and\n * rejected-token against a real provider definition, through the same Provision and Cleanup\n * services hosts use.\n */\nexport const provider = (\n definition: Provider.Definition,\n credential: Provider.Credential,\n zone: string,\n options: {\n readonly prefix?: string;\n /**\n * Token values the provider rejects, for the `rejected-token` case. Default: every required\n * field set to an empty secret, which needs a token method whose required fields are all\n * secrets.\n */\n readonly rejectedToken?: Provider.TokenValues;\n } = {},\n): Effect.Effect<void, Errors.DomainKitError> => {\n const prefix = options.prefix ?? \"domainkit-conformance\";\n const run = <A>(\n target: Provider.Definition,\n body: (\n dns: Provider.Dns,\n attachment: Storage.Attachment,\n ) => Effect.Effect<A, Errors.DomainKitError, Env | Principal.Service>,\n ) =>\n Effect.gen(function* () {\n const attachment = yield* attachZone(target, credential, zone);\n const { session, target: resolved } = yield* Connect.session(attachment.id);\n return yield* body(session.dns(resolved), attachment);\n }).pipe(\n Effect.provideService(Principal.Service, principal),\n Effect.provide(\n DomainKit.layerMemory({\n providers: [target],\n resolver: Layer.succeed(Resolver.Service)(silentResolver),\n }),\n ),\n );\n\n const cleanup = (dns: Provider.Dns, ids: () => ReadonlyArray<string>) =>\n Effect.suspend(() =>\n Effect.forEach(ids(), (id) => dns.delete(zone, id).pipe(Effect.ignore), { discard: true }),\n );\n\n const createReadbackCleanup = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"create-readback-cleanup\";\n return Effect.gen(function* () {\n const requirements = [\n cname(zone, `${prefix}-create`),\n txt(zone, `${prefix}-page`, \"domainkit\"),\n ];\n const plan = yield* Provision.plan({ domain: attachment.domain, requirements });\n yield* expect(\n name,\n plan.operations.every(({ _tag }) => _tag === \"Create\"),\n \"expected two Create operations\",\n );\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n created = recordIds(receipt);\n yield* expect(\n name,\n receipt.status === \"complete\" && created.length === 2,\n \"apply must return a complete receipt with provider record ids\",\n );\n const observed = yield* dns.list(zone);\n yield* expect(\n name,\n requirements.every((requirement) =>\n observed.some(({ record }) => DnsRecord.equals(record, requirement)),\n ),\n \"list must return every created record, including across provider pages\",\n );\n const cleanupPlan = yield* Cleanup.plan({ receiptId: receipt.id });\n yield* expect(\n name,\n cleanupPlan.operations.every(({ _tag }) => _tag === \"Delete\"),\n \"cleanup plan must delete both records\",\n );\n const cleaned = yield* Cleanup.apply(yield* Cleanup.approve(cleanupPlan));\n const remaining = yield* Effect.forEach(created, (id) => dns.get(zone, id));\n yield* expect(\n name,\n cleaned.status === \"complete\" && remaining.every((record) => record === null),\n \"receipt-bound cleanup must remove every created record\",\n );\n created = [];\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const exactNoop = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"exact-noop\";\n return Effect.gen(function* () {\n const requirement = cname(zone, `${prefix}-exact`);\n const { providerRecordId } = yield* dns.create(zone, requirement);\n created = [providerRecordId];\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [requirement],\n });\n yield* expect(name, plan.operations[0]?._tag === \"Noop\", \"an exact record must plan as Noop\");\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n yield* expect(\n name,\n Receipt.applied(receipt).length === 0,\n \"an exact plan must not create another record\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const conflict = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"conflict\";\n return Effect.gen(function* () {\n const { providerRecordId } = yield* dns.create(\n zone,\n txt(zone, `${prefix}-conflict`, \"occupied\"),\n );\n created = [providerRecordId];\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [cname(zone, `${prefix}-conflict`)],\n });\n yield* expect(\n name,\n plan.operations[0]?._tag === \"Conflict\",\n \"a CNAME over an occupied name must plan as Conflict\",\n );\n const rejected = yield* Provision.approve(plan).pipe(Effect.result);\n yield* expect(\n name,\n rejected._tag === \"Failure\" && rejected.failure.reason._tag === \"Conflict\",\n \"approve must fail closed on conflicts\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const stalePlan = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"stale-plan\";\n return Effect.gen(function* () {\n const requirement = cname(zone, `${prefix}-stale`);\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [requirement],\n });\n const approval = yield* Provision.approve(plan);\n const { providerRecordId } = yield* dns.create(zone, requirement);\n created = [providerRecordId];\n const stale = yield* Provision.apply(approval).pipe(Effect.result);\n yield* expect(\n name,\n stale._tag === \"Failure\" && stale.failure.reason._tag === \"Stale\",\n \"apply must reject provider state that changed after approval\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const flaky: Provider.Definition = {\n ...definition,\n session: (input) => {\n const session = definition.session(input);\n return {\n ...session,\n dns: (target) => {\n const dns = session.dns(target);\n let writes = 0;\n return {\n ...dns,\n create: (targetZone, record) => {\n writes += 1;\n return writes === 2\n ? Errors.fail(\n new Reason.ProviderUnavailable({\n provider: definition.id,\n message: \"injected conformance failure\",\n }),\n )\n : dns.create(targetZone, record);\n },\n };\n },\n };\n },\n };\n\n const partialApplyCleanup = run(flaky, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"partial-apply-cleanup\";\n return Effect.gen(function* () {\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [\n cname(zone, `${prefix}-partial-one`),\n txt(zone, `${prefix}-partial-two`, \"domainkit\"),\n ],\n });\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n created = recordIds(receipt);\n yield* expect(\n name,\n receipt.status === \"partial\" && created.length === 1,\n \"a later write failure must preserve the first confirmed write in a partial receipt\",\n );\n const cleanupPlan = yield* Cleanup.plan({ receiptId: receipt.id });\n yield* Cleanup.apply(yield* Cleanup.approve(cleanupPlan));\n const remaining = yield* Effect.forEach(created, (id) => dns.get(zone, id));\n yield* expect(\n name,\n remaining.every((record) => record === null),\n \"partial receipt cleanup must remove the confirmed write\",\n );\n created = [];\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n /** A token the provider rejects must read as `Unauthenticated`, so a host tells the customer the provider refused it. */\n const rejectedToken = Effect.gen(function* () {\n const name: Case = \"rejected-token\";\n const auth = definition.auth.token;\n if (auth === undefined) return;\n const required = Provider.tokenFields(auth).filter((field) => field.required);\n if (options.rejectedToken === undefined && required.some((field) => !field.secret)) {\n return yield* Effect.fail(\n failure(\n name,\n `the token method requires ${required\n .filter((field) => !field.secret)\n .map((field) => field.name)\n .join(\", \")}; pass options.rejectedToken with values the provider rejects`,\n ),\n );\n }\n const values =\n options.rejectedToken ??\n Object.fromEntries(required.map((field) => [field.name, Redacted.make(\"\")]));\n const outcome = yield* auth.authenticate(values).pipe(Effect.result);\n yield* expect(\n name,\n outcome._tag === \"Failure\" && outcome.failure.reason._tag === \"Unauthenticated\",\n \"authenticating a rejected token must fail Unauthenticated\",\n );\n });\n\n return Effect.all(\n [createReadbackCleanup, exactNoop, conflict, stalePlan, partialApplyCleanup, rejectedToken],\n { discard: true },\n );\n};\n\n/** Verification is out of scope for the provider contract; keep the pool quiet. */\nconst silentResolver: Resolver.Interface = {\n resolve: () => Effect.succeed([]),\n};\n"],"mappings":";;;;;;;;;;;;;;;;AA2BA,MAAM,YAAYA,KAAe;CAAE,SAAS;CAAe,SAAS;AAAc,CAAC;AAEnF,MAAM,WAAW,MAAY,YAC3B,IAAIC,eAAsB,EACxB,QAAQ,IAAIC,aAAoB;CAC9B,SAAS,wBAAwB,KAAK,IAAI;CAC1C,OAAO;AACT,CAAC,EACH,CAAC;AAEH,MAAM,UAAU,MAAY,WAAoB,YAC9C,YAAY,OAAO,OAAO,OAAO,KAAK,QAAQ,MAAM,OAAO,CAAC;;AAK9D,MAAM,cACJ,YACA,YACA,SAEA,OAAO,IAAI,aAAa;CACtB,MAAM,UAAU,OAAOC;CACvB,MAAM,UAAU,OAAOC;CACvB,MAAM,MAAM,OAAO,SAAS;CAC5B,MAAM,gBAAgB,IAAIC,cAAsB;EAC9C,IAAI,OAAO,MAAM,MAAM;EACvB,SAAS,UAAU;EACnB,UAAU,WAAW;EACrB,QAAQ;EACR,cAAc,CAAC,YAAY,WAAW;EACtC,SAAS,OAAOC,cAAuB,YAAY,WAAW,OAAO;EACrE,YAAY;EACZ,WAAW,UAAU;EACrB,WAAW;CACb,CAAC;CACD,OAAO,QAAQ,eAAe,OAAO;EACnC;EACA,YAAY,IAAIC,WAAmB;GACjC,YAAY,OAAO,QAAQ,KAAK,WAAW,MAAM;GACjD,WAAW;GACX,WAAW;EACb,CAAC;CACH,CAAC;CACD,MAAM,aAAa,OAAO,QAAQ,YAAY,OAAO,cAAc,EAAE;CACrE,MAAM,WAAW,OAAOC,OAAe;EAAE,cAAc,WAAW;EAAI,QAAQ;CAAK,CAAC;CACpF,IAAI,EAAE,oBAAoBC,aACxB,OAAO,OAAO,OAAO,KACnB,QAAQ,2BAA2B,QAAQ,KAAK,uCAAuC,CACzF;CAEF,OAAO;AACT,CAAC;AAEH,MAAM,SAAS,MAAc,UAC3BC,QAAgB;CAAE,MAAM,GAAG,MAAM,GAAG;CAAQ,QAAQ;CAAsB,KAAK;AAAI,CAAC;AACtF,MAAM,OAAO,MAAc,OAAe,UACxCC,MAAc;CAAE,MAAM,GAAG,MAAM,GAAG;CAAQ;CAAO,KAAK;AAAI,CAAC;AAE7D,MAAM,aAAa,YACjBC,QAAgB,OAAO,CAAC,CAAC,KAAK,EAAE,uBAAuB,gBAAgB;;;;;;AAOzE,MAAa,YACX,YACA,YACA,MACA,UAQI,CAAC,MAC0C;CAC/C,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,OACJ,QACA,SAKA,OAAO,IAAI,aAAa;EACtB,MAAM,aAAa,OAAO,WAAW,QAAQ,YAAY,IAAI;EAC7D,MAAM,EAAE,SAAA,WAAS,QAAQ,aAAa,OAAOC,QAAgB,WAAW,EAAE;EAC1E,OAAO,OAAO,KAAKC,UAAQ,IAAI,QAAQ,GAAG,UAAU;CACtD,CAAC,CAAC,CAAC,KACD,OAAO,eAAeC,WAAmB,SAAS,GAClD,OAAO,QACLC,YAAsB;EACpB,WAAW,CAAC,MAAM;EAClB,UAAU,MAAM,QAAQC,SAAgB,CAAC,CAAC,cAAc;CAC1D,CAAC,CACH,CACF;CAEF,MAAM,WAAW,KAAmB,QAClC,OAAO,cACL,OAAO,QAAQ,IAAI,IAAI,OAAO,IAAI,OAAO,MAAM,EAAE,CAAC,CAAC,KAAK,OAAO,MAAM,GAAG,EAAE,SAAS,KAAK,CAAC,CAC3F;CAEF,MAAM,wBAAwB,IAAI,aAAa,KAAK,eAAe;EACjE,IAAI,UAAiC,CAAC;EACtC,MAAM,OAAa;EACnB,OAAO,OAAO,IAAI,aAAa;GAC7B,MAAM,eAAe,CACnB,MAAM,MAAM,GAAG,OAAO,QAAQ,GAC9B,IAAI,MAAM,GAAG,OAAO,QAAQ,WAAW,CACzC;GACA,MAAMC,SAAO,OAAOC,KAAe;IAAE,QAAQ,WAAW;IAAQ;GAAa,CAAC;GAC9E,OAAO,OACL,MACAD,OAAK,WAAW,OAAO,EAAE,WAAW,SAAS,QAAQ,GACrD,gCACF;GACA,MAAM,UAAU,OAAOE,MAAgB,OAAOC,QAAkBH,MAAI,CAAC;GACrE,UAAU,UAAU,OAAO;GAC3B,OAAO,OACL,MACA,QAAQ,WAAW,cAAc,QAAQ,WAAW,GACpD,+DACF;GACA,MAAM,WAAW,OAAO,IAAI,KAAK,IAAI;GACrC,OAAO,OACL,MACA,aAAa,OAAO,gBAClB,SAAS,MAAM,EAAE,aAAaI,OAAiB,QAAQ,WAAW,CAAC,CACrE,GACA,wEACF;GACA,MAAM,cAAc,OAAOC,OAAa,EAAE,WAAW,QAAQ,GAAG,CAAC;GACjE,OAAO,OACL,MACA,YAAY,WAAW,OAAO,EAAE,WAAW,SAAS,QAAQ,GAC5D,uCACF;GACA,MAAM,UAAU,OAAOC,QAAc,OAAOC,UAAgB,WAAW,CAAC;GACxE,MAAM,YAAY,OAAO,OAAO,QAAQ,UAAU,OAAO,IAAI,IAAI,MAAM,EAAE,CAAC;GAC1E,OAAO,OACL,MACA,QAAQ,WAAW,cAAc,UAAU,OAAO,WAAW,WAAW,IAAI,GAC5E,wDACF;GACA,UAAU,CAAC;EACb,CAAC,CAAC,CAAC,KAAK,OAAO,SAAS,QAAQ,WAAW,OAAO,CAAC,CAAC;CACtD,CAAC;CAED,MAAM,YAAY,IAAI,aAAa,KAAK,eAAe;EACrD,IAAI,UAAiC,CAAC;EACtC,MAAM,OAAa;EACnB,OAAO,OAAO,IAAI,aAAa;GAC7B,MAAM,cAAc,MAAM,MAAM,GAAG,OAAO,OAAO;GACjD,MAAM,EAAE,qBAAqB,OAAO,IAAI,OAAO,MAAM,WAAW;GAChE,UAAU,CAAC,gBAAgB;GAC3B,MAAMP,SAAO,OAAOC,KAAe;IACjC,QAAQ,WAAW;IACnB,cAAc,CAAC,WAAW;GAC5B,CAAC;GACD,OAAO,OAAO,MAAMD,OAAK,WAAW,EAAE,EAAE,SAAS,QAAQ,mCAAmC;GAC5F,MAAM,UAAU,OAAOE,MAAgB,OAAOC,QAAkBH,MAAI,CAAC;GACrE,OAAO,OACL,MACAN,QAAgB,OAAO,CAAC,CAAC,WAAW,GACpC,8CACF;EACF,CAAC,CAAC,CAAC,KAAK,OAAO,SAAS,QAAQ,WAAW,OAAO,CAAC,CAAC;CACtD,CAAC;CAED,MAAM,WAAW,IAAI,aAAa,KAAK,eAAe;EACpD,IAAI,UAAiC,CAAC;EACtC,MAAM,OAAa;EACnB,OAAO,OAAO,IAAI,aAAa;GAC7B,MAAM,EAAE,qBAAqB,OAAO,IAAI,OACtC,MACA,IAAI,MAAM,GAAG,OAAO,YAAY,UAAU,CAC5C;GACA,UAAU,CAAC,gBAAgB;GAC3B,MAAMM,SAAO,OAAOC,KAAe;IACjC,QAAQ,WAAW;IACnB,cAAc,CAAC,MAAM,MAAM,GAAG,OAAO,UAAU,CAAC;GAClD,CAAC;GACD,OAAO,OACL,MACAD,OAAK,WAAW,EAAE,EAAE,SAAS,YAC7B,qDACF;GACA,MAAM,WAAW,OAAOG,QAAkBH,MAAI,CAAC,CAAC,KAAK,OAAO,MAAM;GAClE,OAAO,OACL,MACA,SAAS,SAAS,aAAa,SAAS,QAAQ,OAAO,SAAS,YAChE,uCACF;EACF,CAAC,CAAC,CAAC,KAAK,OAAO,SAAS,QAAQ,WAAW,OAAO,CAAC,CAAC;CACtD,CAAC;CAED,MAAM,YAAY,IAAI,aAAa,KAAK,eAAe;EACrD,IAAI,UAAiC,CAAC;EACtC,MAAM,OAAa;EACnB,OAAO,OAAO,IAAI,aAAa;GAC7B,MAAM,cAAc,MAAM,MAAM,GAAG,OAAO,OAAO;GACjD,MAAMA,SAAO,OAAOC,KAAe;IACjC,QAAQ,WAAW;IACnB,cAAc,CAAC,WAAW;GAC5B,CAAC;GACD,MAAM,WAAW,OAAOE,QAAkBH,MAAI;GAC9C,MAAM,EAAE,qBAAqB,OAAO,IAAI,OAAO,MAAM,WAAW;GAChE,UAAU,CAAC,gBAAgB;GAC3B,MAAM,QAAQ,OAAOE,MAAgB,QAAQ,CAAC,CAAC,KAAK,OAAO,MAAM;GACjE,OAAO,OACL,MACA,MAAM,SAAS,aAAa,MAAM,QAAQ,OAAO,SAAS,SAC1D,8DACF;EACF,CAAC,CAAC,CAAC,KAAK,OAAO,SAAS,QAAQ,WAAW,OAAO,CAAC,CAAC;CACtD,CAAC;CA8BD,MAAM,sBAAsB,IAAI;EA3B9B,GAAG;EACH,UAAU,UAAU;GAClB,MAAM,UAAU,WAAW,QAAQ,KAAK;GACxC,OAAO;IACL,GAAG;IACH,MAAM,WAAW;KACf,MAAM,MAAM,QAAQ,IAAI,MAAM;KAC9B,IAAI,SAAS;KACb,OAAO;MACL,GAAG;MACH,SAAS,YAAY,WAAW;OAC9B,UAAU;OACV,OAAO,WAAW,IACdM,KACE,IAAIC,oBAA2B;QAC7B,UAAU,WAAW;QACrB,SAAS;OACX,CAAC,CACH,IACA,IAAI,OAAO,YAAY,MAAM;MACnC;KACF;IACF;GACF;EACF;CAGkC,IAAI,KAAK,eAAe;EAC1D,IAAI,UAAiC,CAAC;EACtC,MAAM,OAAa;EACnB,OAAO,OAAO,IAAI,aAAa;GAC7B,MAAMT,SAAO,OAAOC,KAAe;IACjC,QAAQ,WAAW;IACnB,cAAc,CACZ,MAAM,MAAM,GAAG,OAAO,aAAa,GACnC,IAAI,MAAM,GAAG,OAAO,eAAe,WAAW,CAChD;GACF,CAAC;GACD,MAAM,UAAU,OAAOC,MAAgB,OAAOC,QAAkBH,MAAI,CAAC;GACrE,UAAU,UAAU,OAAO;GAC3B,OAAO,OACL,MACA,QAAQ,WAAW,aAAa,QAAQ,WAAW,GACnD,oFACF;GACA,MAAM,cAAc,OAAOK,OAAa,EAAE,WAAW,QAAQ,GAAG,CAAC;GACjE,OAAOC,QAAc,OAAOC,UAAgB,WAAW,CAAC;GACxD,MAAM,YAAY,OAAO,OAAO,QAAQ,UAAU,OAAO,IAAI,IAAI,MAAM,EAAE,CAAC;GAC1E,OAAO,OACL,MACA,UAAU,OAAO,WAAW,WAAW,IAAI,GAC3C,yDACF;GACA,UAAU,CAAC;EACb,CAAC,CAAC,CAAC,KAAK,OAAO,SAAS,QAAQ,WAAW,OAAO,CAAC,CAAC;CACtD,CAAC;;CAGD,MAAM,gBAAgB,OAAO,IAAI,aAAa;EAC5C,MAAM,OAAa;EACnB,MAAM,OAAO,WAAW,KAAK;EAC7B,IAAI,SAAS,KAAA,GAAW;EACxB,MAAM,WAAWG,YAAqB,IAAI,CAAC,CAAC,QAAQ,UAAU,MAAM,QAAQ;EAC5E,IAAI,QAAQ,kBAAkB,KAAA,KAAa,SAAS,MAAM,UAAU,CAAC,MAAM,MAAM,GAC/E,OAAO,OAAO,OAAO,KACnB,QACE,MACA,6BAA6B,SAC1B,QAAQ,UAAU,CAAC,MAAM,MAAM,CAAC,CAChC,KAAK,UAAU,MAAM,IAAI,CAAC,CAC1B,KAAK,IAAI,EAAE,8DAChB,CACF;EAEF,MAAM,SACJ,QAAQ,iBACR,OAAO,YAAY,SAAS,KAAK,UAAU,CAAC,MAAM,MAAM,SAAS,KAAK,EAAE,CAAC,CAAC,CAAC;EAC7E,MAAM,UAAU,OAAO,KAAK,aAAa,MAAM,CAAC,CAAC,KAAK,OAAO,MAAM;EACnE,OAAO,OACL,MACA,QAAQ,SAAS,aAAa,QAAQ,QAAQ,OAAO,SAAS,mBAC9D,2DACF;CACF,CAAC;CAED,OAAO,OAAO,IACZ;EAAC;EAAuB;EAAW;EAAU;EAAW;EAAqB;CAAa,GAC1F,EAAE,SAAS,KAAK,CAClB;AACF;;AAGA,MAAM,iBAAqC,EACzC,eAAe,OAAO,QAAQ,CAAC,CAAC,EAClC"}
|
package/dist/internal/doh.mjs
CHANGED
|
@@ -26,7 +26,8 @@ const query = (input) => Effect.tryPromise({
|
|
|
26
26
|
}],
|
|
27
27
|
type: "query"
|
|
28
28
|
}));
|
|
29
|
-
const
|
|
29
|
+
const { fetch: send } = input;
|
|
30
|
+
const response = await send(input.url, {
|
|
30
31
|
body,
|
|
31
32
|
headers: {
|
|
32
33
|
accept: "application/dns-message",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doh.mjs","names":["Errors.DomainKitError","Reason.ResolverFailed","Errors.isDomainKitError","DnsRecord.Opaque","DnsRecord.a","DnsRecord.aaaa","DnsRecord.cname","DnsRecord.ns","DnsRecord.txt","DnsRecord.mx","DnsRecord.caa","DnsRecord.srv"],"sources":["../../src/internal/doh.ts"],"sourcesContent":["import * as DnsPacket from \"@leichtgewicht/dns-packet\";\nimport { Effect } from \"effect\";\n\nimport * as DnsRecord from \"../DnsRecord.ts\";\nimport * as Errors from \"./error.ts\";\nimport * as Reason from \"../Reason.ts\";\nimport type { Fetch } from \"./http.ts\";\n\nexport interface Answer {\n readonly records: ReadonlyArray<DnsRecord.Observed>;\n readonly negative: boolean;\n readonly ttl: number | null;\n}\n\nconst failed = (resolver: string, message: string) =>\n new Errors.DomainKitError({\n reason: new Reason.ResolverFailed({ resolver, message }),\n });\n\n/** One RFC 8484 wire-format query; the caller applies the timeout. */\nexport const query = (input: {\n readonly resolver: string;\n readonly url: string;\n readonly fetch: Fetch;\n readonly name: string;\n readonly type: DnsRecord.Type;\n /** Host-supplied abort, combined with the fiber's own interruption signal. */\n readonly signal?: AbortSignal | undefined;\n}): Effect.Effect<Answer, Errors.DomainKitError> =>\n Effect.tryPromise({\n try: async (fiberSignal) => {\n const controller = new AbortController();\n const abort = (reason: unknown) => controller.abort(reason);\n fiberSignal.addEventListener(\"abort\", () => abort(fiberSignal.reason), { once: true });\n input.signal?.addEventListener(\"abort\", () => abort(input.signal?.reason), { once: true });\n if (input.signal?.aborted === true) abort(input.signal.reason);\n const signal = controller.signal;\n const body = Uint8Array.from(\n DnsPacket.encode({\n flags: DnsPacket.RECURSION_DESIRED,\n id: 0,\n questions: [{ name: input.name, type: input.type }],\n type: \"query\",\n }),\n );\n const response = await input.fetch(input.url, {\n body,\n headers: { accept: \"application/dns-message\", \"content-type\": \"application/dns-message\" },\n method: \"POST\",\n signal,\n });\n if (!response.ok) throw failed(input.resolver, `DoH returned HTTP ${response.status}`);\n const contentType = response.headers\n .get(\"content-type\")\n ?.split(\";\", 1)[0]\n ?.trim()\n .toLowerCase();\n if (contentType !== \"application/dns-message\") {\n throw failed(\n input.resolver,\n `DoH returned unsupported content type ${contentType ?? \"<missing>\"}`,\n );\n }\n return decode(\n input.resolver,\n input.name,\n input.type,\n new Uint8Array(await response.arrayBuffer()),\n );\n },\n catch: (cause) =>\n Errors.isDomainKitError(cause)\n ? cause\n : failed(input.resolver, cause instanceof Error ? cause.message : \"DNS resolution failed\"),\n });\n\nfunction decode(resolver: string, name: string, type: DnsRecord.Type, message: Uint8Array): Answer {\n let packet: DnsPacket.Packet;\n try {\n packet = DnsPacket.decode(message);\n } catch {\n throw failed(resolver, \"DoH returned an invalid DNS message\");\n }\n if (packet.type !== \"response\") throw failed(resolver, \"DoH returned a DNS query\");\n if (packet.id !== 0) throw failed(resolver, \"DoH response used an unexpected message ID\");\n if (packet.flag_tc) throw failed(resolver, \"DoH returned a truncated DNS response\");\n const question = packet.questions?.[0];\n if (\n packet.questions?.length !== 1 ||\n question === undefined ||\n question.class !== \"IN\" ||\n question.type !== type ||\n question.name.toLowerCase().replace(/\\.$/, \"\") !== name\n ) {\n throw failed(resolver, \"DoH response did not match the DNS question\");\n }\n if (packet.rcode === \"NXDOMAIN\") return { records: [], negative: true, ttl: null };\n if (packet.rcode !== \"NOERROR\") {\n throw failed(resolver, `DNS resolver returned ${packet.rcode ?? \"an unknown response code\"}`);\n }\n const answers = (packet.answers ?? []).filter((answer) => answer.class === \"IN\");\n if (answers.some((answer) => answer.ttl === undefined)) {\n throw failed(resolver, \"DoH response omitted the DNS record TTL\");\n }\n const records = answers.map(toRecord);\n const ttls = answers.flatMap((answer) => (answer.ttl === undefined ? [] : [answer.ttl]));\n return {\n records,\n negative: records.length === 0,\n ttl: ttls.length === 0 ? null : Math.min(...ttls),\n };\n}\n\nfunction toRecord(answer: DnsPacket.Answer): DnsRecord.Observed {\n const name = answer.name;\n const opaque = () => new DnsRecord.Opaque({ name, type: answer.type, raw: answer });\n const ttl = answer.ttl === undefined || answer.ttl <= 0 ? {} : { ttl: answer.ttl };\n try {\n switch (answer.type) {\n case \"A\":\n return DnsRecord.a({ name, address: answer.data, ...ttl });\n case \"AAAA\":\n return DnsRecord.aaaa({ name, address: answer.data, ...ttl });\n case \"CNAME\":\n return DnsRecord.cname({ name, target: answer.data, ...ttl });\n case \"NS\":\n return DnsRecord.ns({ name, nameserver: answer.data, ...ttl });\n case \"TXT\":\n return DnsRecord.txt({ name, value: decodeTxt(answer.data), ...ttl });\n case \"MX\":\n return DnsRecord.mx({\n name,\n exchange: answer.data.exchange,\n priority: answer.data.preference ?? 0,\n ...ttl,\n });\n case \"CAA\":\n return DnsRecord.caa({\n name,\n flags: answer.data.flags ?? 0,\n tag: answer.data.tag,\n value: answer.data.value,\n ...ttl,\n });\n case \"SRV\":\n return DnsRecord.srv({\n name,\n target: answer.data.target,\n port: required(answer.data.port),\n priority: answer.data.priority ?? 0,\n weight: answer.data.weight ?? 0,\n ...ttl,\n });\n default:\n return opaque();\n }\n } catch {\n return opaque();\n }\n}\n\nfunction required<A>(value: A | undefined): A {\n if (value === undefined) throw new Error(\"DoH answer omitted a required field\");\n return value;\n}\n\nfunction decodeTxt(data: DnsPacket.TxtData): string {\n const chunks = Array.isArray(data) ? data : [data];\n const encoded = chunks.map((chunk) =>\n typeof chunk === \"string\" ? new TextEncoder().encode(chunk) : chunk,\n );\n const joined = new Uint8Array(encoded.reduce((total, chunk) => total + chunk.length, 0));\n let offset = 0;\n for (const chunk of encoded) {\n joined.set(chunk, offset);\n offset += chunk.length;\n }\n return new TextDecoder(\"utf-8\", { fatal: true }).decode(joined);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"doh.mjs","names":["Errors.DomainKitError","Reason.ResolverFailed","Errors.isDomainKitError","DnsRecord.Opaque","DnsRecord.a","DnsRecord.aaaa","DnsRecord.cname","DnsRecord.ns","DnsRecord.txt","DnsRecord.mx","DnsRecord.caa","DnsRecord.srv"],"sources":["../../src/internal/doh.ts"],"sourcesContent":["import * as DnsPacket from \"@leichtgewicht/dns-packet\";\nimport { Effect } from \"effect\";\n\nimport * as DnsRecord from \"../DnsRecord.ts\";\nimport * as Errors from \"./error.ts\";\nimport * as Reason from \"../Reason.ts\";\nimport type { Fetch } from \"./http.ts\";\n\nexport interface Answer {\n readonly records: ReadonlyArray<DnsRecord.Observed>;\n readonly negative: boolean;\n readonly ttl: number | null;\n}\n\nconst failed = (resolver: string, message: string) =>\n new Errors.DomainKitError({\n reason: new Reason.ResolverFailed({ resolver, message }),\n });\n\n/** One RFC 8484 wire-format query; the caller applies the timeout. */\nexport const query = (input: {\n readonly resolver: string;\n readonly url: string;\n readonly fetch: Fetch;\n readonly name: string;\n readonly type: DnsRecord.Type;\n /** Host-supplied abort, combined with the fiber's own interruption signal. */\n readonly signal?: AbortSignal | undefined;\n}): Effect.Effect<Answer, Errors.DomainKitError> =>\n Effect.tryPromise({\n try: async (fiberSignal) => {\n const controller = new AbortController();\n const abort = (reason: unknown) => controller.abort(reason);\n fiberSignal.addEventListener(\"abort\", () => abort(fiberSignal.reason), { once: true });\n input.signal?.addEventListener(\"abort\", () => abort(input.signal?.reason), { once: true });\n if (input.signal?.aborted === true) abort(input.signal.reason);\n const signal = controller.signal;\n const body = Uint8Array.from(\n DnsPacket.encode({\n flags: DnsPacket.RECURSION_DESIRED,\n id: 0,\n questions: [{ name: input.name, type: input.type }],\n type: \"query\",\n }),\n );\n const { fetch: send } = input; // a free call keeps `this` undefined for a native fetch\n const response = await send(input.url, {\n body,\n headers: { accept: \"application/dns-message\", \"content-type\": \"application/dns-message\" },\n method: \"POST\",\n signal,\n });\n if (!response.ok) throw failed(input.resolver, `DoH returned HTTP ${response.status}`);\n const contentType = response.headers\n .get(\"content-type\")\n ?.split(\";\", 1)[0]\n ?.trim()\n .toLowerCase();\n if (contentType !== \"application/dns-message\") {\n throw failed(\n input.resolver,\n `DoH returned unsupported content type ${contentType ?? \"<missing>\"}`,\n );\n }\n return decode(\n input.resolver,\n input.name,\n input.type,\n new Uint8Array(await response.arrayBuffer()),\n );\n },\n catch: (cause) =>\n Errors.isDomainKitError(cause)\n ? cause\n : failed(input.resolver, cause instanceof Error ? cause.message : \"DNS resolution failed\"),\n });\n\nfunction decode(resolver: string, name: string, type: DnsRecord.Type, message: Uint8Array): Answer {\n let packet: DnsPacket.Packet;\n try {\n packet = DnsPacket.decode(message);\n } catch {\n throw failed(resolver, \"DoH returned an invalid DNS message\");\n }\n if (packet.type !== \"response\") throw failed(resolver, \"DoH returned a DNS query\");\n if (packet.id !== 0) throw failed(resolver, \"DoH response used an unexpected message ID\");\n if (packet.flag_tc) throw failed(resolver, \"DoH returned a truncated DNS response\");\n const question = packet.questions?.[0];\n if (\n packet.questions?.length !== 1 ||\n question === undefined ||\n question.class !== \"IN\" ||\n question.type !== type ||\n question.name.toLowerCase().replace(/\\.$/, \"\") !== name\n ) {\n throw failed(resolver, \"DoH response did not match the DNS question\");\n }\n if (packet.rcode === \"NXDOMAIN\") return { records: [], negative: true, ttl: null };\n if (packet.rcode !== \"NOERROR\") {\n throw failed(resolver, `DNS resolver returned ${packet.rcode ?? \"an unknown response code\"}`);\n }\n const answers = (packet.answers ?? []).filter((answer) => answer.class === \"IN\");\n if (answers.some((answer) => answer.ttl === undefined)) {\n throw failed(resolver, \"DoH response omitted the DNS record TTL\");\n }\n const records = answers.map(toRecord);\n const ttls = answers.flatMap((answer) => (answer.ttl === undefined ? [] : [answer.ttl]));\n return {\n records,\n negative: records.length === 0,\n ttl: ttls.length === 0 ? null : Math.min(...ttls),\n };\n}\n\nfunction toRecord(answer: DnsPacket.Answer): DnsRecord.Observed {\n const name = answer.name;\n const opaque = () => new DnsRecord.Opaque({ name, type: answer.type, raw: answer });\n const ttl = answer.ttl === undefined || answer.ttl <= 0 ? {} : { ttl: answer.ttl };\n try {\n switch (answer.type) {\n case \"A\":\n return DnsRecord.a({ name, address: answer.data, ...ttl });\n case \"AAAA\":\n return DnsRecord.aaaa({ name, address: answer.data, ...ttl });\n case \"CNAME\":\n return DnsRecord.cname({ name, target: answer.data, ...ttl });\n case \"NS\":\n return DnsRecord.ns({ name, nameserver: answer.data, ...ttl });\n case \"TXT\":\n return DnsRecord.txt({ name, value: decodeTxt(answer.data), ...ttl });\n case \"MX\":\n return DnsRecord.mx({\n name,\n exchange: answer.data.exchange,\n priority: answer.data.preference ?? 0,\n ...ttl,\n });\n case \"CAA\":\n return DnsRecord.caa({\n name,\n flags: answer.data.flags ?? 0,\n tag: answer.data.tag,\n value: answer.data.value,\n ...ttl,\n });\n case \"SRV\":\n return DnsRecord.srv({\n name,\n target: answer.data.target,\n port: required(answer.data.port),\n priority: answer.data.priority ?? 0,\n weight: answer.data.weight ?? 0,\n ...ttl,\n });\n default:\n return opaque();\n }\n } catch {\n return opaque();\n }\n}\n\nfunction required<A>(value: A | undefined): A {\n if (value === undefined) throw new Error(\"DoH answer omitted a required field\");\n return value;\n}\n\nfunction decodeTxt(data: DnsPacket.TxtData): string {\n const chunks = Array.isArray(data) ? data : [data];\n const encoded = chunks.map((chunk) =>\n typeof chunk === \"string\" ? new TextEncoder().encode(chunk) : chunk,\n );\n const joined = new Uint8Array(encoded.reduce((total, chunk) => total + chunk.length, 0));\n let offset = 0;\n for (const chunk of encoded) {\n joined.set(chunk, offset);\n offset += chunk.length;\n }\n return new TextDecoder(\"utf-8\", { fatal: true }).decode(joined);\n}\n"],"mappings":"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"}
|
package/dist/internal/http.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Forbidden, ProviderConflict, ProviderRejected, ProviderUnavailable, Unauthenticated } from "../Reason.mjs";
|
|
2
|
-
import { DomainKitError, isDomainKitError } from "./error.mjs";
|
|
2
|
+
import { DomainKitError, fail, isDomainKitError } from "./error.mjs";
|
|
3
3
|
import { Effect } from "effect";
|
|
4
4
|
//#region src/internal/http.ts
|
|
5
5
|
/** One provider HTTP round trip: transport failures and non-JSON replies are already classified. */
|
|
6
6
|
const requestJson = (input) => Effect.tryPromise({
|
|
7
7
|
try: async (signal) => {
|
|
8
|
-
const
|
|
8
|
+
const { fetch: send } = input;
|
|
9
|
+
const response = await send(input.url, {
|
|
9
10
|
...input.init,
|
|
10
11
|
signal
|
|
11
12
|
});
|
|
@@ -62,6 +63,11 @@ const classify = (provider, status, headers, message, code) => {
|
|
|
62
63
|
code: code ?? String(status)
|
|
63
64
|
});
|
|
64
65
|
};
|
|
66
|
+
/**
|
|
67
|
+
* During token verification a `Forbidden` answer means the provider did not accept the token, so
|
|
68
|
+
* it reads as `Unauthenticated` with the provider's own message.
|
|
69
|
+
*/
|
|
70
|
+
const rejectedToken = (verification) => verification.pipe(Effect.catchIf((error) => error.reason._tag === "Forbidden", (error) => fail(new Unauthenticated({ message: error.reason.message }))));
|
|
65
71
|
const rejected = (provider, message, code) => new DomainKitError({ reason: new ProviderRejected({
|
|
66
72
|
provider,
|
|
67
73
|
message,
|
|
@@ -84,6 +90,6 @@ const bearer = (token, init) => ({
|
|
|
84
90
|
}
|
|
85
91
|
});
|
|
86
92
|
//#endregion
|
|
87
|
-
export { bearer, classify, headersFrom, rejected, requestJson };
|
|
93
|
+
export { bearer, classify, headersFrom, rejected, rejectedToken, requestJson };
|
|
88
94
|
|
|
89
95
|
//# sourceMappingURL=http.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.mjs","names":["Errors.DomainKitError","Reason.ProviderRejected","Errors.isDomainKitError","Reason.ProviderUnavailable","Reason.Unauthenticated","Reason.Forbidden","Reason.ProviderConflict"],"sources":["../../src/internal/http.ts"],"sourcesContent":["import { Effect } from \"effect\";\n\nimport * as Errors from \"./error.ts\";\nimport * as Reason from \"../Reason.ts\";\n\nexport type Fetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;\n\nexport interface Reply {\n readonly status: number;\n readonly ok: boolean;\n readonly headers: Headers;\n /** Parsed JSON, or `null` for an empty body. */\n readonly body: unknown;\n}\n\n/** One provider HTTP round trip: transport failures and non-JSON replies are already classified. */\nexport const requestJson = (input: {\n readonly fetch: Fetch;\n readonly provider: string;\n readonly url: URL | string;\n readonly init?: RequestInit;\n}): Effect.Effect<Reply, Errors.DomainKitError> =>\n Effect.tryPromise({\n try: async (signal): Promise<Reply> => {\n const response = await
|
|
1
|
+
{"version":3,"file":"http.mjs","names":["Errors.DomainKitError","Reason.ProviderRejected","Errors.isDomainKitError","Reason.ProviderUnavailable","Reason.Unauthenticated","Reason.Forbidden","Reason.ProviderConflict","Errors.fail"],"sources":["../../src/internal/http.ts"],"sourcesContent":["import { Effect } from \"effect\";\n\nimport * as Errors from \"./error.ts\";\nimport * as Reason from \"../Reason.ts\";\n\nexport type Fetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;\n\nexport interface Reply {\n readonly status: number;\n readonly ok: boolean;\n readonly headers: Headers;\n /** Parsed JSON, or `null` for an empty body. */\n readonly body: unknown;\n}\n\n/** One provider HTTP round trip: transport failures and non-JSON replies are already classified. */\nexport const requestJson = (input: {\n readonly fetch: Fetch;\n readonly provider: string;\n readonly url: URL | string;\n readonly init?: RequestInit;\n}): Effect.Effect<Reply, Errors.DomainKitError> =>\n Effect.tryPromise({\n try: async (signal): Promise<Reply> => {\n const { fetch: send } = input; // a free call keeps `this` undefined for a native fetch\n const response = await send(input.url, { ...input.init, signal });\n const text = await response.text();\n let body: unknown = null;\n if (text.length > 0) {\n try {\n body = JSON.parse(text);\n } catch {\n throw new Errors.DomainKitError({\n reason: response.ok\n ? new Reason.ProviderRejected({\n provider: input.provider,\n message: `${input.provider} returned a non-JSON response`,\n })\n : classify(\n input.provider,\n response.status,\n response.headers,\n `HTTP ${response.status}`,\n ),\n });\n }\n }\n return { status: response.status, ok: response.ok, headers: response.headers, body };\n },\n catch: (cause) =>\n Errors.isDomainKitError(cause)\n ? cause\n : new Errors.DomainKitError({\n reason: new Reason.ProviderUnavailable({\n provider: input.provider,\n message: cause instanceof Error ? cause.message : `${input.provider} request failed`,\n }),\n }),\n });\n\n/** Host-supplied request headers, given as a value or as a thunk that may await a fresh token. */\nexport const headersFrom = (\n source: HeadersInit | (() => HeadersInit | Promise<HeadersInit>) | undefined,\n): Effect.Effect<Headers, Errors.DomainKitError> =>\n source === undefined\n ? Effect.sync(() => new Headers())\n : typeof source !== \"function\"\n ? Effect.sync(() => new Headers(source))\n : Effect.tryPromise({\n try: async () => new Headers(await source()),\n catch: (cause) =>\n Errors.isDomainKitError(cause)\n ? cause\n : new Errors.DomainKitError({\n reason: new Reason.Unauthenticated({\n message:\n cause instanceof Error ? cause.message : \"Request headers were unavailable\",\n }),\n }),\n });\n\n/**\n * Map an HTTP failure to a reason: 401 -> Unauthenticated, 403 -> Forbidden, 409 or a provider\n * conflict code -> ProviderConflict, 429/5xx -> ProviderUnavailable, else ProviderRejected.\n */\nexport const classify = (\n provider: string,\n status: number,\n headers: Headers,\n message: string,\n code?: string,\n): Reason.Model => {\n if (status === 401) return new Reason.Unauthenticated({ message });\n if (status === 403) return new Reason.Forbidden({ message });\n if (status === 409 || code === \"conflict\") {\n return new Reason.ProviderConflict({\n provider,\n message,\n code: code ?? String(status),\n });\n }\n if (status === 429 || status >= 500) {\n const retryAfterMs = retryAfter(headers.get(\"retry-after\"));\n return new Reason.ProviderUnavailable({\n provider,\n message,\n ...(retryAfterMs === undefined ? {} : { retryAfterMs }),\n });\n }\n return new Reason.ProviderRejected({\n provider,\n message,\n code: code ?? String(status),\n });\n};\n\n/**\n * During token verification a `Forbidden` answer means the provider did not accept the token, so\n * it reads as `Unauthenticated` with the provider's own message.\n */\nexport const rejectedToken = <A, R>(\n verification: Effect.Effect<A, Errors.DomainKitError, R>,\n): Effect.Effect<A, Errors.DomainKitError, R> =>\n verification.pipe(\n Effect.catchIf(\n (error) => error.reason._tag === \"Forbidden\",\n (error) => Errors.fail(new Reason.Unauthenticated({ message: error.reason.message })),\n ),\n );\n\nexport const rejected = (provider: string, message: string, code?: string) =>\n new Errors.DomainKitError({\n reason: new Reason.ProviderRejected({\n provider,\n message,\n ...(code === undefined ? {} : { code }),\n }),\n });\n\nfunction retryAfter(value: string | null): number | undefined {\n if (value === null) return undefined;\n const seconds = Number(value);\n if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);\n const date = Date.parse(value);\n return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());\n}\n\nexport const bearer = (token: string, init?: RequestInit): RequestInit => ({\n ...init,\n headers: {\n Accept: \"application/json\",\n Authorization: `Bearer ${token}`,\n ...(init?.body === undefined ? {} : { \"Content-Type\": \"application/json\" }),\n ...init?.headers,\n },\n});\n"],"mappings":"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"}
|
package/dist/package.mjs
CHANGED
package/dist/types/Connect.d.ts
CHANGED
|
@@ -283,14 +283,24 @@ export type Discovery = Data.TaggedEnum<{
|
|
|
283
283
|
readonly target: Provider.Target;
|
|
284
284
|
}>;
|
|
285
285
|
};
|
|
286
|
+
/**
|
|
287
|
+
* No connection reaches the domain. `host` names the one registered provider whose declared
|
|
288
|
+
* nameserver suffixes cover every authoritative nameserver; `null` when none or several do.
|
|
289
|
+
*/
|
|
286
290
|
NotFound: {
|
|
287
291
|
readonly nameservers: ReadonlyArray<string>;
|
|
292
|
+
readonly host: {
|
|
293
|
+
readonly provider: string;
|
|
294
|
+
} | null;
|
|
288
295
|
};
|
|
289
296
|
}>;
|
|
290
297
|
export declare const Discovery: {
|
|
291
298
|
readonly $is: <Tag extends "NotFound" | "Resolved" | "SelectionRequired">(tag: Tag) => (u: unknown) => u is Extract<{
|
|
292
299
|
readonly _tag: "NotFound";
|
|
293
300
|
readonly nameservers: ReadonlyArray<string>;
|
|
301
|
+
readonly host: {
|
|
302
|
+
readonly provider: string;
|
|
303
|
+
} | null;
|
|
294
304
|
}, {
|
|
295
305
|
readonly _tag: Tag;
|
|
296
306
|
}> | Extract<{
|
|
@@ -312,6 +322,9 @@ export declare const Discovery: {
|
|
|
312
322
|
<Cases extends { readonly [Tag in "NotFound" | "Resolved" | "SelectionRequired"]: (args: Extract<{
|
|
313
323
|
readonly _tag: "NotFound";
|
|
314
324
|
readonly nameservers: ReadonlyArray<string>;
|
|
325
|
+
readonly host: {
|
|
326
|
+
readonly provider: string;
|
|
327
|
+
} | null;
|
|
315
328
|
} | {
|
|
316
329
|
readonly _tag: "Resolved";
|
|
317
330
|
readonly connectionId: string;
|
|
@@ -327,6 +340,9 @@ export declare const Discovery: {
|
|
|
327
340
|
}>) => any; }>(cases: Cases): (value: {
|
|
328
341
|
readonly _tag: "NotFound";
|
|
329
342
|
readonly nameservers: ReadonlyArray<string>;
|
|
343
|
+
readonly host: {
|
|
344
|
+
readonly provider: string;
|
|
345
|
+
} | null;
|
|
330
346
|
} | {
|
|
331
347
|
readonly _tag: "Resolved";
|
|
332
348
|
readonly connectionId: string;
|
|
@@ -341,6 +357,9 @@ export declare const Discovery: {
|
|
|
341
357
|
<Cases extends { readonly [Tag in "NotFound" | "Resolved" | "SelectionRequired"]: (args: Extract<{
|
|
342
358
|
readonly _tag: "NotFound";
|
|
343
359
|
readonly nameservers: ReadonlyArray<string>;
|
|
360
|
+
readonly host: {
|
|
361
|
+
readonly provider: string;
|
|
362
|
+
} | null;
|
|
344
363
|
} | {
|
|
345
364
|
readonly _tag: "Resolved";
|
|
346
365
|
readonly connectionId: string;
|
|
@@ -356,6 +375,9 @@ export declare const Discovery: {
|
|
|
356
375
|
}>) => any; }>(value: {
|
|
357
376
|
readonly _tag: "NotFound";
|
|
358
377
|
readonly nameservers: ReadonlyArray<string>;
|
|
378
|
+
readonly host: {
|
|
379
|
+
readonly provider: string;
|
|
380
|
+
} | null;
|
|
359
381
|
} | {
|
|
360
382
|
readonly _tag: "Resolved";
|
|
361
383
|
readonly connectionId: string;
|
|
@@ -371,6 +393,9 @@ export declare const Discovery: {
|
|
|
371
393
|
readonly NotFound: Data.TaggedEnum.ConstructorFrom<{
|
|
372
394
|
readonly _tag: "NotFound";
|
|
373
395
|
readonly nameservers: ReadonlyArray<string>;
|
|
396
|
+
readonly host: {
|
|
397
|
+
readonly provider: string;
|
|
398
|
+
} | null;
|
|
374
399
|
}, "_tag">;
|
|
375
400
|
readonly Resolved: Data.TaggedEnum.ConstructorFrom<{
|
|
376
401
|
readonly _tag: "Resolved";
|
|
@@ -446,6 +471,9 @@ export declare const inspect: (domain: string) => Effect.Effect<Snapshot, Errors
|
|
|
446
471
|
export declare const discover: (domain: string) => Effect.Effect<{
|
|
447
472
|
readonly _tag: "NotFound";
|
|
448
473
|
readonly nameservers: ReadonlyArray<string>;
|
|
474
|
+
readonly host: {
|
|
475
|
+
readonly provider: string;
|
|
476
|
+
} | null;
|
|
449
477
|
} | {
|
|
450
478
|
readonly _tag: "Resolved";
|
|
451
479
|
readonly connectionId: string;
|
package/dist/types/Provider.d.ts
CHANGED
|
@@ -210,6 +210,12 @@ export interface IntegrationAuth {
|
|
|
210
210
|
export interface Definition<Context = unknown> {
|
|
211
211
|
readonly id: string;
|
|
212
212
|
readonly name: string;
|
|
213
|
+
/**
|
|
214
|
+
* Hostname suffixes of the nameservers this provider operates, e.g. `ns.cloudflare.com`.
|
|
215
|
+
* `Connect.discover` names the provider as a domain's host when every authoritative nameserver
|
|
216
|
+
* ends in one of them.
|
|
217
|
+
*/
|
|
218
|
+
readonly nameservers?: ReadonlyArray<string>;
|
|
213
219
|
readonly auth: {
|
|
214
220
|
readonly token?: TokenAuth;
|
|
215
221
|
readonly oauth?: OAuthAuth;
|
package/dist/types/Server.d.ts
CHANGED
|
@@ -198,7 +198,11 @@ export declare class DiscoverySelectionRequired extends DiscoverySelectionRequir
|
|
|
198
198
|
}
|
|
199
199
|
declare const DiscoveryNotFound_base: Schema.Class<DiscoveryNotFound, Schema.TaggedStruct<"NotFound", {
|
|
200
200
|
readonly nameservers: Schema.$Array<Schema.String>;
|
|
201
|
+
readonly host: Schema.NullOr<Schema.Struct<{
|
|
202
|
+
readonly provider: Schema.String;
|
|
203
|
+
}>>;
|
|
201
204
|
}>, {}>;
|
|
205
|
+
/** `host` names the registered provider whose nameservers serve the domain, by definition id. */
|
|
202
206
|
export declare class DiscoveryNotFound extends DiscoveryNotFound_base {
|
|
203
207
|
}
|
|
204
208
|
export declare const Discovery: Schema.Union<readonly [typeof DiscoveryResolved, typeof DiscoverySelectionRequired, typeof DiscoveryNotFound]>;
|
package/dist/types/Testing.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface FakeProviderOptions {
|
|
|
29
29
|
readonly failWrite?: (index: number) => boolean;
|
|
30
30
|
/** Nameservers per zone; default `ns1.<zone>`, `ns2.<zone>`. `resolver()` answers NS queries from them. */
|
|
31
31
|
readonly nameservers?: Readonly<Record<string, ReadonlyArray<string>>>;
|
|
32
|
+
/** Suffixes the fake declares as `Definition.nameservers`, so `Connect.discover` can name it as a host. */
|
|
33
|
+
readonly nameserverSuffixes?: ReadonlyArray<string>;
|
|
32
34
|
}
|
|
33
35
|
export interface FakeProvider extends Provider.Definition<FakeContext> {
|
|
34
36
|
readonly records: (zone: string) => ReadonlyArray<DnsRecord.Observed>;
|
|
@@ -79,6 +81,6 @@ export type { Case as StorageCase } from "./internal/conformance/storage.ts";
|
|
|
79
81
|
export declare const conformance: {
|
|
80
82
|
/** Runs every Storage invariant (tenant isolation, leases, exactly-once continuations, revocation recovery). */
|
|
81
83
|
storage: typeof storageCases;
|
|
82
|
-
/** Runs create/readback/cleanup, exact-noop, conflict, stale-plan, partial-apply against a real provider definition. */
|
|
84
|
+
/** Runs create/readback/cleanup, exact-noop, conflict, stale-plan, partial-apply, and rejected-token against a real provider definition. */
|
|
83
85
|
provider: typeof providerCases;
|
|
84
86
|
};
|
package/dist/types/Vercel.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Vercel: personal tokens and marketplace integrations. */
|
|
2
2
|
import { type Config, Redacted, Schema } from "effect";
|
|
3
|
-
import type
|
|
3
|
+
import { type Fetch } from "./internal/http.ts";
|
|
4
4
|
import * as Provider from "./Provider.ts";
|
|
5
5
|
/** `teamId` is `null` for a personal account; targets carry the team that owns the zone. */
|
|
6
6
|
export declare const TeamContext: Schema.Struct<{
|