domainkit 0.8.0 → 0.9.1

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.
Files changed (165) hide show
  1. package/README.md +213 -41
  2. package/dist/Approval.mjs +34 -0
  3. package/dist/Approval.mjs.map +1 -0
  4. package/dist/Cleanup.mjs +97 -0
  5. package/dist/Cleanup.mjs.map +1 -0
  6. package/dist/Cloudflare.mjs +225 -0
  7. package/dist/Cloudflare.mjs.map +1 -0
  8. package/dist/Connect.mjs +463 -0
  9. package/dist/Connect.mjs.map +1 -0
  10. package/dist/Custody.mjs +64 -0
  11. package/dist/Custody.mjs.map +1 -0
  12. package/dist/DnsRecord.mjs +213 -0
  13. package/dist/DnsRecord.mjs.map +1 -0
  14. package/dist/DomainKit.mjs +34 -0
  15. package/dist/DomainKit.mjs.map +1 -0
  16. package/dist/DomainName.mjs +100 -0
  17. package/dist/DomainName.mjs.map +1 -0
  18. package/dist/Plan.mjs +102 -0
  19. package/dist/Plan.mjs.map +1 -0
  20. package/dist/Principal.mjs +23 -0
  21. package/dist/Principal.mjs.map +1 -0
  22. package/dist/Provider.mjs +130 -0
  23. package/dist/Provider.mjs.map +1 -0
  24. package/dist/Providers.mjs +40 -0
  25. package/dist/Providers.mjs.map +1 -0
  26. package/dist/Provision.mjs +93 -0
  27. package/dist/Provision.mjs.map +1 -0
  28. package/dist/Reason.mjs +162 -0
  29. package/dist/Reason.mjs.map +1 -0
  30. package/dist/Receipt.mjs +71 -0
  31. package/dist/Receipt.mjs.map +1 -0
  32. package/dist/Resolver.mjs +75 -0
  33. package/dist/Resolver.mjs.map +1 -0
  34. package/dist/Server.mjs +516 -0
  35. package/dist/Server.mjs.map +1 -0
  36. package/dist/Storage.mjs +236 -0
  37. package/dist/Storage.mjs.map +1 -0
  38. package/dist/Testing.mjs +226 -0
  39. package/dist/Testing.mjs.map +1 -0
  40. package/dist/Transport.mjs +187 -0
  41. package/dist/Transport.mjs.map +1 -0
  42. package/dist/Vercel.mjs +116 -0
  43. package/dist/Vercel.mjs.map +1 -0
  44. package/dist/Verify.mjs +292 -0
  45. package/dist/Verify.mjs.map +1 -0
  46. package/dist/{rolldown-runtime-D7D4PA-g.mjs → _virtual/_rolldown/runtime.mjs} +1 -1
  47. package/dist/entry/client.mjs +2 -0
  48. package/dist/entry/server.mjs +2 -0
  49. package/dist/entry/testing.mjs +3 -0
  50. package/dist/index.mjs +33 -462
  51. package/dist/index.mjs.map +1 -1
  52. package/dist/internal/aes.mjs +61 -0
  53. package/dist/internal/aes.mjs.map +1 -0
  54. package/dist/internal/attempts.mjs +284 -0
  55. package/dist/internal/attempts.mjs.map +1 -0
  56. package/dist/internal/cloudflare/client.mjs +100 -0
  57. package/dist/internal/cloudflare/client.mjs.map +1 -0
  58. package/dist/internal/cloudflare/protocol.mjs +59 -0
  59. package/dist/internal/cloudflare/protocol.mjs.map +1 -0
  60. package/dist/internal/cloudflare/records.mjs +156 -0
  61. package/dist/internal/cloudflare/records.mjs.map +1 -0
  62. package/dist/internal/config.mjs +13 -0
  63. package/dist/internal/config.mjs.map +1 -0
  64. package/dist/internal/conformance/provider.mjs +207 -0
  65. package/dist/internal/conformance/provider.mjs.map +1 -0
  66. package/dist/internal/conformance/storage.mjs +428 -0
  67. package/dist/internal/conformance/storage.mjs.map +1 -0
  68. package/dist/internal/digest.mjs +25 -0
  69. package/dist/internal/digest.mjs.map +1 -0
  70. package/dist/internal/doh.mjs +153 -0
  71. package/dist/internal/doh.mjs.map +1 -0
  72. package/dist/internal/error.mjs +69 -0
  73. package/dist/internal/error.mjs.map +1 -0
  74. package/dist/internal/http.mjs +90 -0
  75. package/dist/internal/http.mjs.map +1 -0
  76. package/dist/internal/ids.mjs +8 -0
  77. package/dist/internal/ids.mjs.map +1 -0
  78. package/dist/internal/oauth.mjs +113 -0
  79. package/dist/internal/oauth.mjs.map +1 -0
  80. package/dist/internal/planner.mjs +88 -0
  81. package/dist/internal/planner.mjs.map +1 -0
  82. package/dist/internal/provider-async.mjs +83 -0
  83. package/dist/internal/provider-async.mjs.map +1 -0
  84. package/dist/internal/storage-memory.mjs +302 -0
  85. package/dist/internal/storage-memory.mjs.map +1 -0
  86. package/dist/internal/vercel/client.mjs +122 -0
  87. package/dist/internal/vercel/client.mjs.map +1 -0
  88. package/dist/internal/vercel/protocol.mjs +80 -0
  89. package/dist/internal/vercel/protocol.mjs.map +1 -0
  90. package/dist/internal/vercel/records.mjs +151 -0
  91. package/dist/internal/vercel/records.mjs.map +1 -0
  92. package/dist/package.mjs +6 -0
  93. package/dist/package.mjs.map +1 -0
  94. package/dist/types/Approval.d.ts +26 -0
  95. package/dist/types/Cleanup.d.ts +41 -0
  96. package/dist/types/Cloudflare.d.ts +36 -0
  97. package/dist/types/Connect.d.ts +506 -0
  98. package/dist/types/Custody.d.ts +38 -0
  99. package/dist/types/DnsRecord.d.ts +158 -0
  100. package/dist/types/DomainKit.d.ts +31 -0
  101. package/dist/types/DomainName.d.ts +27 -0
  102. package/dist/types/Plan.d.ts +76 -0
  103. package/dist/types/Principal.d.ts +18 -0
  104. package/dist/types/Provider.d.ts +248 -0
  105. package/dist/types/Providers.d.ts +16 -0
  106. package/dist/types/Provision.d.ts +75 -0
  107. package/dist/types/Reason.d.ts +118 -0
  108. package/dist/types/Receipt.d.ts +55 -0
  109. package/dist/types/Resolver.d.ts +135 -0
  110. package/dist/types/Server.d.ts +6523 -0
  111. package/dist/types/Storage.d.ts +314 -0
  112. package/dist/types/Testing.d.ts +84 -0
  113. package/dist/types/Transport.d.ts +124 -0
  114. package/dist/types/Vercel.d.ts +25 -0
  115. package/dist/types/Verify.d.ts +109 -0
  116. package/dist/types/entry/client.d.ts +2 -0
  117. package/dist/types/entry/server.d.ts +2 -0
  118. package/dist/types/entry/testing.d.ts +3 -0
  119. package/dist/types/index.d.ts +21 -0
  120. package/dist/types/internal/aes.d.ts +8 -0
  121. package/dist/types/internal/attempts.d.ts +40 -0
  122. package/dist/types/internal/cloudflare/client.d.ts +23 -0
  123. package/dist/types/internal/cloudflare/protocol.d.ts +71 -0
  124. package/dist/types/internal/cloudflare/records.d.ts +8 -0
  125. package/dist/types/internal/config.d.ts +4 -0
  126. package/dist/types/internal/conformance/provider.d.ts +12 -0
  127. package/dist/types/internal/conformance/storage.d.ts +14 -0
  128. package/dist/types/internal/digest.d.ts +10 -0
  129. package/dist/types/internal/doh.d.ts +19 -0
  130. package/dist/types/internal/error.d.ts +24 -0
  131. package/dist/types/internal/http.d.ts +27 -0
  132. package/dist/types/internal/ids.d.ts +3 -0
  133. package/dist/types/internal/oauth.d.ts +58 -0
  134. package/dist/types/internal/planner.d.ts +22 -0
  135. package/dist/types/internal/provider-async.d.ts +78 -0
  136. package/dist/types/internal/storage-memory.d.ts +2 -0
  137. package/dist/types/internal/vercel/client.d.ts +29 -0
  138. package/dist/types/internal/vercel/protocol.d.ts +124 -0
  139. package/dist/types/internal/vercel/records.d.ts +7 -0
  140. package/package.json +88 -12
  141. package/dist/canonical-json-CQYKsZw7.mjs +0 -310
  142. package/dist/canonical-json-CQYKsZw7.mjs.map +0 -1
  143. package/dist/connect-D-X8T9W8.d.mts +0 -1597
  144. package/dist/connect-oI8hcMxc.mjs +0 -185
  145. package/dist/connect-oI8hcMxc.mjs.map +0 -1
  146. package/dist/connection-Dp6ZdSo8.d.mts +0 -186
  147. package/dist/connection-api-lqG1nBtD.d.mts +0 -26
  148. package/dist/index.d.mts +0 -1551
  149. package/dist/managed-dns-connections-C3A_GBaz.mjs +0 -99
  150. package/dist/managed-dns-connections-C3A_GBaz.mjs.map +0 -1
  151. package/dist/promise.d.mts +0 -378
  152. package/dist/promise.mjs +0 -598
  153. package/dist/promise.mjs.map +0 -1
  154. package/dist/resolver-BeGz1ukw.mjs +0 -921
  155. package/dist/resolver-BeGz1ukw.mjs.map +0 -1
  156. package/dist/resolver-xoSwyXDJ.d.mts +0 -3734
  157. package/dist/server.d.mts +0 -128
  158. package/dist/server.mjs +0 -319
  159. package/dist/server.mjs.map +0 -1
  160. package/dist/testing.d.mts +0 -62
  161. package/dist/testing.mjs +0 -507
  162. package/dist/testing.mjs.map +0 -1
  163. package/dist/verify-DvGPb5C9.mjs +0 -2402
  164. package/dist/verify-DvGPb5C9.mjs.map +0 -1
  165. package/dist/verify-FpunAcM7.d.mts +0 -1335
