domainkit 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/Cloudflare.mjs +36 -12
- package/dist/Cloudflare.mjs.map +1 -1
- package/dist/Connect.mjs +19 -2
- package/dist/Connect.mjs.map +1 -1
- package/dist/Provider.mjs +12 -13
- package/dist/Provider.mjs.map +1 -1
- package/dist/Server.mjs +12 -2
- package/dist/Server.mjs.map +1 -1
- package/dist/Testing.mjs +26 -24
- package/dist/Testing.mjs.map +1 -1
- package/dist/Vercel.mjs +3 -1
- package/dist/Vercel.mjs.map +1 -1
- package/dist/internal/cloudflare/client.mjs +10 -5
- package/dist/internal/cloudflare/client.mjs.map +1 -1
- package/dist/internal/conformance/provider.mjs +31 -18
- package/dist/internal/conformance/provider.mjs.map +1 -1
- package/dist/internal/http.mjs +7 -2
- package/dist/internal/http.mjs.map +1 -1
- package/dist/internal/oauth.mjs +35 -7
- package/dist/internal/oauth.mjs.map +1 -1
- package/dist/package.mjs +1 -1
- package/dist/types/Cloudflare.d.ts +21 -0
- package/dist/types/Connect.d.ts +30 -0
- package/dist/types/Provider.d.ts +11 -0
- package/dist/types/Server.d.ts +10 -0
- package/dist/types/Testing.d.ts +13 -4
- package/dist/types/Vercel.d.ts +1 -1
- package/dist/types/internal/cloudflare/client.d.ts +4 -2
- package/dist/types/internal/conformance/provider.d.ts +10 -3
- package/dist/types/internal/http.d.ts +5 -0
- package/dist/types/internal/oauth.d.ts +4 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.mjs","names":["Reason.NotFound","Errors.fail","S","Protocol.BaseEnvelope","Reason.ProviderConflict","Errors.decode","Protocol.Envelope","Protocol.Zone","Protocol.Token","Reason.Unauthenticated","Protocol.Record","Records.isWithinZone","Records.decode","Records.encode"],"sources":["../../../src/internal/cloudflare/client.ts"],"sourcesContent":["import { DateTime, Effect, Schema as S } from \"effect\";\n\nimport type * as DnsRecord from \"../../DnsRecord.ts\";\nimport * as Errors from \"../error.ts\";\nimport * as Reason from \"../../Reason.ts\";\nimport type * as Provider from \"../../Provider.ts\";\nimport { bearer, classify, type Fetch, rejected, requestJson } from \"../http.ts\";\nimport * as Protocol from \"./protocol.ts\";\nimport * as Records from \"./records.ts\";\n\nexport const provider = \"cloudflare\";\n\nconst notFound = new Reason.NotFound({ entity: \"zone\", id: \"cloudflare\" });\n\n/** Cloudflare's \"record already exists\" family (81056 identical, 81057 same name and content, 81058 CNAME). */\nconst conflictCodes = new Set([81056, 81057, 81058]);\n\nexport interface Options {\n readonly token: string;\n readonly fetch: Fetch;\n readonly baseUrl: string;\n}\n\ntype Fx<A> = Effect.Effect<A, Errors.DomainKitError>;\n\nconst call = <R extends S.ConstraintDecoder<unknown>>(\n options: Options,\n path: string,\n result: R,\n init?: RequestInit,\n): Fx<{ readonly result: R[\"Type\"]; readonly totalPages: number | undefined }> =>\n Effect.gen(function* () {\n const reply = yield* requestJson({\n fetch: options.fetch,\n provider,\n url: `${options.baseUrl}${path}`,\n init: bearer(options.token, init),\n });\n if (reply.status === 404) return yield* Errors.fail(notFound);\n const base = S.decodeUnknownOption(Protocol.BaseEnvelope)(reply.body);\n if (!reply.ok || base._tag === \"None\" || !base.value.success) {\n const detail = base._tag === \"Some\" ? base.value.errors[0] : undefined;\n if (detail !== undefined && conflictCodes.has(detail.code)) {\n return yield* Errors.fail(\n new Reason.ProviderConflict({\n provider,\n code: String(detail.code),\n message: detail.message,\n }),\n );\n }\n return yield* Errors.fail(\n classify(\n provider,\n reply.status,\n reply.headers,\n detail?.message ?? `Cloudflare request failed with HTTP ${reply.status}`,\n detail === undefined ? undefined : String(detail.code),\n ),\n );\n }\n const contract = () =>\n rejected(provider, \"Cloudflare response did not match its API contract\", \"response\");\n const envelope = yield* Errors.decode(Protocol.Envelope, reply.body).pipe(\n Effect.mapError(contract),\n );\n const decoded: R[\"Type\"] = yield* Errors.decode(result, envelope.result).pipe(\n Effect.mapError(contract),\n );\n return { result: decoded, totalPages: envelope.result_info?.total_pages };\n });\n\nconst paginate = <R extends S.ConstraintDecoder<unknown>>(\n options: Options,\n path: (page: number) => string,\n result: R,\n perPage: number,\n): Fx<ReadonlyArray<R[\"Type\"]>> =>\n Effect.gen(function* () {\n const rows: Array<R[\"Type\"]> = [];\n let page = 1;\n while (true) {\n const reply = yield* call(options, path(page), S.Array(result));\n rows.push(...reply.result);\n const last =\n reply.totalPages === undefined ? reply.result.length < perPage : page >= reply.totalPages;\n if (last) return rows;\n page += 1;\n }\n });\n\n/** Every zone the credential can see, across accounts; internal zones cannot host records. */\nexport const listZones = (options: Options, accountId?: string): Fx<ReadonlyArray<Protocol.Zone>> =>\n paginate(\n options,\n (page) => {\n const query = new URLSearchParams({ page: String(page), per_page: \"50\" });\n if (accountId !== undefined) query.set(\"account.id\", accountId);\n return `/zones?${query}`;\n },\n Protocol.Zone,\n 50,\n ).pipe(Effect.map((zones) => zones.filter((zone) => zone.type !== \"internal\")));\n\n/** Token expiry from the verify endpoint; `null` when the token never expires or verification is unavailable. */\n/** Token status and expiry from a verify endpoint; an inactive token is `Unauthenticated`. */\nexport const verifyToken = (options: Options, path: string): Fx<DateTime.Utc | null> =>\n call(options, path, Protocol.Token).pipe(\n Effect.flatMap(({ result }) =>\n result.status === \"active\"\n ? Effect.succeed(\n typeof result.expires_on === \"string\" ? DateTime.makeUnsafe(result.expires_on) : null,\n )\n : Errors.fail(\n new Reason.Unauthenticated({\n message: `Cloudflare token is ${result.status}`,\n }),\n ),\n ),\n );\n\nexport const tokenExpiry = (\n options: Options,\n accountId: string | null,\n): Fx<{ readonly expiresAt: DateTime.Utc | null; readonly kind: \"user\" | \"account\" }> => {\n const verify = (path: string) =>\n call(options, path, Protocol.Token).pipe(\n Effect.flatMap(({ result }) =>\n result.status === \"active\"\n ? Effect.succeed(\n typeof result.expires_on === \"string\" ? DateTime.makeUnsafe(result.expires_on) : null,\n )\n : Errors.fail(\n new Reason.Unauthenticated({\n message: `Cloudflare token is ${result.status}`,\n }),\n ),\n ),\n );\n return verify(\"/user/tokens/verify\").pipe(\n Effect.map((expiresAt) => ({ expiresAt, kind: \"user\" as const })),\n Effect.catchIf(\n (error) => error.reason._tag !== \"Unauthenticated\" || accountId !== null,\n () =>\n accountId === null\n ? Effect.succeed({ expiresAt: null, kind: \"user\" as const })\n : verify(`/accounts/${encodeURIComponent(accountId)}/tokens/verify`).pipe(\n Effect.map((expiresAt) => ({ expiresAt, kind: \"account\" as const })),\n Effect.catchIf(\n (error) => error.reason._tag !== \"Unauthenticated\",\n () => Effect.succeed({ expiresAt: null, kind: \"user\" as const }),\n ),\n ),\n ),\n );\n};\n\nexport const dns = (options: Options, zoneId: string): Provider.Dns => {\n const base = `/zones/${encodeURIComponent(zoneId)}/dns_records`;\n return {\n list: (zone) =>\n paginate(\n options,\n (page) => `${base}?${new URLSearchParams({ page: String(page), per_page: \"100\" })}`,\n Protocol.Record,\n 100,\n ).pipe(\n Effect.flatMap((records) =>\n Effect.forEach(\n records.filter((record) => Records.isWithinZone(record.name, zone)),\n Records.decode,\n ),\n ),\n ),\n create: (_zone, record: DnsRecord.Model) =>\n call(options, base, Protocol.Record, {\n method: \"POST\",\n body: JSON.stringify(Records.encode(record)),\n }).pipe(Effect.map(({ result }) => ({ providerRecordId: result.id }))),\n get: (_zone, providerRecordId) =>\n call(options, `${base}/${encodeURIComponent(providerRecordId)}`, Protocol.Record).pipe(\n Effect.flatMap(({ result }) => Records.decode(result)),\n Effect.map(({ record }) => record),\n Effect.catchIf(\n (error) => error.reason === notFound,\n () => Effect.succeed(null),\n ),\n ),\n delete: (_zone, providerRecordId) =>\n call(options, `${base}/${encodeURIComponent(providerRecordId)}`, S.Unknown, {\n method: \"DELETE\",\n }).pipe(Effect.asVoid),\n };\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.mjs","names":["Reason.NotFound","Errors.fail","S","Protocol.BaseEnvelope","Reason.Unauthenticated","Reason.ProviderConflict","Errors.decode","Protocol.Envelope","Protocol.Zone","Protocol.Token","Protocol.Record","Records.isWithinZone","Records.decode","Records.encode"],"sources":["../../../src/internal/cloudflare/client.ts"],"sourcesContent":["import { DateTime, Effect, Schema as S } from \"effect\";\n\nimport type * as DnsRecord from \"../../DnsRecord.ts\";\nimport * as Errors from \"../error.ts\";\nimport * as Reason from \"../../Reason.ts\";\nimport type * as Provider from \"../../Provider.ts\";\nimport { bearer, classify, type Fetch, rejected, rejectedToken, requestJson } from \"../http.ts\";\nimport * as Protocol from \"./protocol.ts\";\nimport * as Records from \"./records.ts\";\n\nexport const provider = \"cloudflare\";\n\nconst notFound = new Reason.NotFound({ entity: \"zone\", id: \"cloudflare\" });\n\n/** Cloudflare's \"record already exists\" family (81056 identical, 81057 same name and content, 81058 CNAME). */\nconst conflictCodes = new Set([81056, 81057, 81058]);\n\n/** Cloudflare answers a malformed bearer token with HTTP 400: 6003 invalid request headers, 6111 invalid Authorization format. */\nconst rejectedTokenCodes = new Set([6003, 6111]);\n\nexport interface Options {\n readonly token: string;\n readonly fetch: Fetch;\n readonly baseUrl: string;\n}\n\ntype Fx<A> = Effect.Effect<A, Errors.DomainKitError>;\n\nconst call = <R extends S.ConstraintDecoder<unknown>>(\n options: Options,\n path: string,\n result: R,\n init?: RequestInit,\n): Fx<{ readonly result: R[\"Type\"]; readonly totalPages: number | undefined }> =>\n Effect.gen(function* () {\n const reply = yield* requestJson({\n fetch: options.fetch,\n provider,\n url: `${options.baseUrl}${path}`,\n init: bearer(options.token, init),\n });\n if (reply.status === 404) return yield* Errors.fail(notFound);\n const base = S.decodeUnknownOption(Protocol.BaseEnvelope)(reply.body);\n if (!reply.ok || base._tag === \"None\" || !base.value.success) {\n const detail = base._tag === \"Some\" ? base.value.errors[0] : undefined;\n if (detail !== undefined && rejectedTokenCodes.has(detail.code)) {\n return yield* Errors.fail(new Reason.Unauthenticated({ message: detail.message }));\n }\n if (detail !== undefined && conflictCodes.has(detail.code)) {\n return yield* Errors.fail(\n new Reason.ProviderConflict({\n provider,\n code: String(detail.code),\n message: detail.message,\n }),\n );\n }\n return yield* Errors.fail(\n classify(\n provider,\n reply.status,\n reply.headers,\n detail?.message ?? `Cloudflare request failed with HTTP ${reply.status}`,\n detail === undefined ? undefined : String(detail.code),\n ),\n );\n }\n const contract = () =>\n rejected(provider, \"Cloudflare response did not match its API contract\", \"response\");\n const envelope = yield* Errors.decode(Protocol.Envelope, reply.body).pipe(\n Effect.mapError(contract),\n );\n const decoded: R[\"Type\"] = yield* Errors.decode(result, envelope.result).pipe(\n Effect.mapError(contract),\n );\n return { result: decoded, totalPages: envelope.result_info?.total_pages };\n });\n\nconst paginate = <R extends S.ConstraintDecoder<unknown>>(\n options: Options,\n path: (page: number) => string,\n result: R,\n perPage: number,\n): Fx<ReadonlyArray<R[\"Type\"]>> =>\n Effect.gen(function* () {\n const rows: Array<R[\"Type\"]> = [];\n let page = 1;\n while (true) {\n const reply = yield* call(options, path(page), S.Array(result));\n rows.push(...reply.result);\n const last =\n reply.totalPages === undefined ? reply.result.length < perPage : page >= reply.totalPages;\n if (last) return rows;\n page += 1;\n }\n });\n\n/** Every zone the credential can see, across accounts; internal zones cannot host records. */\nexport const listZones = (options: Options, accountId?: string): Fx<ReadonlyArray<Protocol.Zone>> =>\n paginate(\n options,\n (page) => {\n const query = new URLSearchParams({ page: String(page), per_page: \"50\" });\n if (accountId !== undefined) query.set(\"account.id\", accountId);\n return `/zones?${query}`;\n },\n Protocol.Zone,\n 50,\n ).pipe(Effect.map((zones) => zones.filter((zone) => zone.type !== \"internal\")));\n\n/**\n * Token status and expiry from a verify endpoint; `null` expiry means the token never expires. An\n * inactive token, or a 403 from the verify endpoint, is `Unauthenticated`.\n */\nexport const verifyToken = (options: Options, path: string): Fx<DateTime.Utc | null> =>\n call(options, path, Protocol.Token).pipe(\n Effect.flatMap(({ result }) =>\n result.status === \"active\"\n ? Effect.succeed(\n typeof result.expires_on === \"string\" ? DateTime.makeUnsafe(result.expires_on) : null,\n )\n : Errors.fail(\n new Reason.Unauthenticated({\n message: `Cloudflare token is ${result.status}`,\n }),\n ),\n ),\n rejectedToken,\n );\n\nexport const tokenExpiry = (\n options: Options,\n accountId: string | null,\n): Fx<{ readonly expiresAt: DateTime.Utc | null; readonly kind: \"user\" | \"account\" }> => {\n const verify = (path: string) => verifyToken(options, path);\n return verify(\"/user/tokens/verify\").pipe(\n Effect.map((expiresAt) => ({ expiresAt, kind: \"user\" as const })),\n Effect.catchIf(\n (error) => error.reason._tag !== \"Unauthenticated\" || accountId !== null,\n () =>\n accountId === null\n ? Effect.succeed({ expiresAt: null, kind: \"user\" as const })\n : verify(`/accounts/${encodeURIComponent(accountId)}/tokens/verify`).pipe(\n Effect.map((expiresAt) => ({ expiresAt, kind: \"account\" as const })),\n Effect.catchIf(\n (error) => error.reason._tag !== \"Unauthenticated\",\n () => Effect.succeed({ expiresAt: null, kind: \"user\" as const }),\n ),\n ),\n ),\n );\n};\n\nexport const dns = (options: Options, zoneId: string): Provider.Dns => {\n const base = `/zones/${encodeURIComponent(zoneId)}/dns_records`;\n return {\n list: (zone) =>\n paginate(\n options,\n (page) => `${base}?${new URLSearchParams({ page: String(page), per_page: \"100\" })}`,\n Protocol.Record,\n 100,\n ).pipe(\n Effect.flatMap((records) =>\n Effect.forEach(\n records.filter((record) => Records.isWithinZone(record.name, zone)),\n Records.decode,\n ),\n ),\n ),\n create: (_zone, record: DnsRecord.Model) =>\n call(options, base, Protocol.Record, {\n method: \"POST\",\n body: JSON.stringify(Records.encode(record)),\n }).pipe(Effect.map(({ result }) => ({ providerRecordId: result.id }))),\n get: (_zone, providerRecordId) =>\n call(options, `${base}/${encodeURIComponent(providerRecordId)}`, Protocol.Record).pipe(\n Effect.flatMap(({ result }) => Records.decode(result)),\n Effect.map(({ record }) => record),\n Effect.catchIf(\n (error) => error.reason === notFound,\n () => Effect.succeed(null),\n ),\n ),\n delete: (_zone, providerRecordId) =>\n call(options, `${base}/${encodeURIComponent(providerRecordId)}`, S.Unknown, {\n method: \"DELETE\",\n }).pipe(Effect.asVoid),\n };\n};\n"],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@ import { DomainKitError, fail } from "../error.mjs";
|
|
|
4
4
|
import { Service } from "../../Custody.mjs";
|
|
5
5
|
import { fresh } from "../ids.mjs";
|
|
6
6
|
import { Service as Service$1, make } from "../../Principal.mjs";
|
|
7
|
-
import { encodeContext } from "../../Provider.mjs";
|
|
7
|
+
import { encodeContext, tokenFields } from "../../Provider.mjs";
|
|
8
8
|
import { Service as Service$2 } from "../../Resolver.mjs";
|
|
9
9
|
import { applied } from "../../Receipt.mjs";
|
|
10
10
|
import { Attachment, Authorization, Credential, Service as Service$3 } from "../../Storage.mjs";
|
|
@@ -12,7 +12,7 @@ import { attach, session } from "../../Connect.mjs";
|
|
|
12
12
|
import { apply, approve, plan } from "../../Provision.mjs";
|
|
13
13
|
import { apply as apply$1, approve as approve$1, plan as plan$1 } from "../../Cleanup.mjs";
|
|
14
14
|
import { layerMemory } from "../../DomainKit.mjs";
|
|
15
|
-
import { DateTime, Effect, Layer } from "effect";
|
|
15
|
+
import { DateTime, Effect, Layer, Redacted } from "effect";
|
|
16
16
|
//#region src/internal/conformance/provider.ts
|
|
17
17
|
const principal = make({
|
|
18
18
|
ownerId: "conformance",
|
|
@@ -67,8 +67,9 @@ const txt = (zone, label, value) => txt$1({
|
|
|
67
67
|
});
|
|
68
68
|
const recordIds = (receipt) => applied(receipt).map(({ providerRecordId }) => providerRecordId);
|
|
69
69
|
/**
|
|
70
|
-
* Runs create/readback/cleanup, exact-noop, conflict, stale-plan,
|
|
71
|
-
* real provider definition, through the same Provision and Cleanup
|
|
70
|
+
* Runs create/readback/cleanup, exact-noop, conflict, stale-plan, partial-apply, and
|
|
71
|
+
* rejected-token against a real provider definition, through the same Provision and Cleanup
|
|
72
|
+
* services hosts use.
|
|
72
73
|
*/
|
|
73
74
|
const provider = (definition, credential, zone, options = {}) => {
|
|
74
75
|
const prefix = options.prefix ?? "domainkit-conformance";
|
|
@@ -86,12 +87,12 @@ const provider = (definition, credential, zone, options = {}) => {
|
|
|
86
87
|
const name = "create-readback-cleanup";
|
|
87
88
|
return Effect.gen(function* () {
|
|
88
89
|
const requirements = [cname(zone, `${prefix}-create`), txt(zone, `${prefix}-page`, "domainkit")];
|
|
89
|
-
const plan$
|
|
90
|
+
const plan$4 = yield* plan({
|
|
90
91
|
domain: attachment.domain,
|
|
91
92
|
requirements
|
|
92
93
|
});
|
|
93
|
-
yield* expect(name, plan$
|
|
94
|
-
const receipt = yield* apply(yield* approve(plan$
|
|
94
|
+
yield* expect(name, plan$4.operations.every(({ _tag }) => _tag === "Create"), "expected two Create operations");
|
|
95
|
+
const receipt = yield* apply(yield* approve(plan$4));
|
|
95
96
|
created = recordIds(receipt);
|
|
96
97
|
yield* expect(name, receipt.status === "complete" && created.length === 2, "apply must return a complete receipt with provider record ids");
|
|
97
98
|
const observed = yield* dns.list(zone);
|
|
@@ -111,12 +112,12 @@ const provider = (definition, credential, zone, options = {}) => {
|
|
|
111
112
|
const requirement = cname(zone, `${prefix}-exact`);
|
|
112
113
|
const { providerRecordId } = yield* dns.create(zone, requirement);
|
|
113
114
|
created = [providerRecordId];
|
|
114
|
-
const plan$
|
|
115
|
+
const plan$5 = yield* plan({
|
|
115
116
|
domain: attachment.domain,
|
|
116
117
|
requirements: [requirement]
|
|
117
118
|
});
|
|
118
|
-
yield* expect(name, plan$
|
|
119
|
-
const receipt = yield* apply(yield* approve(plan$
|
|
119
|
+
yield* expect(name, plan$5.operations[0]?._tag === "Noop", "an exact record must plan as Noop");
|
|
120
|
+
const receipt = yield* apply(yield* approve(plan$5));
|
|
120
121
|
yield* expect(name, applied(receipt).length === 0, "an exact plan must not create another record");
|
|
121
122
|
}).pipe(Effect.ensuring(cleanup(dns, () => created)));
|
|
122
123
|
});
|
|
@@ -126,12 +127,12 @@ const provider = (definition, credential, zone, options = {}) => {
|
|
|
126
127
|
return Effect.gen(function* () {
|
|
127
128
|
const { providerRecordId } = yield* dns.create(zone, txt(zone, `${prefix}-conflict`, "occupied"));
|
|
128
129
|
created = [providerRecordId];
|
|
129
|
-
const plan$
|
|
130
|
+
const plan$3 = yield* plan({
|
|
130
131
|
domain: attachment.domain,
|
|
131
132
|
requirements: [cname(zone, `${prefix}-conflict`)]
|
|
132
133
|
});
|
|
133
|
-
yield* expect(name, plan$
|
|
134
|
-
const rejected = yield* approve(plan$
|
|
134
|
+
yield* expect(name, plan$3.operations[0]?._tag === "Conflict", "a CNAME over an occupied name must plan as Conflict");
|
|
135
|
+
const rejected = yield* approve(plan$3).pipe(Effect.result);
|
|
135
136
|
yield* expect(name, rejected._tag === "Failure" && rejected.failure.reason._tag === "Conflict", "approve must fail closed on conflicts");
|
|
136
137
|
}).pipe(Effect.ensuring(cleanup(dns, () => created)));
|
|
137
138
|
});
|
|
@@ -140,11 +141,11 @@ const provider = (definition, credential, zone, options = {}) => {
|
|
|
140
141
|
const name = "stale-plan";
|
|
141
142
|
return Effect.gen(function* () {
|
|
142
143
|
const requirement = cname(zone, `${prefix}-stale`);
|
|
143
|
-
const plan$
|
|
144
|
+
const plan$6 = yield* plan({
|
|
144
145
|
domain: attachment.domain,
|
|
145
146
|
requirements: [requirement]
|
|
146
147
|
});
|
|
147
|
-
const approval = yield* approve(plan$
|
|
148
|
+
const approval = yield* approve(plan$6);
|
|
148
149
|
const { providerRecordId } = yield* dns.create(zone, requirement);
|
|
149
150
|
created = [providerRecordId];
|
|
150
151
|
const stale = yield* apply(approval).pipe(Effect.result);
|
|
@@ -177,11 +178,11 @@ const provider = (definition, credential, zone, options = {}) => {
|
|
|
177
178
|
let created = [];
|
|
178
179
|
const name = "partial-apply-cleanup";
|
|
179
180
|
return Effect.gen(function* () {
|
|
180
|
-
const plan$
|
|
181
|
+
const plan$2 = yield* plan({
|
|
181
182
|
domain: attachment.domain,
|
|
182
183
|
requirements: [cname(zone, `${prefix}-partial-one`), txt(zone, `${prefix}-partial-two`, "domainkit")]
|
|
183
184
|
});
|
|
184
|
-
const receipt = yield* apply(yield* approve(plan$
|
|
185
|
+
const receipt = yield* apply(yield* approve(plan$2));
|
|
185
186
|
created = recordIds(receipt);
|
|
186
187
|
yield* expect(name, receipt.status === "partial" && created.length === 1, "a later write failure must preserve the first confirmed write in a partial receipt");
|
|
187
188
|
const cleanupPlan = yield* plan$1({ receiptId: receipt.id });
|
|
@@ -191,12 +192,24 @@ const provider = (definition, credential, zone, options = {}) => {
|
|
|
191
192
|
created = [];
|
|
192
193
|
}).pipe(Effect.ensuring(cleanup(dns, () => created)));
|
|
193
194
|
});
|
|
195
|
+
/** A token the provider rejects must read as `Unauthenticated`, so a host tells the customer the provider refused it. */
|
|
196
|
+
const rejectedToken = Effect.gen(function* () {
|
|
197
|
+
const name = "rejected-token";
|
|
198
|
+
const auth = definition.auth.token;
|
|
199
|
+
if (auth === void 0) return;
|
|
200
|
+
const required = tokenFields(auth).filter((field) => field.required);
|
|
201
|
+
if (options.rejectedToken === void 0 && required.some((field) => !field.secret)) return yield* Effect.fail(failure(name, `the token method requires ${required.filter((field) => !field.secret).map((field) => field.name).join(", ")}; pass options.rejectedToken with values the provider rejects`));
|
|
202
|
+
const values = options.rejectedToken ?? Object.fromEntries(required.map((field) => [field.name, Redacted.make("")]));
|
|
203
|
+
const outcome = yield* auth.authenticate(values).pipe(Effect.result);
|
|
204
|
+
yield* expect(name, outcome._tag === "Failure" && outcome.failure.reason._tag === "Unauthenticated", "authenticating a rejected token must fail Unauthenticated");
|
|
205
|
+
});
|
|
194
206
|
return Effect.all([
|
|
195
207
|
createReadbackCleanup,
|
|
196
208
|
exactNoop,
|
|
197
209
|
conflict,
|
|
198
210
|
stalePlan,
|
|
199
|
-
partialApplyCleanup
|
|
211
|
+
partialApplyCleanup,
|
|
212
|
+
rejectedToken
|
|
200
213
|
], { discard: true });
|
|
201
214
|
};
|
|
202
215
|
/** Verification is out of scope for the provider contract; keep the pool quiet. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.mjs","names":["Principal.make","Errors.DomainKitError","Reason.InvalidInput","Storage.Service","Custody.Service","Storage.Authorization","Provider.encodeContext","Storage.Credential","Connect.attach","Storage.Attachment","DnsRecord.cname","DnsRecord.txt","Receipt.applied","Connect.session","session","Principal.Service","DomainKit.layerMemory","Resolver.Service","plan","Provision.plan","Provision.apply","Provision.approve","DnsRecord.equals","Cleanup.plan","Cleanup.apply","Cleanup.approve","Errors.fail","Reason.ProviderUnavailable"],"sources":["../../../src/internal/conformance/provider.ts"],"sourcesContent":["import { DateTime, Effect, Layer } from \"effect\";\n\nimport * as Cleanup from \"../../Cleanup.ts\";\nimport * as Connect from \"../../Connect.ts\";\nimport * as Custody from \"../../Custody.ts\";\nimport * as DnsRecord from \"../../DnsRecord.ts\";\nimport * as Errors from \"../error.ts\";\nimport * as Reason from \"../../Reason.ts\";\nimport * as DomainKit from \"../../DomainKit.ts\";\nimport * as Principal from \"../../Principal.ts\";\nimport * as Provider from \"../../Provider.ts\";\nimport * as Provision from \"../../Provision.ts\";\nimport * as Receipt from \"../../Receipt.ts\";\nimport * as Resolver from \"../../Resolver.ts\";\nimport * as Storage from \"../../Storage.ts\";\nimport { fresh } from \"../ids.ts\";\n\nexport const cases = [\n \"create-readback-cleanup\",\n \"exact-noop\",\n \"conflict\",\n \"stale-plan\",\n \"partial-apply-cleanup\",\n] as const;\nexport type Case = (typeof cases)[number];\n\nconst principal = Principal.make({ ownerId: \"conformance\", actorId: \"conformance\" });\n\nconst failure = (name: Case, message: string) =>\n new Errors.DomainKitError({\n reason: new Reason.InvalidInput({\n message: `provider conformance ${name}: ${message}`,\n field: \"conformance\",\n }),\n });\n\nconst expect = (name: Case, condition: boolean, message: string) =>\n condition ? Effect.void : Effect.fail(failure(name, message));\n\ntype Env = Storage.Service | Connect.Service | Provision.Service | Cleanup.Service;\n\n/** Attach `zone` to a connection built straight from `credential`, bypassing `authenticate`. */\nconst attachZone = (\n definition: Provider.Definition,\n credential: Provider.Credential,\n zone: string,\n) =>\n Effect.gen(function* () {\n const storage = yield* Storage.Service;\n const custody = yield* Custody.Service;\n const now = yield* DateTime.now;\n const authorization = new Storage.Authorization({\n id: yield* fresh(\"auth\"),\n ownerId: principal.ownerId,\n provider: definition.id,\n method: \"token\",\n capabilities: [\"dns:read\", \"dns:write\"],\n context: yield* Provider.encodeContext(definition, credential.context),\n revocation: \"active\",\n createdBy: principal.actorId,\n createdAt: now,\n });\n yield* storage.authorizations.upsert({\n authorization,\n credential: new Storage.Credential({\n ciphertext: yield* custody.seal(credential.secret),\n expiresAt: null,\n rotatedAt: now,\n }),\n });\n const connection = yield* storage.connections.create(authorization.id);\n const attached = yield* Connect.attach({ connectionId: connection.id, domain: zone });\n if (!(attached instanceof Storage.Attachment)) {\n return yield* Effect.fail(\n failure(\"create-readback-cleanup\", `zone ${zone} needs a selection; pass an exact zone`),\n );\n }\n return attached;\n });\n\nconst cname = (zone: string, label: string) =>\n DnsRecord.cname({ name: `${label}.${zone}`, target: \"target.example.net\", ttl: 300 });\nconst txt = (zone: string, label: string, value: string) =>\n DnsRecord.txt({ name: `${label}.${zone}`, value, ttl: 300 });\n\nconst recordIds = (receipt: Receipt.Model) =>\n Receipt.applied(receipt).map(({ providerRecordId }) => providerRecordId);\n\n/**\n * Runs create/readback/cleanup, exact-noop, conflict, stale-plan, and partial-apply against a\n * real provider definition, through the same Provision and Cleanup services hosts use.\n */\nexport const provider = (\n definition: Provider.Definition,\n credential: Provider.Credential,\n zone: string,\n options: { readonly prefix?: string } = {},\n): Effect.Effect<void, Errors.DomainKitError> => {\n const prefix = options.prefix ?? \"domainkit-conformance\";\n const run = <A>(\n target: Provider.Definition,\n body: (\n dns: Provider.Dns,\n attachment: Storage.Attachment,\n ) => Effect.Effect<A, Errors.DomainKitError, Env | Principal.Service>,\n ) =>\n Effect.gen(function* () {\n const attachment = yield* attachZone(target, credential, zone);\n const { session, target: resolved } = yield* Connect.session(attachment.id);\n return yield* body(session.dns(resolved), attachment);\n }).pipe(\n Effect.provideService(Principal.Service, principal),\n Effect.provide(\n DomainKit.layerMemory({\n providers: [target],\n resolver: Layer.succeed(Resolver.Service)(silentResolver),\n }),\n ),\n );\n\n const cleanup = (dns: Provider.Dns, ids: () => ReadonlyArray<string>) =>\n Effect.suspend(() =>\n Effect.forEach(ids(), (id) => dns.delete(zone, id).pipe(Effect.ignore), { discard: true }),\n );\n\n const createReadbackCleanup = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"create-readback-cleanup\";\n return Effect.gen(function* () {\n const requirements = [\n cname(zone, `${prefix}-create`),\n txt(zone, `${prefix}-page`, \"domainkit\"),\n ];\n const plan = yield* Provision.plan({ domain: attachment.domain, requirements });\n yield* expect(\n name,\n plan.operations.every(({ _tag }) => _tag === \"Create\"),\n \"expected two Create operations\",\n );\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n created = recordIds(receipt);\n yield* expect(\n name,\n receipt.status === \"complete\" && created.length === 2,\n \"apply must return a complete receipt with provider record ids\",\n );\n const observed = yield* dns.list(zone);\n yield* expect(\n name,\n requirements.every((requirement) =>\n observed.some(({ record }) => DnsRecord.equals(record, requirement)),\n ),\n \"list must return every created record, including across provider pages\",\n );\n const cleanupPlan = yield* Cleanup.plan({ receiptId: receipt.id });\n yield* expect(\n name,\n cleanupPlan.operations.every(({ _tag }) => _tag === \"Delete\"),\n \"cleanup plan must delete both records\",\n );\n const cleaned = yield* Cleanup.apply(yield* Cleanup.approve(cleanupPlan));\n const remaining = yield* Effect.forEach(created, (id) => dns.get(zone, id));\n yield* expect(\n name,\n cleaned.status === \"complete\" && remaining.every((record) => record === null),\n \"receipt-bound cleanup must remove every created record\",\n );\n created = [];\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const exactNoop = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"exact-noop\";\n return Effect.gen(function* () {\n const requirement = cname(zone, `${prefix}-exact`);\n const { providerRecordId } = yield* dns.create(zone, requirement);\n created = [providerRecordId];\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [requirement],\n });\n yield* expect(name, plan.operations[0]?._tag === \"Noop\", \"an exact record must plan as Noop\");\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n yield* expect(\n name,\n Receipt.applied(receipt).length === 0,\n \"an exact plan must not create another record\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const conflict = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"conflict\";\n return Effect.gen(function* () {\n const { providerRecordId } = yield* dns.create(\n zone,\n txt(zone, `${prefix}-conflict`, \"occupied\"),\n );\n created = [providerRecordId];\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [cname(zone, `${prefix}-conflict`)],\n });\n yield* expect(\n name,\n plan.operations[0]?._tag === \"Conflict\",\n \"a CNAME over an occupied name must plan as Conflict\",\n );\n const rejected = yield* Provision.approve(plan).pipe(Effect.result);\n yield* expect(\n name,\n rejected._tag === \"Failure\" && rejected.failure.reason._tag === \"Conflict\",\n \"approve must fail closed on conflicts\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const stalePlan = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"stale-plan\";\n return Effect.gen(function* () {\n const requirement = cname(zone, `${prefix}-stale`);\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [requirement],\n });\n const approval = yield* Provision.approve(plan);\n const { providerRecordId } = yield* dns.create(zone, requirement);\n created = [providerRecordId];\n const stale = yield* Provision.apply(approval).pipe(Effect.result);\n yield* expect(\n name,\n stale._tag === \"Failure\" && stale.failure.reason._tag === \"Stale\",\n \"apply must reject provider state that changed after approval\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const flaky: Provider.Definition = {\n ...definition,\n session: (input) => {\n const session = definition.session(input);\n return {\n ...session,\n dns: (target) => {\n const dns = session.dns(target);\n let writes = 0;\n return {\n ...dns,\n create: (targetZone, record) => {\n writes += 1;\n return writes === 2\n ? Errors.fail(\n new Reason.ProviderUnavailable({\n provider: definition.id,\n message: \"injected conformance failure\",\n }),\n )\n : dns.create(targetZone, record);\n },\n };\n },\n };\n },\n };\n\n const partialApplyCleanup = run(flaky, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"partial-apply-cleanup\";\n return Effect.gen(function* () {\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [\n cname(zone, `${prefix}-partial-one`),\n txt(zone, `${prefix}-partial-two`, \"domainkit\"),\n ],\n });\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n created = recordIds(receipt);\n yield* expect(\n name,\n receipt.status === \"partial\" && created.length === 1,\n \"a later write failure must preserve the first confirmed write in a partial receipt\",\n );\n const cleanupPlan = yield* Cleanup.plan({ receiptId: receipt.id });\n yield* Cleanup.apply(yield* Cleanup.approve(cleanupPlan));\n const remaining = yield* Effect.forEach(created, (id) => dns.get(zone, id));\n yield* expect(\n name,\n remaining.every((record) => record === null),\n \"partial receipt cleanup must remove the confirmed write\",\n );\n created = [];\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n return Effect.all([createReadbackCleanup, exactNoop, conflict, stalePlan, partialApplyCleanup], {\n discard: true,\n });\n};\n\n/** Verification is out of scope for the provider contract; keep the pool quiet. */\nconst silentResolver: Resolver.Interface = {\n resolve: () => Effect.succeed([]),\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"provider.mjs","names":["Principal.make","Errors.DomainKitError","Reason.InvalidInput","Storage.Service","Custody.Service","Storage.Authorization","Provider.encodeContext","Storage.Credential","Connect.attach","Storage.Attachment","DnsRecord.cname","DnsRecord.txt","Receipt.applied","Connect.session","session","Principal.Service","DomainKit.layerMemory","Resolver.Service","plan","Provision.plan","Provision.apply","Provision.approve","DnsRecord.equals","Cleanup.plan","Cleanup.apply","Cleanup.approve","Errors.fail","Reason.ProviderUnavailable","Provider.tokenFields"],"sources":["../../../src/internal/conformance/provider.ts"],"sourcesContent":["import { DateTime, Effect, Layer, Redacted } from \"effect\";\n\nimport * as Cleanup from \"../../Cleanup.ts\";\nimport * as Connect from \"../../Connect.ts\";\nimport * as Custody from \"../../Custody.ts\";\nimport * as DnsRecord from \"../../DnsRecord.ts\";\nimport * as Errors from \"../error.ts\";\nimport * as Reason from \"../../Reason.ts\";\nimport * as DomainKit from \"../../DomainKit.ts\";\nimport * as Principal from \"../../Principal.ts\";\nimport * as Provider from \"../../Provider.ts\";\nimport * as Provision from \"../../Provision.ts\";\nimport * as Receipt from \"../../Receipt.ts\";\nimport * as Resolver from \"../../Resolver.ts\";\nimport * as Storage from \"../../Storage.ts\";\nimport { fresh } from \"../ids.ts\";\n\nexport const cases = [\n \"create-readback-cleanup\",\n \"exact-noop\",\n \"conflict\",\n \"stale-plan\",\n \"partial-apply-cleanup\",\n \"rejected-token\",\n] as const;\nexport type Case = (typeof cases)[number];\n\nconst principal = Principal.make({ ownerId: \"conformance\", actorId: \"conformance\" });\n\nconst failure = (name: Case, message: string) =>\n new Errors.DomainKitError({\n reason: new Reason.InvalidInput({\n message: `provider conformance ${name}: ${message}`,\n field: \"conformance\",\n }),\n });\n\nconst expect = (name: Case, condition: boolean, message: string) =>\n condition ? Effect.void : Effect.fail(failure(name, message));\n\ntype Env = Storage.Service | Connect.Service | Provision.Service | Cleanup.Service;\n\n/** Attach `zone` to a connection built straight from `credential`, bypassing `authenticate`. */\nconst attachZone = (\n definition: Provider.Definition,\n credential: Provider.Credential,\n zone: string,\n) =>\n Effect.gen(function* () {\n const storage = yield* Storage.Service;\n const custody = yield* Custody.Service;\n const now = yield* DateTime.now;\n const authorization = new Storage.Authorization({\n id: yield* fresh(\"auth\"),\n ownerId: principal.ownerId,\n provider: definition.id,\n method: \"token\",\n capabilities: [\"dns:read\", \"dns:write\"],\n context: yield* Provider.encodeContext(definition, credential.context),\n revocation: \"active\",\n createdBy: principal.actorId,\n createdAt: now,\n });\n yield* storage.authorizations.upsert({\n authorization,\n credential: new Storage.Credential({\n ciphertext: yield* custody.seal(credential.secret),\n expiresAt: null,\n rotatedAt: now,\n }),\n });\n const connection = yield* storage.connections.create(authorization.id);\n const attached = yield* Connect.attach({ connectionId: connection.id, domain: zone });\n if (!(attached instanceof Storage.Attachment)) {\n return yield* Effect.fail(\n failure(\"create-readback-cleanup\", `zone ${zone} needs a selection; pass an exact zone`),\n );\n }\n return attached;\n });\n\nconst cname = (zone: string, label: string) =>\n DnsRecord.cname({ name: `${label}.${zone}`, target: \"target.example.net\", ttl: 300 });\nconst txt = (zone: string, label: string, value: string) =>\n DnsRecord.txt({ name: `${label}.${zone}`, value, ttl: 300 });\n\nconst recordIds = (receipt: Receipt.Model) =>\n Receipt.applied(receipt).map(({ providerRecordId }) => providerRecordId);\n\n/**\n * Runs create/readback/cleanup, exact-noop, conflict, stale-plan, partial-apply, and\n * rejected-token against a real provider definition, through the same Provision and Cleanup\n * services hosts use.\n */\nexport const provider = (\n definition: Provider.Definition,\n credential: Provider.Credential,\n zone: string,\n options: {\n readonly prefix?: string;\n /**\n * Token values the provider rejects, for the `rejected-token` case. Default: every required\n * field set to an empty secret, which needs a token method whose required fields are all\n * secrets.\n */\n readonly rejectedToken?: Provider.TokenValues;\n } = {},\n): Effect.Effect<void, Errors.DomainKitError> => {\n const prefix = options.prefix ?? \"domainkit-conformance\";\n const run = <A>(\n target: Provider.Definition,\n body: (\n dns: Provider.Dns,\n attachment: Storage.Attachment,\n ) => Effect.Effect<A, Errors.DomainKitError, Env | Principal.Service>,\n ) =>\n Effect.gen(function* () {\n const attachment = yield* attachZone(target, credential, zone);\n const { session, target: resolved } = yield* Connect.session(attachment.id);\n return yield* body(session.dns(resolved), attachment);\n }).pipe(\n Effect.provideService(Principal.Service, principal),\n Effect.provide(\n DomainKit.layerMemory({\n providers: [target],\n resolver: Layer.succeed(Resolver.Service)(silentResolver),\n }),\n ),\n );\n\n const cleanup = (dns: Provider.Dns, ids: () => ReadonlyArray<string>) =>\n Effect.suspend(() =>\n Effect.forEach(ids(), (id) => dns.delete(zone, id).pipe(Effect.ignore), { discard: true }),\n );\n\n const createReadbackCleanup = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"create-readback-cleanup\";\n return Effect.gen(function* () {\n const requirements = [\n cname(zone, `${prefix}-create`),\n txt(zone, `${prefix}-page`, \"domainkit\"),\n ];\n const plan = yield* Provision.plan({ domain: attachment.domain, requirements });\n yield* expect(\n name,\n plan.operations.every(({ _tag }) => _tag === \"Create\"),\n \"expected two Create operations\",\n );\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n created = recordIds(receipt);\n yield* expect(\n name,\n receipt.status === \"complete\" && created.length === 2,\n \"apply must return a complete receipt with provider record ids\",\n );\n const observed = yield* dns.list(zone);\n yield* expect(\n name,\n requirements.every((requirement) =>\n observed.some(({ record }) => DnsRecord.equals(record, requirement)),\n ),\n \"list must return every created record, including across provider pages\",\n );\n const cleanupPlan = yield* Cleanup.plan({ receiptId: receipt.id });\n yield* expect(\n name,\n cleanupPlan.operations.every(({ _tag }) => _tag === \"Delete\"),\n \"cleanup plan must delete both records\",\n );\n const cleaned = yield* Cleanup.apply(yield* Cleanup.approve(cleanupPlan));\n const remaining = yield* Effect.forEach(created, (id) => dns.get(zone, id));\n yield* expect(\n name,\n cleaned.status === \"complete\" && remaining.every((record) => record === null),\n \"receipt-bound cleanup must remove every created record\",\n );\n created = [];\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const exactNoop = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"exact-noop\";\n return Effect.gen(function* () {\n const requirement = cname(zone, `${prefix}-exact`);\n const { providerRecordId } = yield* dns.create(zone, requirement);\n created = [providerRecordId];\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [requirement],\n });\n yield* expect(name, plan.operations[0]?._tag === \"Noop\", \"an exact record must plan as Noop\");\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n yield* expect(\n name,\n Receipt.applied(receipt).length === 0,\n \"an exact plan must not create another record\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const conflict = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"conflict\";\n return Effect.gen(function* () {\n const { providerRecordId } = yield* dns.create(\n zone,\n txt(zone, `${prefix}-conflict`, \"occupied\"),\n );\n created = [providerRecordId];\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [cname(zone, `${prefix}-conflict`)],\n });\n yield* expect(\n name,\n plan.operations[0]?._tag === \"Conflict\",\n \"a CNAME over an occupied name must plan as Conflict\",\n );\n const rejected = yield* Provision.approve(plan).pipe(Effect.result);\n yield* expect(\n name,\n rejected._tag === \"Failure\" && rejected.failure.reason._tag === \"Conflict\",\n \"approve must fail closed on conflicts\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const stalePlan = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"stale-plan\";\n return Effect.gen(function* () {\n const requirement = cname(zone, `${prefix}-stale`);\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [requirement],\n });\n const approval = yield* Provision.approve(plan);\n const { providerRecordId } = yield* dns.create(zone, requirement);\n created = [providerRecordId];\n const stale = yield* Provision.apply(approval).pipe(Effect.result);\n yield* expect(\n name,\n stale._tag === \"Failure\" && stale.failure.reason._tag === \"Stale\",\n \"apply must reject provider state that changed after approval\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const flaky: Provider.Definition = {\n ...definition,\n session: (input) => {\n const session = definition.session(input);\n return {\n ...session,\n dns: (target) => {\n const dns = session.dns(target);\n let writes = 0;\n return {\n ...dns,\n create: (targetZone, record) => {\n writes += 1;\n return writes === 2\n ? Errors.fail(\n new Reason.ProviderUnavailable({\n provider: definition.id,\n message: \"injected conformance failure\",\n }),\n )\n : dns.create(targetZone, record);\n },\n };\n },\n };\n },\n };\n\n const partialApplyCleanup = run(flaky, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"partial-apply-cleanup\";\n return Effect.gen(function* () {\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [\n cname(zone, `${prefix}-partial-one`),\n txt(zone, `${prefix}-partial-two`, \"domainkit\"),\n ],\n });\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n created = recordIds(receipt);\n yield* expect(\n name,\n receipt.status === \"partial\" && created.length === 1,\n \"a later write failure must preserve the first confirmed write in a partial receipt\",\n );\n const cleanupPlan = yield* Cleanup.plan({ receiptId: receipt.id });\n yield* Cleanup.apply(yield* Cleanup.approve(cleanupPlan));\n const remaining = yield* Effect.forEach(created, (id) => dns.get(zone, id));\n yield* expect(\n name,\n remaining.every((record) => record === null),\n \"partial receipt cleanup must remove the confirmed write\",\n );\n created = [];\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n /** A token the provider rejects must read as `Unauthenticated`, so a host tells the customer the provider refused it. */\n const rejectedToken = Effect.gen(function* () {\n const name: Case = \"rejected-token\";\n const auth = definition.auth.token;\n if (auth === undefined) return;\n const required = Provider.tokenFields(auth).filter((field) => field.required);\n if (options.rejectedToken === undefined && required.some((field) => !field.secret)) {\n return yield* Effect.fail(\n failure(\n name,\n `the token method requires ${required\n .filter((field) => !field.secret)\n .map((field) => field.name)\n .join(\", \")}; pass options.rejectedToken with values the provider rejects`,\n ),\n );\n }\n const values =\n options.rejectedToken ??\n Object.fromEntries(required.map((field) => [field.name, Redacted.make(\"\")]));\n const outcome = yield* auth.authenticate(values).pipe(Effect.result);\n yield* expect(\n name,\n outcome._tag === \"Failure\" && outcome.failure.reason._tag === \"Unauthenticated\",\n \"authenticating a rejected token must fail Unauthenticated\",\n );\n });\n\n return Effect.all(\n [createReadbackCleanup, exactNoop, conflict, stalePlan, partialApplyCleanup, rejectedToken],\n { discard: true },\n );\n};\n\n/** Verification is out of scope for the provider contract; keep the pool quiet. */\nconst silentResolver: Resolver.Interface = {\n resolve: () => Effect.succeed([]),\n};\n"],"mappings":"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"}
|
package/dist/internal/http.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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. */
|
|
@@ -63,6 +63,11 @@ const classify = (provider, status, headers, message, code) => {
|
|
|
63
63
|
code: code ?? String(status)
|
|
64
64
|
});
|
|
65
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 }))));
|
|
66
71
|
const rejected = (provider, message, code) => new DomainKitError({ reason: new ProviderRejected({
|
|
67
72
|
provider,
|
|
68
73
|
message,
|
|
@@ -85,6 +90,6 @@ const bearer = (token, init) => ({
|
|
|
85
90
|
}
|
|
86
91
|
});
|
|
87
92
|
//#endregion
|
|
88
|
-
export { bearer, classify, headersFrom, rejected, requestJson };
|
|
93
|
+
export { bearer, classify, headersFrom, rejected, rejectedToken, requestJson };
|
|
89
94
|
|
|
90
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 { 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":";;;;;AAgBA,MAAa,eAAe,UAM1B,OAAO,WAAW;CAChB,KAAK,OAAO,WAA2B;EACrC,MAAM,EAAE,OAAO,SAAS;EACxB,MAAM,WAAW,MAAM,KAAK,MAAM,KAAK;GAAE,GAAG,MAAM;GAAM;EAAO,CAAC;EAChE,MAAM,OAAO,MAAM,SAAS,KAAK;EACjC,IAAI,OAAgB;EACpB,IAAI,KAAK,SAAS,GAChB,IAAI;GACF,OAAO,KAAK,MAAM,IAAI;EACxB,QAAQ;GACN,MAAM,IAAIA,eAAsB,EAC9B,QAAQ,SAAS,KACb,IAAIC,iBAAwB;IAC1B,UAAU,MAAM;IAChB,SAAS,GAAG,MAAM,SAAS;GAC7B,CAAC,IACD,SACE,MAAM,UACN,SAAS,QACT,SAAS,SACT,QAAQ,SAAS,QACnB,EACN,CAAC;EACH;EAEF,OAAO;GAAE,QAAQ,SAAS;GAAQ,IAAI,SAAS;GAAI,SAAS,SAAS;GAAS;EAAK;CACrF;CACA,QAAQ,UACNC,iBAAwB,KAAK,IACzB,QACA,IAAIF,eAAsB,EACxB,QAAQ,IAAIG,oBAA2B;EACrC,UAAU,MAAM;EAChB,SAAS,iBAAiB,QAAQ,MAAM,UAAU,GAAG,MAAM,SAAS;CACtE,CAAC,EACH,CAAC;AACT,CAAC;;AAGH,MAAa,eACX,WAEA,WAAW,KAAA,IACP,OAAO,WAAW,IAAI,QAAQ,CAAC,IAC/B,OAAO,WAAW,aAChB,OAAO,WAAW,IAAI,QAAQ,MAAM,CAAC,IACrC,OAAO,WAAW;CAChB,KAAK,YAAY,IAAI,QAAQ,MAAM,OAAO,CAAC;CAC3C,QAAQ,UACND,iBAAwB,KAAK,IACzB,QACA,IAAIF,eAAsB,EACxB,QAAQ,IAAII,gBAAuB,EACjC,SACE,iBAAiB,QAAQ,MAAM,UAAU,mCAC7C,CAAC,EACH,CAAC;AACT,CAAC;;;;;AAMT,MAAa,YACX,UACA,QACA,SACA,SACA,SACiB;CACjB,IAAI,WAAW,KAAK,OAAO,IAAIA,gBAAuB,EAAE,QAAQ,CAAC;CACjE,IAAI,WAAW,KAAK,OAAO,IAAIC,UAAiB,EAAE,QAAQ,CAAC;CAC3D,IAAI,WAAW,OAAO,SAAS,YAC7B,OAAO,IAAIC,iBAAwB;EACjC;EACA;EACA,MAAM,QAAQ,OAAO,MAAM;CAC7B,CAAC;CAEH,IAAI,WAAW,OAAO,UAAU,KAAK;EACnC,MAAM,eAAe,WAAW,QAAQ,IAAI,aAAa,CAAC;EAC1D,OAAO,IAAIH,oBAA2B;GACpC;GACA;GACA,GAAI,iBAAiB,KAAA,IAAY,CAAC,IAAI,EAAE,aAAa;EACvD,CAAC;CACH;CACA,OAAO,IAAIF,iBAAwB;EACjC;EACA;EACA,MAAM,QAAQ,OAAO,MAAM;CAC7B,CAAC;AACH;
|
|
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/internal/oauth.mjs
CHANGED
|
@@ -32,13 +32,32 @@ const clientAuth = (client) => {
|
|
|
32
32
|
const secret = client.clientSecret === null ? "" : Redacted.value(client.clientSecret);
|
|
33
33
|
return client.clientAuth === "client_secret_basic" ? oauth.ClientSecretBasic(secret) : oauth.ClientSecretPost(secret);
|
|
34
34
|
};
|
|
35
|
-
const
|
|
35
|
+
const plaintext = (endpoint) => endpoint !== void 0 && URL.parse(endpoint)?.protocol === "http:";
|
|
36
|
+
/**
|
|
37
|
+
* A destination nothing can move: a loopback literal, or a name RFC 6761 reserves for one. Every
|
|
38
|
+
* other name resolves wherever DNS or a hosts file says, so it is not evidence of anything.
|
|
39
|
+
*/
|
|
40
|
+
const loopback = (endpoint) => {
|
|
41
|
+
const url = endpoint === void 0 ? null : URL.parse(endpoint);
|
|
42
|
+
if (url === null || !plaintext(endpoint)) return false;
|
|
43
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
44
|
+
return host === "localhost" || host.endsWith(".localhost") || host === "::1" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* oauth4webapi refuses plaintext endpoints, which is right for every request that carries a client
|
|
48
|
+
* secret, a code, or a token. Loopback is the one automatic exception: it goes nowhere and nothing
|
|
49
|
+
* can repoint it. Any other plaintext endpoint takes a caller that said so out loud, because a name
|
|
50
|
+
* like `host.docker.internal` resolves wherever the machine has been told to resolve it.
|
|
51
|
+
*/
|
|
52
|
+
const insecureAllowed = (endpoint, allowPlaintext) => loopback(endpoint) || allowPlaintext === true && plaintext(endpoint);
|
|
53
|
+
const requestOptions = (input, fetch, signal) => ({
|
|
36
54
|
signal,
|
|
37
|
-
[oauth.
|
|
55
|
+
...insecureAllowed(input.endpoint, input.allowPlaintext) ? { [oauth.allowInsecureRequests]: true } : {},
|
|
56
|
+
...fetch === void 0 ? {} : { [oauth.customFetch]: (url, init) => fetch(url, {
|
|
38
57
|
...init,
|
|
39
58
|
body: init.body
|
|
40
|
-
})
|
|
41
|
-
};
|
|
59
|
+
}) }
|
|
60
|
+
});
|
|
42
61
|
const tokens = (response, now) => ({
|
|
43
62
|
accessToken: response.access_token,
|
|
44
63
|
refreshToken: response.refresh_token ?? null,
|
|
@@ -74,7 +93,10 @@ const exchangeCode = (input) => Effect.gen(function* () {
|
|
|
74
93
|
code: input.code,
|
|
75
94
|
state: input.state
|
|
76
95
|
}), input.state);
|
|
77
|
-
const reply = await oauth.authorizationCodeGrantRequest(server, client, clientAuth(input.client), params, input.callbackUrl, input.codeVerifier, requestOptions(
|
|
96
|
+
const reply = await oauth.authorizationCodeGrantRequest(server, client, clientAuth(input.client), params, input.callbackUrl, input.codeVerifier, requestOptions({
|
|
97
|
+
allowPlaintext: input.allowPlaintext,
|
|
98
|
+
endpoint: input.server.token_endpoint
|
|
99
|
+
}, input.fetch, signal));
|
|
78
100
|
return oauth.processAuthorizationCodeResponse(server, client, reply);
|
|
79
101
|
},
|
|
80
102
|
catch: (cause) => failure(input.provider, cause)
|
|
@@ -87,7 +109,10 @@ const refresh = (input) => Effect.gen(function* () {
|
|
|
87
109
|
const client = { client_id: input.client.clientId };
|
|
88
110
|
const response = yield* Effect.tryPromise({
|
|
89
111
|
try: async (signal) => {
|
|
90
|
-
const reply = await oauth.refreshTokenGrantRequest(server, client, clientAuth(input.client), input.refreshToken, requestOptions(
|
|
112
|
+
const reply = await oauth.refreshTokenGrantRequest(server, client, clientAuth(input.client), input.refreshToken, requestOptions({
|
|
113
|
+
allowPlaintext: input.allowPlaintext,
|
|
114
|
+
endpoint: input.server.token_endpoint
|
|
115
|
+
}, input.fetch, signal));
|
|
91
116
|
return oauth.processRefreshTokenResponse(server, client, reply);
|
|
92
117
|
},
|
|
93
118
|
catch: (cause) => failure(input.provider, cause)
|
|
@@ -102,7 +127,10 @@ const revoke = (input) => Effect.tryPromise({
|
|
|
102
127
|
try: async (signal) => {
|
|
103
128
|
const server = input.server;
|
|
104
129
|
const client = { client_id: input.client.clientId };
|
|
105
|
-
const reply = await oauth.revocationRequest(server, client, clientAuth(input.client), input.token, requestOptions(
|
|
130
|
+
const reply = await oauth.revocationRequest(server, client, clientAuth(input.client), input.token, requestOptions({
|
|
131
|
+
allowPlaintext: input.allowPlaintext,
|
|
132
|
+
endpoint: input.server.revocation_endpoint
|
|
133
|
+
}, input.fetch, signal));
|
|
106
134
|
await oauth.processRevocationResponse(reply);
|
|
107
135
|
},
|
|
108
136
|
catch: (cause) => failure(input.provider, cause)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth.mjs","names":["Errors.DomainKitError","Reason.CryptoFailed","Reason.Unauthenticated","Reason.ProviderRejected","Reason.ProviderUnavailable"],"sources":["../../src/internal/oauth.ts"],"sourcesContent":["import { DateTime, Effect, Redacted } from \"effect\";\nimport * as oauth from \"oauth4webapi\";\n\nimport * as Errors from \"./error.ts\";\nimport * as Reason from \"../Reason.ts\";\nimport type { Fetch } from \"./http.ts\";\n\nexport interface Server {\n readonly issuer: string;\n readonly authorization_endpoint: string;\n readonly token_endpoint: string;\n readonly revocation_endpoint?: string;\n}\n\nexport type ClientAuth = \"client_secret_basic\" | \"client_secret_post\" | \"none\";\n\nexport interface Client {\n readonly clientId: string;\n readonly clientSecret: Redacted.Redacted<string> | null;\n readonly clientAuth: ClientAuth;\n}\n\n/** A PKCE pair: the verifier stays in the continuation, the challenge goes to the provider. */\nexport const pkce = (): Effect.Effect<\n { readonly codeVerifier: string; readonly codeChallenge: string },\n Errors.DomainKitError\n> =>\n Effect.tryPromise({\n try: async () => {\n const codeVerifier = oauth.generateRandomCodeVerifier();\n return { codeVerifier, codeChallenge: await oauth.calculatePKCECodeChallenge(codeVerifier) };\n },\n catch: () =>\n new Errors.DomainKitError({\n reason: new Reason.CryptoFailed({ operation: \"digest\" }),\n }),\n });\n\nexport const authorizationUrl = (input: {\n readonly server: Server;\n readonly clientId: string;\n readonly scopes: ReadonlyArray<string>;\n readonly state: string;\n readonly callbackUrl: string;\n readonly codeChallenge: string;\n}): string => {\n const url = new URL(input.server.authorization_endpoint);\n url.search = new URLSearchParams({\n client_id: input.clientId,\n code_challenge: input.codeChallenge,\n code_challenge_method: \"S256\",\n redirect_uri: input.callbackUrl,\n response_type: \"code\",\n scope: input.scopes.join(\" \"),\n state: input.state,\n }).toString();\n return url.toString();\n};\n\nexport interface Tokens {\n readonly accessToken: string;\n readonly refreshToken: string | null;\n readonly expiresAt: DateTime.Utc | null;\n readonly scope: string | null;\n}\n\nconst clientAuth = (client: Client): oauth.ClientAuth => {\n if (client.clientAuth === \"none\") return oauth.None();\n const secret = client.clientSecret === null ? \"\" : Redacted.value(client.clientSecret);\n return client.clientAuth === \"client_secret_basic\"\n ? oauth.ClientSecretBasic(secret)\n : oauth.ClientSecretPost(secret);\n};\n\nconst requestOptions = (\n fetch: Fetch | undefined,\n signal: AbortSignal,\n): oauth.TokenEndpointRequestOptions =>\n fetch === undefined\n ? { signal }\n : { signal, [oauth.customFetch]: (url, init) => fetch(url, { ...init, body: init.body }) };\n\nconst tokens = (response: oauth.TokenEndpointResponse, now: DateTime.Utc): Tokens => ({\n accessToken: response.access_token,\n refreshToken: response.refresh_token ?? null,\n expiresAt:\n typeof response.expires_in === \"number\"\n ? DateTime.add(now, { seconds: response.expires_in })\n : null,\n scope: response.scope ?? null,\n});\n\nconst failure = (provider: string, cause: unknown): Errors.DomainKitError => {\n if (cause instanceof oauth.ResponseBodyError) {\n const terminal = [\"invalid_grant\", \"invalid_client\", \"unauthorized_client\"].includes(\n cause.error,\n );\n return new Errors.DomainKitError({\n reason: terminal\n ? new Reason.Unauthenticated({\n message: `${provider} rejected the grant: ${cause.error}`,\n })\n : new Reason.ProviderRejected({\n provider,\n code: cause.error,\n message: cause.error_description ?? cause.error,\n }),\n });\n }\n if (cause instanceof oauth.AuthorizationResponseError) {\n return new Errors.DomainKitError({\n reason: new Reason.Unauthenticated({\n message: `${provider} authorization failed: ${cause.error}`,\n }),\n });\n }\n return new Errors.DomainKitError({\n reason: new Reason.ProviderUnavailable({\n provider,\n message: cause instanceof Error ? cause.message : `${provider} OAuth request failed`,\n }),\n });\n};\n\nexport const exchangeCode = (input: {\n readonly provider: string;\n readonly server: Server;\n readonly client: Client;\n readonly code: string;\n readonly state: string;\n readonly callbackUrl: string;\n readonly codeVerifier: string;\n readonly fetch?: Fetch;\n}): Effect.Effect<Tokens, Errors.DomainKitError> =>\n Effect.gen(function* () {\n const now = yield* DateTime.now;\n const server = input.server as oauth.AuthorizationServer;\n const client: oauth.Client = { client_id: input.client.clientId };\n const response = yield* Effect.tryPromise({\n try: async (signal) => {\n const params = oauth.validateAuthResponse(\n server,\n client,\n new URLSearchParams({ code: input.code, state: input.state }),\n input.state,\n );\n const reply = await oauth.authorizationCodeGrantRequest(\n server,\n client,\n clientAuth(input.client),\n params,\n input.callbackUrl,\n input.codeVerifier,\n requestOptions(input.fetch, signal),\n );\n return oauth.processAuthorizationCodeResponse(server, client, reply);\n },\n catch: (cause) => failure(input.provider, cause),\n });\n return tokens(response, now);\n });\n\nexport const refresh = (input: {\n readonly provider: string;\n readonly server: Server;\n readonly client: Client;\n readonly refreshToken: string;\n readonly fetch?: Fetch;\n}): Effect.Effect<Tokens, Errors.DomainKitError> =>\n Effect.gen(function* () {\n const now = yield* DateTime.now;\n const server = input.server as oauth.AuthorizationServer;\n const client: oauth.Client = { client_id: input.client.clientId };\n const response = yield* Effect.tryPromise({\n try: async (signal) => {\n const reply = await oauth.refreshTokenGrantRequest(\n server,\n client,\n clientAuth(input.client),\n input.refreshToken,\n requestOptions(input.fetch, signal),\n );\n return oauth.processRefreshTokenResponse(server, client, reply);\n },\n catch: (cause) => failure(input.provider, cause),\n });\n const next = tokens(response, now);\n return { ...next, refreshToken: next.refreshToken ?? input.refreshToken };\n });\n\nexport const revoke = (input: {\n readonly provider: string;\n readonly server: Server;\n readonly client: Client;\n readonly token: string;\n readonly fetch?: Fetch;\n}): Effect.Effect<void, Errors.DomainKitError> =>\n Effect.tryPromise({\n try: async (signal) => {\n const server = input.server as oauth.AuthorizationServer;\n const client: oauth.Client = { client_id: input.client.clientId };\n const reply = await oauth.revocationRequest(\n server,\n client,\n clientAuth(input.client),\n input.token,\n requestOptions(input.fetch, signal),\n );\n await oauth.processRevocationResponse(reply);\n },\n catch: (cause) => failure(input.provider, cause),\n });\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"oauth.mjs","names":["Errors.DomainKitError","Reason.CryptoFailed","Reason.Unauthenticated","Reason.ProviderRejected","Reason.ProviderUnavailable"],"sources":["../../src/internal/oauth.ts"],"sourcesContent":["import { DateTime, Effect, Redacted } from \"effect\";\nimport * as oauth from \"oauth4webapi\";\n\nimport * as Errors from \"./error.ts\";\nimport * as Reason from \"../Reason.ts\";\nimport type { Fetch } from \"./http.ts\";\n\nexport interface Server {\n readonly issuer: string;\n readonly authorization_endpoint: string;\n readonly token_endpoint: string;\n readonly revocation_endpoint?: string;\n}\n\nexport type ClientAuth = \"client_secret_basic\" | \"client_secret_post\" | \"none\";\n\nexport interface Client {\n readonly clientId: string;\n readonly clientSecret: Redacted.Redacted<string> | null;\n readonly clientAuth: ClientAuth;\n}\n\n/** A PKCE pair: the verifier stays in the continuation, the challenge goes to the provider. */\nexport const pkce = (): Effect.Effect<\n { readonly codeVerifier: string; readonly codeChallenge: string },\n Errors.DomainKitError\n> =>\n Effect.tryPromise({\n try: async () => {\n const codeVerifier = oauth.generateRandomCodeVerifier();\n return { codeVerifier, codeChallenge: await oauth.calculatePKCECodeChallenge(codeVerifier) };\n },\n catch: () =>\n new Errors.DomainKitError({\n reason: new Reason.CryptoFailed({ operation: \"digest\" }),\n }),\n });\n\nexport const authorizationUrl = (input: {\n readonly server: Server;\n readonly clientId: string;\n readonly scopes: ReadonlyArray<string>;\n readonly state: string;\n readonly callbackUrl: string;\n readonly codeChallenge: string;\n}): string => {\n const url = new URL(input.server.authorization_endpoint);\n url.search = new URLSearchParams({\n client_id: input.clientId,\n code_challenge: input.codeChallenge,\n code_challenge_method: \"S256\",\n redirect_uri: input.callbackUrl,\n response_type: \"code\",\n scope: input.scopes.join(\" \"),\n state: input.state,\n }).toString();\n return url.toString();\n};\n\nexport interface Tokens {\n readonly accessToken: string;\n readonly refreshToken: string | null;\n readonly expiresAt: DateTime.Utc | null;\n readonly scope: string | null;\n}\n\nconst clientAuth = (client: Client): oauth.ClientAuth => {\n if (client.clientAuth === \"none\") return oauth.None();\n const secret = client.clientSecret === null ? \"\" : Redacted.value(client.clientSecret);\n return client.clientAuth === \"client_secret_basic\"\n ? oauth.ClientSecretBasic(secret)\n : oauth.ClientSecretPost(secret);\n};\n\nconst plaintext = (endpoint: string | undefined): boolean =>\n endpoint !== undefined && URL.parse(endpoint)?.protocol === \"http:\";\n\n/**\n * A destination nothing can move: a loopback literal, or a name RFC 6761 reserves for one. Every\n * other name resolves wherever DNS or a hosts file says, so it is not evidence of anything.\n */\nconst loopback = (endpoint: string | undefined): boolean => {\n const url = endpoint === undefined ? null : URL.parse(endpoint);\n if (url === null || !plaintext(endpoint)) return false;\n const host = url.hostname.replace(/^\\[|\\]$/g, \"\");\n return (\n host === \"localhost\" ||\n host.endsWith(\".localhost\") ||\n host === \"::1\" ||\n /^127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/.test(host)\n );\n};\n\n/**\n * oauth4webapi refuses plaintext endpoints, which is right for every request that carries a client\n * secret, a code, or a token. Loopback is the one automatic exception: it goes nowhere and nothing\n * can repoint it. Any other plaintext endpoint takes a caller that said so out loud, because a name\n * like `host.docker.internal` resolves wherever the machine has been told to resolve it.\n */\nconst insecureAllowed = (endpoint: string | undefined, allowPlaintext: boolean | undefined) =>\n loopback(endpoint) || (allowPlaintext === true && plaintext(endpoint));\n\nconst requestOptions = (\n input: {\n readonly endpoint: string | undefined;\n readonly allowPlaintext: boolean | undefined;\n },\n fetch: Fetch | undefined,\n signal: AbortSignal,\n): oauth.TokenEndpointRequestOptions => ({\n signal,\n ...(insecureAllowed(input.endpoint, input.allowPlaintext)\n ? { [oauth.allowInsecureRequests]: true }\n : {}),\n ...(fetch === undefined\n ? {}\n : { [oauth.customFetch]: (url, init) => fetch(url, { ...init, body: init.body }) }),\n});\n\nconst tokens = (response: oauth.TokenEndpointResponse, now: DateTime.Utc): Tokens => ({\n accessToken: response.access_token,\n refreshToken: response.refresh_token ?? null,\n expiresAt:\n typeof response.expires_in === \"number\"\n ? DateTime.add(now, { seconds: response.expires_in })\n : null,\n scope: response.scope ?? null,\n});\n\nconst failure = (provider: string, cause: unknown): Errors.DomainKitError => {\n if (cause instanceof oauth.ResponseBodyError) {\n const terminal = [\"invalid_grant\", \"invalid_client\", \"unauthorized_client\"].includes(\n cause.error,\n );\n return new Errors.DomainKitError({\n reason: terminal\n ? new Reason.Unauthenticated({\n message: `${provider} rejected the grant: ${cause.error}`,\n })\n : new Reason.ProviderRejected({\n provider,\n code: cause.error,\n message: cause.error_description ?? cause.error,\n }),\n });\n }\n if (cause instanceof oauth.AuthorizationResponseError) {\n return new Errors.DomainKitError({\n reason: new Reason.Unauthenticated({\n message: `${provider} authorization failed: ${cause.error}`,\n }),\n });\n }\n return new Errors.DomainKitError({\n reason: new Reason.ProviderUnavailable({\n provider,\n message: cause instanceof Error ? cause.message : `${provider} OAuth request failed`,\n }),\n });\n};\n\nexport const exchangeCode = (input: {\n readonly provider: string;\n readonly server: Server;\n readonly client: Client;\n readonly code: string;\n readonly state: string;\n readonly callbackUrl: string;\n readonly codeVerifier: string;\n readonly fetch?: Fetch;\n /** Permit an `http:` endpoint that is not loopback. Development only; see `insecureAllowed`. */\n readonly allowPlaintext?: boolean;\n}): Effect.Effect<Tokens, Errors.DomainKitError> =>\n Effect.gen(function* () {\n const now = yield* DateTime.now;\n const server = input.server as oauth.AuthorizationServer;\n const client: oauth.Client = { client_id: input.client.clientId };\n const response = yield* Effect.tryPromise({\n try: async (signal) => {\n const params = oauth.validateAuthResponse(\n server,\n client,\n new URLSearchParams({ code: input.code, state: input.state }),\n input.state,\n );\n const reply = await oauth.authorizationCodeGrantRequest(\n server,\n client,\n clientAuth(input.client),\n params,\n input.callbackUrl,\n input.codeVerifier,\n requestOptions(\n { allowPlaintext: input.allowPlaintext, endpoint: input.server.token_endpoint },\n input.fetch,\n signal,\n ),\n );\n return oauth.processAuthorizationCodeResponse(server, client, reply);\n },\n catch: (cause) => failure(input.provider, cause),\n });\n return tokens(response, now);\n });\n\nexport const refresh = (input: {\n readonly provider: string;\n readonly server: Server;\n readonly client: Client;\n readonly refreshToken: string;\n readonly fetch?: Fetch;\n readonly allowPlaintext?: boolean;\n}): Effect.Effect<Tokens, Errors.DomainKitError> =>\n Effect.gen(function* () {\n const now = yield* DateTime.now;\n const server = input.server as oauth.AuthorizationServer;\n const client: oauth.Client = { client_id: input.client.clientId };\n const response = yield* Effect.tryPromise({\n try: async (signal) => {\n const reply = await oauth.refreshTokenGrantRequest(\n server,\n client,\n clientAuth(input.client),\n input.refreshToken,\n requestOptions(\n { allowPlaintext: input.allowPlaintext, endpoint: input.server.token_endpoint },\n input.fetch,\n signal,\n ),\n );\n return oauth.processRefreshTokenResponse(server, client, reply);\n },\n catch: (cause) => failure(input.provider, cause),\n });\n const next = tokens(response, now);\n return { ...next, refreshToken: next.refreshToken ?? input.refreshToken };\n });\n\nexport const revoke = (input: {\n readonly provider: string;\n readonly server: Server;\n readonly client: Client;\n readonly token: string;\n readonly fetch?: Fetch;\n readonly allowPlaintext?: boolean;\n}): Effect.Effect<void, Errors.DomainKitError> =>\n Effect.tryPromise({\n try: async (signal) => {\n const server = input.server as oauth.AuthorizationServer;\n const client: oauth.Client = { client_id: input.client.clientId };\n const reply = await oauth.revocationRequest(\n server,\n client,\n clientAuth(input.client),\n input.token,\n requestOptions(\n { allowPlaintext: input.allowPlaintext, endpoint: input.server.revocation_endpoint },\n input.fetch,\n signal,\n ),\n );\n await oauth.processRevocationResponse(reply);\n },\n catch: (cause) => failure(input.provider, cause),\n });\n"],"mappings":";;;;;;AAuBA,MAAa,aAIX,OAAO,WAAW;CAChB,KAAK,YAAY;EACf,MAAM,eAAe,MAAM,2BAA2B;EACtD,OAAO;GAAE;GAAc,eAAe,MAAM,MAAM,2BAA2B,YAAY;EAAE;CAC7F;CACA,aACE,IAAIA,eAAsB,EACxB,QAAQ,IAAIC,aAAoB,EAAE,WAAW,SAAS,CAAC,EACzD,CAAC;AACL,CAAC;AAEH,MAAa,oBAAoB,UAOnB;CACZ,MAAM,MAAM,IAAI,IAAI,MAAM,OAAO,sBAAsB;CACvD,IAAI,SAAS,IAAI,gBAAgB;EAC/B,WAAW,MAAM;EACjB,gBAAgB,MAAM;EACtB,uBAAuB;EACvB,cAAc,MAAM;EACpB,eAAe;EACf,OAAO,MAAM,OAAO,KAAK,GAAG;EAC5B,OAAO,MAAM;CACf,CAAC,CAAC,CAAC,SAAS;CACZ,OAAO,IAAI,SAAS;AACtB;AASA,MAAM,cAAc,WAAqC;CACvD,IAAI,OAAO,eAAe,QAAQ,OAAO,MAAM,KAAK;CACpD,MAAM,SAAS,OAAO,iBAAiB,OAAO,KAAK,SAAS,MAAM,OAAO,YAAY;CACrF,OAAO,OAAO,eAAe,wBACzB,MAAM,kBAAkB,MAAM,IAC9B,MAAM,iBAAiB,MAAM;AACnC;AAEA,MAAM,aAAa,aACjB,aAAa,KAAA,KAAa,IAAI,MAAM,QAAQ,CAAC,EAAE,aAAa;;;;;AAM9D,MAAM,YAAY,aAA0C;CAC1D,MAAM,MAAM,aAAa,KAAA,IAAY,OAAO,IAAI,MAAM,QAAQ;CAC9D,IAAI,QAAQ,QAAQ,CAAC,UAAU,QAAQ,GAAG,OAAO;CACjD,MAAM,OAAO,IAAI,SAAS,QAAQ,YAAY,EAAE;CAChD,OACE,SAAS,eACT,KAAK,SAAS,YAAY,KAC1B,SAAS,SACT,mCAAmC,KAAK,IAAI;AAEhD;;;;;;;AAQA,MAAM,mBAAmB,UAA8B,mBACrD,SAAS,QAAQ,KAAM,mBAAmB,QAAQ,UAAU,QAAQ;AAEtE,MAAM,kBACJ,OAIA,OACA,YACuC;CACvC;CACA,GAAI,gBAAgB,MAAM,UAAU,MAAM,cAAc,IACpD,GAAG,MAAM,wBAAwB,KAAK,IACtC,CAAC;CACL,GAAI,UAAU,KAAA,IACV,CAAC,IACD,GAAG,MAAM,eAAe,KAAK,SAAS,MAAM,KAAK;EAAE,GAAG;EAAM,MAAM,KAAK;CAAK,CAAC,EAAE;AACrF;AAEA,MAAM,UAAU,UAAuC,SAA+B;CACpF,aAAa,SAAS;CACtB,cAAc,SAAS,iBAAiB;CACxC,WACE,OAAO,SAAS,eAAe,WAC3B,SAAS,IAAI,KAAK,EAAE,SAAS,SAAS,WAAW,CAAC,IAClD;CACN,OAAO,SAAS,SAAS;AAC3B;AAEA,MAAM,WAAW,UAAkB,UAA0C;CAC3E,IAAI,iBAAiB,MAAM,mBAAmB;EAC5C,MAAM,WAAW;GAAC;GAAiB;GAAkB;EAAqB,CAAC,CAAC,SAC1E,MAAM,KACR;EACA,OAAO,IAAID,eAAsB,EAC/B,QAAQ,WACJ,IAAIE,gBAAuB,EACzB,SAAS,GAAG,SAAS,uBAAuB,MAAM,QACpD,CAAC,IACD,IAAIC,iBAAwB;GAC1B;GACA,MAAM,MAAM;GACZ,SAAS,MAAM,qBAAqB,MAAM;EAC5C,CAAC,EACP,CAAC;CACH;CACA,IAAI,iBAAiB,MAAM,4BACzB,OAAO,IAAIH,eAAsB,EAC/B,QAAQ,IAAIE,gBAAuB,EACjC,SAAS,GAAG,SAAS,yBAAyB,MAAM,QACtD,CAAC,EACH,CAAC;CAEH,OAAO,IAAIF,eAAsB,EAC/B,QAAQ,IAAII,oBAA2B;EACrC;EACA,SAAS,iBAAiB,QAAQ,MAAM,UAAU,GAAG,SAAS;CAChE,CAAC,EACH,CAAC;AACH;AAEA,MAAa,gBAAgB,UAY3B,OAAO,IAAI,aAAa;CACtB,MAAM,MAAM,OAAO,SAAS;CAC5B,MAAM,SAAS,MAAM;CACrB,MAAM,SAAuB,EAAE,WAAW,MAAM,OAAO,SAAS;CAChE,MAAM,WAAW,OAAO,OAAO,WAAW;EACxC,KAAK,OAAO,WAAW;GACrB,MAAM,SAAS,MAAM,qBACnB,QACA,QACA,IAAI,gBAAgB;IAAE,MAAM,MAAM;IAAM,OAAO,MAAM;GAAM,CAAC,GAC5D,MAAM,KACR;GACA,MAAM,QAAQ,MAAM,MAAM,8BACxB,QACA,QACA,WAAW,MAAM,MAAM,GACvB,QACA,MAAM,aACN,MAAM,cACN,eACE;IAAE,gBAAgB,MAAM;IAAgB,UAAU,MAAM,OAAO;GAAe,GAC9E,MAAM,OACN,MACF,CACF;GACA,OAAO,MAAM,iCAAiC,QAAQ,QAAQ,KAAK;EACrE;EACA,QAAQ,UAAU,QAAQ,MAAM,UAAU,KAAK;CACjD,CAAC;CACD,OAAO,OAAO,UAAU,GAAG;AAC7B,CAAC;AAEH,MAAa,WAAW,UAQtB,OAAO,IAAI,aAAa;CACtB,MAAM,MAAM,OAAO,SAAS;CAC5B,MAAM,SAAS,MAAM;CACrB,MAAM,SAAuB,EAAE,WAAW,MAAM,OAAO,SAAS;CAChE,MAAM,WAAW,OAAO,OAAO,WAAW;EACxC,KAAK,OAAO,WAAW;GACrB,MAAM,QAAQ,MAAM,MAAM,yBACxB,QACA,QACA,WAAW,MAAM,MAAM,GACvB,MAAM,cACN,eACE;IAAE,gBAAgB,MAAM;IAAgB,UAAU,MAAM,OAAO;GAAe,GAC9E,MAAM,OACN,MACF,CACF;GACA,OAAO,MAAM,4BAA4B,QAAQ,QAAQ,KAAK;EAChE;EACA,QAAQ,UAAU,QAAQ,MAAM,UAAU,KAAK;CACjD,CAAC;CACD,MAAM,OAAO,OAAO,UAAU,GAAG;CACjC,OAAO;EAAE,GAAG;EAAM,cAAc,KAAK,gBAAgB,MAAM;CAAa;AAC1E,CAAC;AAEH,MAAa,UAAU,UAQrB,OAAO,WAAW;CAChB,KAAK,OAAO,WAAW;EACrB,MAAM,SAAS,MAAM;EACrB,MAAM,SAAuB,EAAE,WAAW,MAAM,OAAO,SAAS;EAChE,MAAM,QAAQ,MAAM,MAAM,kBACxB,QACA,QACA,WAAW,MAAM,MAAM,GACvB,MAAM,OACN,eACE;GAAE,gBAAgB,MAAM;GAAgB,UAAU,MAAM,OAAO;EAAoB,GACnF,MAAM,OACN,MACF,CACF;EACA,MAAM,MAAM,0BAA0B,KAAK;CAC7C;CACA,QAAQ,UAAU,QAAQ,MAAM,UAAU,KAAK;AACjD,CAAC"}
|
package/dist/package.mjs
CHANGED
|
@@ -28,6 +28,27 @@ export interface Options {
|
|
|
28
28
|
readonly scopes?: ReadonlyArray<string>;
|
|
29
29
|
/** Default `client_secret_basic`. */
|
|
30
30
|
readonly clientAuth?: OAuth.ClientAuth;
|
|
31
|
+
/**
|
|
32
|
+
* Origin for consent, token exchange, and revocation, which Cloudflare serves at
|
|
33
|
+
* `/oauth2/auth`, `/oauth2/token`, and `/oauth2/revoke` beneath it. Default
|
|
34
|
+
* `https://dash.cloudflare.com`; a stage points it at an emulator that mounts the same three
|
|
35
|
+
* paths. It stays separate from `baseUrl` because in production these are different hosts:
|
|
36
|
+
* `dash.cloudflare.com` for OAuth, `api.cloudflare.com/client/v4` for the REST API.
|
|
37
|
+
*/
|
|
38
|
+
readonly issuer?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Where the server reaches the OAuth endpoints when that differs from where the browser does,
|
|
41
|
+
* for example an API in a container reaching an emulator through `host.docker.internal`.
|
|
42
|
+
* Defaults to `issuer`.
|
|
43
|
+
*/
|
|
44
|
+
readonly serverOrigin?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Permit `http:` for these endpoints when they are not loopback, such as an emulator reached
|
|
47
|
+
* through `host.docker.internal`. Development only: the client secret, the code, and the tokens
|
|
48
|
+
* cross the network in the clear, and a name resolves wherever the machine has been told to.
|
|
49
|
+
* Loopback needs no flag.
|
|
50
|
+
*/
|
|
51
|
+
readonly allowPlaintext?: boolean;
|
|
31
52
|
};
|
|
32
53
|
readonly fetch?: Fetch;
|
|
33
54
|
readonly baseUrl?: string;
|
package/dist/types/Connect.d.ts
CHANGED
|
@@ -113,6 +113,8 @@ export interface Snapshot {
|
|
|
113
113
|
readonly connection: Storage.Connection | null;
|
|
114
114
|
readonly authorization: Pick<Storage.Authorization, "id" | "provider" | "method" | "capabilities" | "revocation"> | null;
|
|
115
115
|
readonly lastReceiptId: string | null;
|
|
116
|
+
/** How many domains this domain's connection serves, including this one; `0` without one. */
|
|
117
|
+
readonly connectionDomains: number;
|
|
116
118
|
/** Other connections of this owner that could serve this domain. */
|
|
117
119
|
readonly reusable: ReadonlyArray<{
|
|
118
120
|
readonly connection: Storage.Connection;
|
|
@@ -283,14 +285,24 @@ export type Discovery = Data.TaggedEnum<{
|
|
|
283
285
|
readonly target: Provider.Target;
|
|
284
286
|
}>;
|
|
285
287
|
};
|
|
288
|
+
/**
|
|
289
|
+
* No connection reaches the domain. `host` names the one registered provider whose declared
|
|
290
|
+
* nameserver suffixes cover every authoritative nameserver; `null` when none or several do.
|
|
291
|
+
*/
|
|
286
292
|
NotFound: {
|
|
287
293
|
readonly nameservers: ReadonlyArray<string>;
|
|
294
|
+
readonly host: {
|
|
295
|
+
readonly provider: string;
|
|
296
|
+
} | null;
|
|
288
297
|
};
|
|
289
298
|
}>;
|
|
290
299
|
export declare const Discovery: {
|
|
291
300
|
readonly $is: <Tag extends "NotFound" | "Resolved" | "SelectionRequired">(tag: Tag) => (u: unknown) => u is Extract<{
|
|
292
301
|
readonly _tag: "NotFound";
|
|
293
302
|
readonly nameservers: ReadonlyArray<string>;
|
|
303
|
+
readonly host: {
|
|
304
|
+
readonly provider: string;
|
|
305
|
+
} | null;
|
|
294
306
|
}, {
|
|
295
307
|
readonly _tag: Tag;
|
|
296
308
|
}> | Extract<{
|
|
@@ -312,6 +324,9 @@ export declare const Discovery: {
|
|
|
312
324
|
<Cases extends { readonly [Tag in "NotFound" | "Resolved" | "SelectionRequired"]: (args: Extract<{
|
|
313
325
|
readonly _tag: "NotFound";
|
|
314
326
|
readonly nameservers: ReadonlyArray<string>;
|
|
327
|
+
readonly host: {
|
|
328
|
+
readonly provider: string;
|
|
329
|
+
} | null;
|
|
315
330
|
} | {
|
|
316
331
|
readonly _tag: "Resolved";
|
|
317
332
|
readonly connectionId: string;
|
|
@@ -327,6 +342,9 @@ export declare const Discovery: {
|
|
|
327
342
|
}>) => any; }>(cases: Cases): (value: {
|
|
328
343
|
readonly _tag: "NotFound";
|
|
329
344
|
readonly nameservers: ReadonlyArray<string>;
|
|
345
|
+
readonly host: {
|
|
346
|
+
readonly provider: string;
|
|
347
|
+
} | null;
|
|
330
348
|
} | {
|
|
331
349
|
readonly _tag: "Resolved";
|
|
332
350
|
readonly connectionId: string;
|
|
@@ -341,6 +359,9 @@ export declare const Discovery: {
|
|
|
341
359
|
<Cases extends { readonly [Tag in "NotFound" | "Resolved" | "SelectionRequired"]: (args: Extract<{
|
|
342
360
|
readonly _tag: "NotFound";
|
|
343
361
|
readonly nameservers: ReadonlyArray<string>;
|
|
362
|
+
readonly host: {
|
|
363
|
+
readonly provider: string;
|
|
364
|
+
} | null;
|
|
344
365
|
} | {
|
|
345
366
|
readonly _tag: "Resolved";
|
|
346
367
|
readonly connectionId: string;
|
|
@@ -356,6 +377,9 @@ export declare const Discovery: {
|
|
|
356
377
|
}>) => any; }>(value: {
|
|
357
378
|
readonly _tag: "NotFound";
|
|
358
379
|
readonly nameservers: ReadonlyArray<string>;
|
|
380
|
+
readonly host: {
|
|
381
|
+
readonly provider: string;
|
|
382
|
+
} | null;
|
|
359
383
|
} | {
|
|
360
384
|
readonly _tag: "Resolved";
|
|
361
385
|
readonly connectionId: string;
|
|
@@ -371,6 +395,9 @@ export declare const Discovery: {
|
|
|
371
395
|
readonly NotFound: Data.TaggedEnum.ConstructorFrom<{
|
|
372
396
|
readonly _tag: "NotFound";
|
|
373
397
|
readonly nameservers: ReadonlyArray<string>;
|
|
398
|
+
readonly host: {
|
|
399
|
+
readonly provider: string;
|
|
400
|
+
} | null;
|
|
374
401
|
}, "_tag">;
|
|
375
402
|
readonly Resolved: Data.TaggedEnum.ConstructorFrom<{
|
|
376
403
|
readonly _tag: "Resolved";
|
|
@@ -446,6 +473,9 @@ export declare const inspect: (domain: string) => Effect.Effect<Snapshot, Errors
|
|
|
446
473
|
export declare const discover: (domain: string) => Effect.Effect<{
|
|
447
474
|
readonly _tag: "NotFound";
|
|
448
475
|
readonly nameservers: ReadonlyArray<string>;
|
|
476
|
+
readonly host: {
|
|
477
|
+
readonly provider: string;
|
|
478
|
+
} | null;
|
|
449
479
|
} | {
|
|
450
480
|
readonly _tag: "Resolved";
|
|
451
481
|
readonly connectionId: string;
|