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.
@@ -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
- /** Token expiry from the verify endpoint; `null` when the token never expires or verification is unavailable. */
64
- /** Token status and expiry from a verify endpoint; an inactive token is `Unauthenticated`. */
65
- 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}` }))));
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) => 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}` }))));
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, and partial-apply against a
71
- * real provider definition, through the same Provision and Cleanup services hosts use.
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$2 = yield* plan({
90
+ const plan$4 = yield* plan({
90
91
  domain: attachment.domain,
91
92
  requirements
92
93
  });
93
- yield* expect(name, plan$2.operations.every(({ _tag }) => _tag === "Create"), "expected two Create operations");
94
- const receipt = yield* apply(yield* approve(plan$2));
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$3 = yield* plan({
115
+ const plan$5 = yield* plan({
115
116
  domain: attachment.domain,
116
117
  requirements: [requirement]
117
118
  });
118
- yield* expect(name, plan$3.operations[0]?._tag === "Noop", "an exact record must plan as Noop");
119
- const receipt = yield* apply(yield* approve(plan$3));
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$6 = yield* 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$6.operations[0]?._tag === "Conflict", "a CNAME over an occupied name must plan as Conflict");
134
- const rejected = yield* approve(plan$6).pipe(Effect.result);
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$4 = yield* plan({
144
+ const plan$6 = yield* plan({
144
145
  domain: attachment.domain,
145
146
  requirements: [requirement]
146
147
  });
147
- const approval = yield* approve(plan$4);
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$5 = yield* 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$5));
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":"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"}
@@ -26,7 +26,8 @@ const query = (input) => Effect.tryPromise({
26
26
  }],
27
27
  type: "query"
28
28
  }));
29
- const response = await input.fetch(input.url, {
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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"}
@@ -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 response = await input.fetch(input.url, {
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 input.fetch(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\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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"}
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
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.9.0";
2
+ var version = "0.10.0";
3
3
  //#endregion
4
4
  export { version };
5
5
 
@@ -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;
@@ -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;
@@ -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]>;
@@ -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
  };
@@ -1,6 +1,6 @@
1
1
  /** Vercel: personal tokens and marketplace integrations. */
2
2
  import { type Config, Redacted, Schema } from "effect";
3
- import type { Fetch } from "./internal/http.ts";
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<{