@@ -0,0 +1,153 @@
1
+ import { Opaque, a, aaaa, caa, cname, mx, ns, srv, txt } from "../DnsRecord.mjs";
2
+ import { ResolverFailed } from "../Reason.mjs";
3
+ import { DomainKitError, isDomainKitError } from "./error.mjs";
4
+ import { Effect } from "effect";
5
+ import * as DnsPacket from "@leichtgewicht/dns-packet";
6
+ //#region src/internal/doh.ts
7
+ const failed = (resolver, message) => new DomainKitError({ reason: new ResolverFailed({
8
+ resolver,
9
+ message
10
+ }) });
11
+ /** One RFC 8484 wire-format query; the caller applies the timeout. */
12
+ const query = (input) => Effect.tryPromise({
13
+ try: async (fiberSignal) => {
14
+ const controller = new AbortController();
15
+ const abort = (reason) => controller.abort(reason);
16
+ fiberSignal.addEventListener("abort", () => abort(fiberSignal.reason), { once: true });
17
+ input.signal?.addEventListener("abort", () => abort(input.signal?.reason), { once: true });
18
+ if (input.signal?.aborted === true) abort(input.signal.reason);
19
+ const signal = controller.signal;
20
+ const body = Uint8Array.from(DnsPacket.encode({
21
+ flags: DnsPacket.RECURSION_DESIRED,
22
+ id: 0,
23
+ questions: [{
24
+ name: input.name,
25
+ type: input.type
26
+ }],
27
+ type: "query"
28
+ }));
29
+ const { fetch: send } = input;
30
+ const response = await send(input.url, {
31
+ body,
32
+ headers: {
33
+ accept: "application/dns-message",
34
+ "content-type": "application/dns-message"
35
+ },
36
+ method: "POST",
37
+ signal
38
+ });
39
+ if (!response.ok) throw failed(input.resolver, `DoH returned HTTP ${response.status}`);
40
+ const contentType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
41
+ if (contentType !== "application/dns-message") throw failed(input.resolver, `DoH returned unsupported content type ${contentType ?? "<missing>"}`);
42
+ return decode(input.resolver, input.name, input.type, new Uint8Array(await response.arrayBuffer()));
43
+ },
44
+ catch: (cause) => isDomainKitError(cause) ? cause : failed(input.resolver, cause instanceof Error ? cause.message : "DNS resolution failed")
45
+ });
46
+ function decode(resolver, name, type, message) {
47
+ let packet;
48
+ try {
49
+ packet = DnsPacket.decode(message);
50
+ } catch {
51
+ throw failed(resolver, "DoH returned an invalid DNS message");
52
+ }
53
+ if (packet.type !== "response") throw failed(resolver, "DoH returned a DNS query");
54
+ if (packet.id !== 0) throw failed(resolver, "DoH response used an unexpected message ID");
55
+ if (packet.flag_tc) throw failed(resolver, "DoH returned a truncated DNS response");
56
+ const question = packet.questions?.[0];
57
+ if (packet.questions?.length !== 1 || question === void 0 || question.class !== "IN" || question.type !== type || question.name.toLowerCase().replace(/\.$/, "") !== name) throw failed(resolver, "DoH response did not match the DNS question");
58
+ if (packet.rcode === "NXDOMAIN") return {
59
+ records: [],
60
+ negative: true,
61
+ ttl: null
62
+ };
63
+ if (packet.rcode !== "NOERROR") throw failed(resolver, `DNS resolver returned ${packet.rcode ?? "an unknown response code"}`);
64
+ const answers = (packet.answers ?? []).filter((answer) => answer.class === "IN");
65
+ if (answers.some((answer) => answer.ttl === void 0)) throw failed(resolver, "DoH response omitted the DNS record TTL");
66
+ const records = answers.map(toRecord);
67
+ const ttls = answers.flatMap((answer) => answer.ttl === void 0 ? [] : [answer.ttl]);
68
+ return {
69
+ records,
70
+ negative: records.length === 0,
71
+ ttl: ttls.length === 0 ? null : Math.min(...ttls)
72
+ };
73
+ }
74
+ function toRecord(answer) {
75
+ const name = answer.name;
76
+ const opaque = () => new Opaque({
77
+ name,
78
+ type: answer.type,
79
+ raw: answer
80
+ });
81
+ const ttl = answer.ttl === void 0 || answer.ttl <= 0 ? {} : { ttl: answer.ttl };
82
+ try {
83
+ switch (answer.type) {
84
+ case "A": return a({
85
+ name,
86
+ address: answer.data,
87
+ ...ttl
88
+ });
89
+ case "AAAA": return aaaa({
90
+ name,
91
+ address: answer.data,
92
+ ...ttl
93
+ });
94
+ case "CNAME": return cname({
95
+ name,
96
+ target: answer.data,
97
+ ...ttl
98
+ });
99
+ case "NS": return ns({
100
+ name,
101
+ nameserver: answer.data,
102
+ ...ttl
103
+ });
104
+ case "TXT": return txt({
105
+ name,
106
+ value: decodeTxt(answer.data),
107
+ ...ttl
108
+ });
109
+ case "MX": return mx({
110
+ name,
111
+ exchange: answer.data.exchange,
112
+ priority: answer.data.preference ?? 0,
113
+ ...ttl
114
+ });
115
+ case "CAA": return caa({
116
+ name,
117
+ flags: answer.data.flags ?? 0,
118
+ tag: answer.data.tag,
119
+ value: answer.data.value,
120
+ ...ttl
121
+ });
122
+ case "SRV": return srv({
123
+ name,
124
+ target: answer.data.target,
125
+ port: required(answer.data.port),
126
+ priority: answer.data.priority ?? 0,
127
+ weight: answer.data.weight ?? 0,
128
+ ...ttl
129
+ });
130
+ default: return opaque();
131
+ }
132
+ } catch {
133
+ return opaque();
134
+ }
135
+ }
136
+ function required(value) {
137
+ if (value === void 0) throw new Error("DoH answer omitted a required field");
138
+ return value;
139
+ }
140
+ function decodeTxt(data) {
141
+ const encoded = (Array.isArray(data) ? data : [data]).map((chunk) => typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk);
142
+ const joined = new Uint8Array(encoded.reduce((total, chunk) => total + chunk.length, 0));
143
+ let offset = 0;
144
+ for (const chunk of encoded) {
145
+ joined.set(chunk, offset);
146
+ offset += chunk.length;
147
+ }
148
+ return new TextDecoder("utf-8", { fatal: true }).decode(joined);
149
+ }
150
+ //#endregion
151
+ export { query };
152
+
153
+ //# sourceMappingURL=doh.mjs.map
@@ -0,0 +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 { 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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"}
@@ -0,0 +1,69 @@
1
+ import { InvalidInput, Model } from "../Reason.mjs";
2
+ import { Effect, Schema } from "effect";
3
+ //#region src/internal/error.ts
4
+ /**
5
+ * The one error every DomainKit operation fails with, published as `DomainKit.Error`. Hosts match
6
+ * on `reason`; `category`, `isRetryable`, and `httpStatus` derive from it. Modelled on `SqlError`
7
+ * and `HttpClientError` in Effect.
8
+ */
9
+ var DomainKitError = class extends Schema.TaggedError("@domainkit/DomainKitError")("DomainKitError", { reason: Schema.suspend(() => Model) }) {
10
+ get category() {
11
+ switch (this.reason._tag) {
12
+ case "InvalidInput":
13
+ case "NotFound": return "request";
14
+ case "Unauthenticated":
15
+ case "Forbidden":
16
+ case "Reconnect": return "auth";
17
+ case "Conflict":
18
+ case "Stale":
19
+ case "Expired":
20
+ case "Busy": return "plan";
21
+ case "ProviderRejected":
22
+ case "ProviderUnavailable":
23
+ case "ProviderConflict":
24
+ case "Unsupported": return "provider";
25
+ case "StorageFailed": return "storage";
26
+ case "CryptoFailed":
27
+ case "ResolverFailed": return "internal";
28
+ }
29
+ }
30
+ get isRetryable() {
31
+ return this.reason._tag === "Busy" || this.reason._tag === "ProviderUnavailable" || this.reason._tag === "StorageFailed";
32
+ }
33
+ /** The HTTP status `domainkit/server` answers with; hosts mapping their own routes use the same table. */
34
+ get httpStatus() {
35
+ switch (this.reason._tag) {
36
+ case "InvalidInput": return 400;
37
+ case "Unauthenticated": return 401;
38
+ case "Forbidden":
39
+ case "Reconnect": return 403;
40
+ case "NotFound": return 404;
41
+ case "Conflict":
42
+ case "Stale":
43
+ case "Expired":
44
+ case "Busy": return 409;
45
+ case "ProviderRejected": return 502;
46
+ case "ProviderConflict": return 409;
47
+ case "Unsupported": return 501;
48
+ case "ProviderUnavailable": return 503;
49
+ case "StorageFailed":
50
+ case "CryptoFailed":
51
+ case "ResolverFailed": return 500;
52
+ }
53
+ }
54
+ get message() {
55
+ return this.reason.message;
56
+ }
57
+ };
58
+ const isDomainKitError = (input) => input instanceof DomainKitError;
59
+ /** `Effect.fail(new DomainKitError({ reason }))` shorthand used across the package. */
60
+ const fail = (reason) => Effect.fail(new DomainKitError({ reason }));
61
+ /** Decode with any schema, failing with reason `InvalidInput` and the schema's message. */
62
+ const decode = (schema, input, field) => Schema.decodeUnknownEffect(schema)(input).pipe(Effect.mapError((cause) => new DomainKitError({ reason: new InvalidInput({
63
+ message: cause.message,
64
+ ...field === void 0 ? {} : { field }
65
+ }) })));
66
+ //#endregion
67
+ export { DomainKitError, decode, fail, isDomainKitError };
68
+
69
+ //# sourceMappingURL=error.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.mjs","names":["Reason.Model","Reason.InvalidInput"],"sources":["../../src/internal/error.ts"],"sourcesContent":["/**\n * The one error every DomainKit operation fails with, published as `DomainKit.Error`. Hosts match\n * on `reason`; `category`, `isRetryable`, and `httpStatus` derive from it. Modelled on `SqlError`\n * and `HttpClientError` in Effect.\n */\nimport { Effect, Schema } from \"effect\";\n\nimport * as Reason from \"../Reason.ts\";\n\nexport type Category = \"request\" | \"auth\" | \"plan\" | \"provider\" | \"storage\" | \"internal\";\n\nexport class DomainKitError extends Schema.TaggedError<DomainKitError>(\"@domainkit/DomainKitError\")(\n \"DomainKitError\",\n { reason: Schema.suspend(() => Reason.Model) },\n) {\n get category(): Category {\n switch (this.reason._tag) {\n case \"InvalidInput\":\n case \"NotFound\":\n return \"request\";\n case \"Unauthenticated\":\n case \"Forbidden\":\n case \"Reconnect\":\n return \"auth\";\n case \"Conflict\":\n case \"Stale\":\n case \"Expired\":\n case \"Busy\":\n return \"plan\";\n case \"ProviderRejected\":\n case \"ProviderUnavailable\":\n case \"ProviderConflict\":\n case \"Unsupported\":\n return \"provider\";\n case \"StorageFailed\":\n return \"storage\";\n case \"CryptoFailed\":\n case \"ResolverFailed\":\n return \"internal\";\n }\n }\n\n get isRetryable(): boolean {\n return (\n this.reason._tag === \"Busy\" ||\n this.reason._tag === \"ProviderUnavailable\" ||\n this.reason._tag === \"StorageFailed\"\n );\n }\n\n /** The HTTP status `domainkit/server` answers with; hosts mapping their own routes use the same table. */\n get httpStatus(): number {\n switch (this.reason._tag) {\n case \"InvalidInput\":\n return 400;\n case \"Unauthenticated\":\n return 401;\n case \"Forbidden\":\n case \"Reconnect\":\n return 403;\n case \"NotFound\":\n return 404;\n case \"Conflict\":\n case \"Stale\":\n case \"Expired\":\n case \"Busy\":\n return 409;\n case \"ProviderRejected\":\n return 502;\n case \"ProviderConflict\":\n return 409;\n case \"Unsupported\":\n return 501;\n case \"ProviderUnavailable\":\n return 503;\n case \"StorageFailed\":\n case \"CryptoFailed\":\n case \"ResolverFailed\":\n return 500;\n }\n }\n\n override get message(): string {\n return this.reason.message;\n }\n}\n\nexport const isDomainKitError = (input: unknown): input is DomainKitError =>\n input instanceof DomainKitError;\n\n/** `Effect.fail(new DomainKitError({ reason }))` shorthand used across the package. */\nexport const fail = <R extends Reason.Model>(reason: R): Effect.Effect<never, DomainKitError> =>\n Effect.fail(new DomainKitError({ reason }));\n\n/** Decode with any schema, failing with reason `InvalidInput` and the schema's message. */\nexport const decode = <S extends Schema.ConstraintDecoder<unknown>>(\n schema: S,\n input: unknown,\n field?: string,\n): Effect.Effect<S[\"Type\"], DomainKitError> =>\n Schema.decodeUnknownEffect(schema)(input).pipe(\n Effect.mapError(\n (cause) =>\n new DomainKitError({\n reason: new Reason.InvalidInput({\n message: cause.message,\n ...(field === undefined ? {} : { field }),\n }),\n }),\n ),\n );\n"],"mappings":";;;;;;;;AAWA,IAAa,iBAAb,cAAoC,OAAO,YAA4B,2BAA2B,CAAC,CACjG,kBACA,EAAE,QAAQ,OAAO,cAAcA,KAAY,EAAE,CAC/C,CAAC,CAAC;CACA,IAAI,WAAqB;EACvB,QAAQ,KAAK,OAAO,MAApB;GACE,KAAK;GACL,KAAK,YACH,OAAO;GACT,KAAK;GACL,KAAK;GACL,KAAK,aACH,OAAO;GACT,KAAK;GACL,KAAK;GACL,KAAK;GACL,KAAK,QACH,OAAO;GACT,KAAK;GACL,KAAK;GACL,KAAK;GACL,KAAK,eACH,OAAO;GACT,KAAK,iBACH,OAAO;GACT,KAAK;GACL,KAAK,kBACH,OAAO;EACX;CACF;CAEA,IAAI,cAAuB;EACzB,OACE,KAAK,OAAO,SAAS,UACrB,KAAK,OAAO,SAAS,yBACrB,KAAK,OAAO,SAAS;CAEzB;;CAGA,IAAI,aAAqB;EACvB,QAAQ,KAAK,OAAO,MAApB;GACE,KAAK,gBACH,OAAO;GACT,KAAK,mBACH,OAAO;GACT,KAAK;GACL,KAAK,aACH,OAAO;GACT,KAAK,YACH,OAAO;GACT,KAAK;GACL,KAAK;GACL,KAAK;GACL,KAAK,QACH,OAAO;GACT,KAAK,oBACH,OAAO;GACT,KAAK,oBACH,OAAO;GACT,KAAK,eACH,OAAO;GACT,KAAK,uBACH,OAAO;GACT,KAAK;GACL,KAAK;GACL,KAAK,kBACH,OAAO;EACX;CACF;CAEA,IAAa,UAAkB;EAC7B,OAAO,KAAK,OAAO;CACrB;AACF;AAEA,MAAa,oBAAoB,UAC/B,iBAAiB;;AAGnB,MAAa,QAAgC,WAC3C,OAAO,KAAK,IAAI,eAAe,EAAE,OAAO,CAAC,CAAC;;AAG5C,MAAa,UACX,QACA,OACA,UAEA,OAAO,oBAAoB,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,KACxC,OAAO,UACJ,UACC,IAAI,eAAe,EACjB,QAAQ,IAAIC,aAAoB;CAC9B,SAAS,MAAM;CACf,GAAI,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM;AACzC,CAAC,EACH,CAAC,CACL,CACF"}
@@ -0,0 +1,90 @@
1
+ import { Forbidden, ProviderConflict, ProviderRejected, ProviderUnavailable, Unauthenticated } from "../Reason.mjs";
2
+ import { DomainKitError, isDomainKitError } from "./error.mjs";
3
+ import { Effect } from "effect";
4
+ //#region src/internal/http.ts
5
+ /** One provider HTTP round trip: transport failures and non-JSON replies are already classified. */
6
+ const requestJson = (input) => Effect.tryPromise({
7
+ try: async (signal) => {
8
+ const { fetch: send } = input;
9
+ const response = await send(input.url, {
10
+ ...input.init,
11
+ signal
12
+ });
13
+ const text = await response.text();
14
+ let body = null;
15
+ if (text.length > 0) try {
16
+ body = JSON.parse(text);
17
+ } catch {
18
+ throw new DomainKitError({ reason: response.ok ? new ProviderRejected({
19
+ provider: input.provider,
20
+ message: `${input.provider} returned a non-JSON response`
21
+ }) : classify(input.provider, response.status, response.headers, `HTTP ${response.status}`) });
22
+ }
23
+ return {
24
+ status: response.status,
25
+ ok: response.ok,
26
+ headers: response.headers,
27
+ body
28
+ };
29
+ },
30
+ catch: (cause) => isDomainKitError(cause) ? cause : new DomainKitError({ reason: new ProviderUnavailable({
31
+ provider: input.provider,
32
+ message: cause instanceof Error ? cause.message : `${input.provider} request failed`
33
+ }) })
34
+ });
35
+ /** Host-supplied request headers, given as a value or as a thunk that may await a fresh token. */
36
+ const headersFrom = (source) => source === void 0 ? Effect.sync(() => new Headers()) : typeof source !== "function" ? Effect.sync(() => new Headers(source)) : Effect.tryPromise({
37
+ try: async () => new Headers(await source()),
38
+ catch: (cause) => isDomainKitError(cause) ? cause : new DomainKitError({ reason: new Unauthenticated({ message: cause instanceof Error ? cause.message : "Request headers were unavailable" }) })
39
+ });
40
+ /**
41
+ * Map an HTTP failure to a reason: 401 -> Unauthenticated, 403 -> Forbidden, 409 or a provider
42
+ * conflict code -> ProviderConflict, 429/5xx -> ProviderUnavailable, else ProviderRejected.
43
+ */
44
+ const classify = (provider, status, headers, message, code) => {
45
+ if (status === 401) return new Unauthenticated({ message });
46
+ if (status === 403) return new Forbidden({ message });
47
+ if (status === 409 || code === "conflict") return new ProviderConflict({
48
+ provider,
49
+ message,
50
+ code: code ?? String(status)
51
+ });
52
+ if (status === 429 || status >= 500) {
53
+ const retryAfterMs = retryAfter(headers.get("retry-after"));
54
+ return new ProviderUnavailable({
55
+ provider,
56
+ message,
57
+ ...retryAfterMs === void 0 ? {} : { retryAfterMs }
58
+ });
59
+ }
60
+ return new ProviderRejected({
61
+ provider,
62
+ message,
63
+ code: code ?? String(status)
64
+ });
65
+ };
66
+ const rejected = (provider, message, code) => new DomainKitError({ reason: new ProviderRejected({
67
+ provider,
68
+ message,
69
+ ...code === void 0 ? {} : { code }
70
+ }) });
71
+ function retryAfter(value) {
72
+ if (value === null) return void 0;
73
+ const seconds = Number(value);
74
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
75
+ const date = Date.parse(value);
76
+ return Number.isNaN(date) ? void 0 : Math.max(0, date - Date.now());
77
+ }
78
+ const bearer = (token, init) => ({
79
+ ...init,
80
+ headers: {
81
+ Accept: "application/json",
82
+ Authorization: `Bearer ${token}`,
83
+ ...init?.body === void 0 ? {} : { "Content-Type": "application/json" },
84
+ ...init?.headers
85
+ }
86
+ });
87
+ //#endregion
88
+ export { bearer, classify, headersFrom, rejected, requestJson };
89
+
90
+ //# sourceMappingURL=http.mjs.map
@@ -0,0 +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 { 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\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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"}
@@ -0,0 +1,8 @@
1
+ import { Effect } from "effect";
2
+ //#region src/internal/ids.ts
3
+ /** A fresh, prefixed, URL-safe identifier (`prefix_<uuid>`). */
4
+ const fresh = (prefix) => Effect.sync(() => `${prefix}_${crypto.randomUUID()}`);
5
+ //#endregion
6
+ export { fresh };
7
+
8
+ //# sourceMappingURL=ids.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ids.mjs","names":[],"sources":["../../src/internal/ids.ts"],"sourcesContent":["import { Effect } from \"effect\";\n\n/** A fresh, prefixed, URL-safe identifier (`prefix_<uuid>`). */\nexport const fresh = (prefix: string): Effect.Effect<string> =>\n Effect.sync(() => `${prefix}_${crypto.randomUUID()}`);\n"],"mappings":";;;AAGA,MAAa,SAAS,WACpB,OAAO,WAAW,GAAG,OAAO,GAAG,OAAO,WAAW,GAAG"}
@@ -0,0 +1,113 @@
1
+ import { CryptoFailed, ProviderRejected, ProviderUnavailable, Unauthenticated } from "../Reason.mjs";
2
+ import { DomainKitError } from "./error.mjs";
3
+ import { DateTime, Effect, Redacted } from "effect";
4
+ import * as oauth from "oauth4webapi";
5
+ //#region src/internal/oauth.ts
6
+ /** A PKCE pair: the verifier stays in the continuation, the challenge goes to the provider. */
7
+ const pkce = () => Effect.tryPromise({
8
+ try: async () => {
9
+ const codeVerifier = oauth.generateRandomCodeVerifier();
10
+ return {
11
+ codeVerifier,
12
+ codeChallenge: await oauth.calculatePKCECodeChallenge(codeVerifier)
13
+ };
14
+ },
15
+ catch: () => new DomainKitError({ reason: new CryptoFailed({ operation: "digest" }) })
16
+ });
17
+ const authorizationUrl = (input) => {
18
+ const url = new URL(input.server.authorization_endpoint);
19
+ url.search = new URLSearchParams({
20
+ client_id: input.clientId,
21
+ code_challenge: input.codeChallenge,
22
+ code_challenge_method: "S256",
23
+ redirect_uri: input.callbackUrl,
24
+ response_type: "code",
25
+ scope: input.scopes.join(" "),
26
+ state: input.state
27
+ }).toString();
28
+ return url.toString();
29
+ };
30
+ const clientAuth = (client) => {
31
+ if (client.clientAuth === "none") return oauth.None();
32
+ const secret = client.clientSecret === null ? "" : Redacted.value(client.clientSecret);
33
+ return client.clientAuth === "client_secret_basic" ? oauth.ClientSecretBasic(secret) : oauth.ClientSecretPost(secret);
34
+ };
35
+ const requestOptions = (fetch, signal) => fetch === void 0 ? { signal } : {
36
+ signal,
37
+ [oauth.customFetch]: (url, init) => fetch(url, {
38
+ ...init,
39
+ body: init.body
40
+ })
41
+ };
42
+ const tokens = (response, now) => ({
43
+ accessToken: response.access_token,
44
+ refreshToken: response.refresh_token ?? null,
45
+ expiresAt: typeof response.expires_in === "number" ? DateTime.add(now, { seconds: response.expires_in }) : null,
46
+ scope: response.scope ?? null
47
+ });
48
+ const failure = (provider, cause) => {
49
+ if (cause instanceof oauth.ResponseBodyError) {
50
+ const terminal = [
51
+ "invalid_grant",
52
+ "invalid_client",
53
+ "unauthorized_client"
54
+ ].includes(cause.error);
55
+ return new DomainKitError({ reason: terminal ? new Unauthenticated({ message: `${provider} rejected the grant: ${cause.error}` }) : new ProviderRejected({
56
+ provider,
57
+ code: cause.error,
58
+ message: cause.error_description ?? cause.error
59
+ }) });
60
+ }
61
+ if (cause instanceof oauth.AuthorizationResponseError) return new DomainKitError({ reason: new Unauthenticated({ message: `${provider} authorization failed: ${cause.error}` }) });
62
+ return new DomainKitError({ reason: new ProviderUnavailable({
63
+ provider,
64
+ message: cause instanceof Error ? cause.message : `${provider} OAuth request failed`
65
+ }) });
66
+ };
67
+ const exchangeCode = (input) => Effect.gen(function* () {
68
+ const now = yield* DateTime.now;
69
+ const server = input.server;
70
+ const client = { client_id: input.client.clientId };
71
+ const response = yield* Effect.tryPromise({
72
+ try: async (signal) => {
73
+ const params = oauth.validateAuthResponse(server, client, new URLSearchParams({
74
+ code: input.code,
75
+ state: input.state
76
+ }), input.state);
77
+ const reply = await oauth.authorizationCodeGrantRequest(server, client, clientAuth(input.client), params, input.callbackUrl, input.codeVerifier, requestOptions(input.fetch, signal));
78
+ return oauth.processAuthorizationCodeResponse(server, client, reply);
79
+ },
80
+ catch: (cause) => failure(input.provider, cause)
81
+ });
82
+ return tokens(response, now);
83
+ });
84
+ const refresh = (input) => Effect.gen(function* () {
85
+ const now = yield* DateTime.now;
86
+ const server = input.server;
87
+ const client = { client_id: input.client.clientId };
88
+ const response = yield* Effect.tryPromise({
89
+ try: async (signal) => {
90
+ const reply = await oauth.refreshTokenGrantRequest(server, client, clientAuth(input.client), input.refreshToken, requestOptions(input.fetch, signal));
91
+ return oauth.processRefreshTokenResponse(server, client, reply);
92
+ },
93
+ catch: (cause) => failure(input.provider, cause)
94
+ });
95
+ const next = tokens(response, now);
96
+ return {
97
+ ...next,
98
+ refreshToken: next.refreshToken ?? input.refreshToken
99
+ };
100
+ });
101
+ const revoke = (input) => Effect.tryPromise({
102
+ try: async (signal) => {
103
+ const server = input.server;
104
+ const client = { client_id: input.client.clientId };
105
+ const reply = await oauth.revocationRequest(server, client, clientAuth(input.client), input.token, requestOptions(input.fetch, signal));
106
+ await oauth.processRevocationResponse(reply);
107
+ },
108
+ catch: (cause) => failure(input.provider, cause)
109
+ });
110
+ //#endregion
111
+ export { authorizationUrl, exchangeCode, pkce, refresh, revoke };
112
+
113
+ //# sourceMappingURL=oauth.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.mjs","names":["Errors.DomainKitError","Reason.CryptoFailed","Reason.Unauthenticated","Reason.ProviderRejected","Reason.ProviderUnavailable"],"sources":["../../src/internal/oauth.ts"],"sourcesContent":["import { DateTime, Effect, Redacted } from \"effect\";\nimport * as oauth from \"oauth4webapi\";\n\nimport * as Errors from \"./error.ts\";\nimport * as Reason from \"../Reason.ts\";\nimport type { Fetch } from \"./http.ts\";\n\nexport interface Server {\n readonly issuer: string;\n readonly authorization_endpoint: string;\n readonly token_endpoint: string;\n readonly revocation_endpoint?: string;\n}\n\nexport type ClientAuth = \"client_secret_basic\" | \"client_secret_post\" | \"none\";\n\nexport interface Client {\n readonly clientId: string;\n readonly clientSecret: Redacted.Redacted<string> | null;\n readonly clientAuth: ClientAuth;\n}\n\n/** A PKCE pair: the verifier stays in the continuation, the challenge goes to the provider. */\nexport const pkce = (): Effect.Effect<\n { readonly codeVerifier: string; readonly codeChallenge: string },\n Errors.DomainKitError\n> =>\n Effect.tryPromise({\n try: async () => {\n const codeVerifier = oauth.generateRandomCodeVerifier();\n return { codeVerifier, codeChallenge: await oauth.calculatePKCECodeChallenge(codeVerifier) };\n },\n catch: () =>\n new Errors.DomainKitError({\n reason: new Reason.CryptoFailed({ operation: \"digest\" }),\n }),\n });\n\nexport const authorizationUrl = (input: {\n readonly server: Server;\n readonly clientId: string;\n readonly scopes: ReadonlyArray<string>;\n readonly state: string;\n readonly callbackUrl: string;\n readonly codeChallenge: string;\n}): string => {\n const url = new URL(input.server.authorization_endpoint);\n url.search = new URLSearchParams({\n client_id: input.clientId,\n code_challenge: input.codeChallenge,\n code_challenge_method: \"S256\",\n redirect_uri: input.callbackUrl,\n response_type: \"code\",\n scope: input.scopes.join(\" \"),\n state: input.state,\n }).toString();\n return url.toString();\n};\n\nexport interface Tokens {\n readonly accessToken: string;\n readonly refreshToken: string | null;\n readonly expiresAt: DateTime.Utc | null;\n readonly scope: string | null;\n}\n\nconst clientAuth = (client: Client): oauth.ClientAuth => {\n if (client.clientAuth === \"none\") return oauth.None();\n const secret = client.clientSecret === null ? \"\" : Redacted.value(client.clientSecret);\n return client.clientAuth === \"client_secret_basic\"\n ? oauth.ClientSecretBasic(secret)\n : oauth.ClientSecretPost(secret);\n};\n\nconst requestOptions = (\n fetch: Fetch | undefined,\n signal: AbortSignal,\n): oauth.TokenEndpointRequestOptions =>\n fetch === undefined\n ? { signal }\n : { signal, [oauth.customFetch]: (url, init) => fetch(url, { ...init, body: init.body }) };\n\nconst tokens = (response: oauth.TokenEndpointResponse, now: DateTime.Utc): Tokens => ({\n accessToken: response.access_token,\n refreshToken: response.refresh_token ?? null,\n expiresAt:\n typeof response.expires_in === \"number\"\n ? DateTime.add(now, { seconds: response.expires_in })\n : null,\n scope: response.scope ?? null,\n});\n\nconst failure = (provider: string, cause: unknown): Errors.DomainKitError => {\n if (cause instanceof oauth.ResponseBodyError) {\n const terminal = [\"invalid_grant\", \"invalid_client\", \"unauthorized_client\"].includes(\n cause.error,\n );\n return new Errors.DomainKitError({\n reason: terminal\n ? new Reason.Unauthenticated({\n message: `${provider} rejected the grant: ${cause.error}`,\n })\n : new Reason.ProviderRejected({\n provider,\n code: cause.error,\n message: cause.error_description ?? cause.error,\n }),\n });\n }\n if (cause instanceof oauth.AuthorizationResponseError) {\n return new Errors.DomainKitError({\n reason: new Reason.Unauthenticated({\n message: `${provider} authorization failed: ${cause.error}`,\n }),\n });\n }\n return new Errors.DomainKitError({\n reason: new Reason.ProviderUnavailable({\n provider,\n message: cause instanceof Error ? cause.message : `${provider} OAuth request failed`,\n }),\n });\n};\n\nexport const exchangeCode = (input: {\n readonly provider: string;\n readonly server: Server;\n readonly client: Client;\n readonly code: string;\n readonly state: string;\n readonly callbackUrl: string;\n readonly codeVerifier: string;\n readonly fetch?: Fetch;\n}): Effect.Effect<Tokens, Errors.DomainKitError> =>\n Effect.gen(function* () {\n const now = yield* DateTime.now;\n const server = input.server as oauth.AuthorizationServer;\n const client: oauth.Client = { client_id: input.client.clientId };\n const response = yield* Effect.tryPromise({\n try: async (signal) => {\n const params = oauth.validateAuthResponse(\n server,\n client,\n new URLSearchParams({ code: input.code, state: input.state }),\n input.state,\n );\n const reply = await oauth.authorizationCodeGrantRequest(\n server,\n client,\n clientAuth(input.client),\n params,\n input.callbackUrl,\n input.codeVerifier,\n requestOptions(input.fetch, signal),\n );\n return oauth.processAuthorizationCodeResponse(server, client, reply);\n },\n catch: (cause) => failure(input.provider, cause),\n });\n return tokens(response, now);\n });\n\nexport const refresh = (input: {\n readonly provider: string;\n readonly server: Server;\n readonly client: Client;\n readonly refreshToken: string;\n readonly fetch?: Fetch;\n}): Effect.Effect<Tokens, Errors.DomainKitError> =>\n Effect.gen(function* () {\n const now = yield* DateTime.now;\n const server = input.server as oauth.AuthorizationServer;\n const client: oauth.Client = { client_id: input.client.clientId };\n const response = yield* Effect.tryPromise({\n try: async (signal) => {\n const reply = await oauth.refreshTokenGrantRequest(\n server,\n client,\n clientAuth(input.client),\n input.refreshToken,\n requestOptions(input.fetch, signal),\n );\n return oauth.processRefreshTokenResponse(server, client, reply);\n },\n catch: (cause) => failure(input.provider, cause),\n });\n const next = tokens(response, now);\n return { ...next, refreshToken: next.refreshToken ?? input.refreshToken };\n });\n\nexport const revoke = (input: {\n readonly provider: string;\n readonly server: Server;\n readonly client: Client;\n readonly token: string;\n readonly fetch?: Fetch;\n}): Effect.Effect<void, Errors.DomainKitError> =>\n Effect.tryPromise({\n try: async (signal) => {\n const server = input.server as oauth.AuthorizationServer;\n const client: oauth.Client = { client_id: input.client.clientId };\n const reply = await oauth.revocationRequest(\n server,\n client,\n clientAuth(input.client),\n input.token,\n requestOptions(input.fetch, signal),\n );\n await oauth.processRevocationResponse(reply);\n },\n catch: (cause) => failure(input.provider, cause),\n });\n"],"mappings":"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"}
@@ -0,0 +1,88 @@
1
+ import { Model, Observed, equals, sameSet } from "../DnsRecord.mjs";
2
+ import { Conflict, Create, Digest, Kind, Noop, Operation, OperationId, Version } from "../Plan.mjs";
3
+ import { CryptoFailed } from "../Reason.mjs";
4
+ import { DomainKitError } from "./error.mjs";
5
+ import { sha256Hex, stringify } from "./digest.mjs";
6
+ import { Effect, Schema } from "effect";
7
+ //#region src/internal/planner.ts
8
+ const Unsigned = Schema.Struct({
9
+ version: Version,
10
+ kind: Kind,
11
+ domain: Schema.String,
12
+ zone: Schema.String,
13
+ provider: Schema.String,
14
+ attachmentId: Schema.String,
15
+ operations: Schema.Array(Operation)
16
+ });
17
+ const operationId = (record) => sha256Hex(stringify({ record: strip(record) })).pipe(Effect.map(OperationId.make));
18
+ /** The digest covers every operation but not requirement labels, which are display metadata. */
19
+ const digest = (unsigned) => Effect.try({
20
+ try: () => {
21
+ const encoded = Schema.encodeSync(Unsigned)(unsigned);
22
+ return stringify({
23
+ ...encoded,
24
+ operations: encoded.operations.map((operation) => ({
25
+ ...operation,
26
+ record: withoutPurpose(operation.record)
27
+ }))
28
+ });
29
+ },
30
+ catch: () => new DomainKitError({ reason: new CryptoFailed({ operation: "digest" }) })
31
+ }).pipe(Effect.flatMap(sha256Hex), Effect.map(Digest.make));
32
+ const withoutPurpose = (record) => {
33
+ const { purpose: _purpose, ...rest } = record;
34
+ return rest;
35
+ };
36
+ /** Additive, fail-closed reconciliation of requirements against what the provider holds. */
37
+ const reconcile = (requirements, existing) => Effect.gen(function* () {
38
+ const sorted = [...requirements].sort(compare);
39
+ const projected = [...existing].sort(compare);
40
+ const operations = [];
41
+ for (const record of sorted) {
42
+ const id = yield* operationId(record);
43
+ const operation = reconcileOne(id, record, projected);
44
+ operations.push(operation);
45
+ if (operation._tag === "Create") projected.push(record);
46
+ }
47
+ return operations;
48
+ });
49
+ const reconcileOne = (id, record, existing) => {
50
+ const sameName = existing.filter((candidate) => candidate.name === record.name);
51
+ const exact = sameName.find((candidate) => equals(candidate, record));
52
+ if (exact !== void 0) return new Noop({
53
+ id,
54
+ record,
55
+ existing: exact,
56
+ ttlDrift: exact._tag !== "Opaque" && exact.ttl !== record.ttl
57
+ });
58
+ const conflict = (reason, culprits) => new Conflict({
59
+ id,
60
+ record,
61
+ existing: culprits,
62
+ reason
63
+ });
64
+ if (sameName.length > 0 && (record._tag === "CNAME" || sameName.some((candidate) => candidate._tag === "CNAME"))) return conflict("cname-collision", sameName);
65
+ const sameSet$1 = sameName.filter((candidate) => sameSet(candidate, record));
66
+ if (sameSet$1.some((candidate) => candidate._tag === "Opaque")) return conflict("opaque", sameSet$1);
67
+ if (record.policy === "exclusive" && sameSet$1.length > 0) return conflict("exclusive-name", sameSet$1);
68
+ return new Create({
69
+ id,
70
+ record
71
+ });
72
+ };
73
+ /** Requirements a plan was built from, for re-planning at apply time. */
74
+ const requirements = (plan) => plan.operations.map((operation) => operation.record);
75
+ /** Operation ids hash the encoded requirement without its label, so relabeling never invalidates a plan. */
76
+ function strip(record) {
77
+ return withoutPurpose(Schema.encodeSync(Model)(record));
78
+ }
79
+ function compare(left, right) {
80
+ const encode = Schema.encodeSync(Observed);
81
+ const leftKey = stringify(encode(left));
82
+ const rightKey = stringify(encode(right));
83
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
84
+ }
85
+ //#endregion
86
+ export { digest, operationId, reconcile, requirements };
87
+
88
+ //# sourceMappingURL=planner.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"planner.mjs","names":["Plan.Version","Plan.Kind","Plan.Operation","Errors.DomainKitError","Reason.CryptoFailed","DnsRecord.equals","Plan.Noop","Plan.Conflict","sameSet","DnsRecord.sameSet","Plan.Create","DnsRecord.Model","DnsRecord.Observed"],"sources":["../../src/internal/planner.ts"],"sourcesContent":["import { Effect, Schema } from \"effect\";\n\nimport * as DnsRecord from \"../DnsRecord.ts\";\nimport * as Errors from \"./error.ts\";\nimport * as Reason from \"../Reason.ts\";\nimport * as Plan from \"../Plan.ts\";\nimport { sha256Hex, stringify } from \"./digest.ts\";\n\nconst Unsigned = Schema.Struct({\n version: Plan.Version,\n kind: Plan.Kind,\n domain: Schema.String,\n zone: Schema.String,\n provider: Schema.String,\n attachmentId: Schema.String,\n operations: Schema.Array(Plan.Operation),\n});\nexport type Unsigned = typeof Unsigned.Type;\n\nexport const operationId = (\n record: DnsRecord.Model,\n): Effect.Effect<Plan.OperationId, Errors.DomainKitError> =>\n sha256Hex(stringify({ record: strip(record) })).pipe(Effect.map(Plan.OperationId.make));\n\n/** The digest covers every operation but not requirement labels, which are display metadata. */\nexport const digest = (unsigned: Unsigned): Effect.Effect<Plan.Digest, Errors.DomainKitError> =>\n Effect.try({\n try: () => {\n const encoded = Schema.encodeSync(Unsigned)(unsigned);\n return stringify({\n ...encoded,\n operations: encoded.operations.map((operation) => ({\n ...operation,\n record: withoutPurpose(operation.record),\n })),\n });\n },\n catch: () =>\n new Errors.DomainKitError({\n reason: new Reason.CryptoFailed({ operation: \"digest\" }),\n }),\n }).pipe(Effect.flatMap(sha256Hex), Effect.map(Plan.Digest.make));\n\nconst withoutPurpose = (record: DnsRecord.Encoded): DnsRecord.Encoded => {\n const { purpose: _purpose, ...rest } = record;\n return rest;\n};\n\n/** Additive, fail-closed reconciliation of requirements against what the provider holds. */\nexport const reconcile = (\n requirements: ReadonlyArray<DnsRecord.Model>,\n existing: ReadonlyArray<DnsRecord.Observed>,\n): Effect.Effect<ReadonlyArray<Plan.Create | Plan.Noop | Plan.Conflict>, Errors.DomainKitError> =>\n Effect.gen(function* () {\n const sorted = [...requirements].sort(compare);\n const projected: Array<DnsRecord.Observed> = [...existing].sort(compare);\n const operations: Array<Plan.Create | Plan.Noop | Plan.Conflict> = [];\n for (const record of sorted) {\n const id = yield* operationId(record);\n const operation = reconcileOne(id, record, projected);\n operations.push(operation);\n if (operation._tag === \"Create\") projected.push(record);\n }\n return operations;\n });\n\nconst reconcileOne = (\n id: Plan.OperationId,\n record: DnsRecord.Model,\n existing: ReadonlyArray<DnsRecord.Observed>,\n): Plan.Create | Plan.Noop | Plan.Conflict => {\n const sameName = existing.filter((candidate) => candidate.name === record.name);\n const exact = sameName.find((candidate) => DnsRecord.equals(candidate, record));\n if (exact !== undefined) {\n return new Plan.Noop({\n id,\n record,\n existing: exact,\n ttlDrift: exact._tag !== \"Opaque\" && exact.ttl !== record.ttl,\n });\n }\n const conflict = (reason: Plan.Conflict[\"reason\"], culprits: ReadonlyArray<DnsRecord.Observed>) =>\n new Plan.Conflict({ id, record, existing: culprits, reason });\n if (\n sameName.length > 0 &&\n (record._tag === \"CNAME\" || sameName.some((candidate) => candidate._tag === \"CNAME\"))\n ) {\n return conflict(\"cname-collision\", sameName);\n }\n const sameSet = sameName.filter((candidate) => DnsRecord.sameSet(candidate, record));\n if (sameSet.some((candidate) => candidate._tag === \"Opaque\")) return conflict(\"opaque\", sameSet);\n if (record.policy === \"exclusive\" && sameSet.length > 0)\n return conflict(\"exclusive-name\", sameSet);\n return new Plan.Create({ id, record });\n};\n\n/** Requirements a plan was built from, for re-planning at apply time. */\nexport const requirements = (plan: Plan.Model): ReadonlyArray<DnsRecord.Model> =>\n plan.operations.map((operation) => operation.record);\n\n/** Operation ids hash the encoded requirement without its label, so relabeling never invalidates a plan. */\nfunction strip(record: DnsRecord.Model): DnsRecord.Encoded {\n return withoutPurpose(Schema.encodeSync(DnsRecord.Model)(record));\n}\n\nfunction compare(left: DnsRecord.Observed, right: DnsRecord.Observed): number {\n const encode = Schema.encodeSync(DnsRecord.Observed);\n const leftKey = stringify(encode(left));\n const rightKey = stringify(encode(right));\n return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;\n}\n"],"mappings":"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"}