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.
@@ -1 +1 @@
1
- {"version":3,"file":"client.mjs","names":["Reason.NotFound","Errors.fail","S","Protocol.BaseEnvelope","Reason.ProviderConflict","Errors.decode","Protocol.Envelope","Protocol.Zone","Protocol.Token","Reason.Unauthenticated","Protocol.Record","Records.isWithinZone","Records.decode","Records.encode"],"sources":["../../../src/internal/cloudflare/client.ts"],"sourcesContent":["import { DateTime, Effect, Schema as S } from \"effect\";\n\nimport type * as DnsRecord from \"../../DnsRecord.ts\";\nimport * as Errors from \"../error.ts\";\nimport * as Reason from \"../../Reason.ts\";\nimport type * as Provider from \"../../Provider.ts\";\nimport { bearer, classify, type Fetch, rejected, requestJson } from \"../http.ts\";\nimport * as Protocol from \"./protocol.ts\";\nimport * as Records from \"./records.ts\";\n\nexport const provider = \"cloudflare\";\n\nconst notFound = new Reason.NotFound({ entity: \"zone\", id: \"cloudflare\" });\n\n/** Cloudflare's \"record already exists\" family (81056 identical, 81057 same name and content, 81058 CNAME). */\nconst conflictCodes = new Set([81056, 81057, 81058]);\n\nexport interface Options {\n readonly token: string;\n readonly fetch: Fetch;\n readonly baseUrl: string;\n}\n\ntype Fx<A> = Effect.Effect<A, Errors.DomainKitError>;\n\nconst call = <R extends S.ConstraintDecoder<unknown>>(\n options: Options,\n path: string,\n result: R,\n init?: RequestInit,\n): Fx<{ readonly result: R[\"Type\"]; readonly totalPages: number | undefined }> =>\n Effect.gen(function* () {\n const reply = yield* requestJson({\n fetch: options.fetch,\n provider,\n url: `${options.baseUrl}${path}`,\n init: bearer(options.token, init),\n });\n if (reply.status === 404) return yield* Errors.fail(notFound);\n const base = S.decodeUnknownOption(Protocol.BaseEnvelope)(reply.body);\n if (!reply.ok || base._tag === \"None\" || !base.value.success) {\n const detail = base._tag === \"Some\" ? base.value.errors[0] : undefined;\n if (detail !== undefined && conflictCodes.has(detail.code)) {\n return yield* Errors.fail(\n new Reason.ProviderConflict({\n provider,\n code: String(detail.code),\n message: detail.message,\n }),\n );\n }\n return yield* Errors.fail(\n classify(\n provider,\n reply.status,\n reply.headers,\n detail?.message ?? `Cloudflare request failed with HTTP ${reply.status}`,\n detail === undefined ? undefined : String(detail.code),\n ),\n );\n }\n const contract = () =>\n rejected(provider, \"Cloudflare response did not match its API contract\", \"response\");\n const envelope = yield* Errors.decode(Protocol.Envelope, reply.body).pipe(\n Effect.mapError(contract),\n );\n const decoded: R[\"Type\"] = yield* Errors.decode(result, envelope.result).pipe(\n Effect.mapError(contract),\n );\n return { result: decoded, totalPages: envelope.result_info?.total_pages };\n });\n\nconst paginate = <R extends S.ConstraintDecoder<unknown>>(\n options: Options,\n path: (page: number) => string,\n result: R,\n perPage: number,\n): Fx<ReadonlyArray<R[\"Type\"]>> =>\n Effect.gen(function* () {\n const rows: Array<R[\"Type\"]> = [];\n let page = 1;\n while (true) {\n const reply = yield* call(options, path(page), S.Array(result));\n rows.push(...reply.result);\n const last =\n reply.totalPages === undefined ? reply.result.length < perPage : page >= reply.totalPages;\n if (last) return rows;\n page += 1;\n }\n });\n\n/** Every zone the credential can see, across accounts; internal zones cannot host records. */\nexport const listZones = (options: Options, accountId?: string): Fx<ReadonlyArray<Protocol.Zone>> =>\n paginate(\n options,\n (page) => {\n const query = new URLSearchParams({ page: String(page), per_page: \"50\" });\n if (accountId !== undefined) query.set(\"account.id\", accountId);\n return `/zones?${query}`;\n },\n Protocol.Zone,\n 50,\n ).pipe(Effect.map((zones) => zones.filter((zone) => zone.type !== \"internal\")));\n\n/** Token expiry from the verify endpoint; `null` when the token never expires or verification is unavailable. */\n/** Token status and expiry from a verify endpoint; an inactive token is `Unauthenticated`. */\nexport const verifyToken = (options: Options, path: string): Fx<DateTime.Utc | null> =>\n call(options, path, Protocol.Token).pipe(\n Effect.flatMap(({ result }) =>\n result.status === \"active\"\n ? Effect.succeed(\n typeof result.expires_on === \"string\" ? DateTime.makeUnsafe(result.expires_on) : null,\n )\n : Errors.fail(\n new Reason.Unauthenticated({\n message: `Cloudflare token is ${result.status}`,\n }),\n ),\n ),\n );\n\nexport const tokenExpiry = (\n options: Options,\n accountId: string | null,\n): Fx<{ readonly expiresAt: DateTime.Utc | null; readonly kind: \"user\" | \"account\" }> => {\n const verify = (path: string) =>\n call(options, path, Protocol.Token).pipe(\n Effect.flatMap(({ result }) =>\n result.status === \"active\"\n ? Effect.succeed(\n typeof result.expires_on === \"string\" ? DateTime.makeUnsafe(result.expires_on) : null,\n )\n : Errors.fail(\n new Reason.Unauthenticated({\n message: `Cloudflare token is ${result.status}`,\n }),\n ),\n ),\n );\n return verify(\"/user/tokens/verify\").pipe(\n Effect.map((expiresAt) => ({ expiresAt, kind: \"user\" as const })),\n Effect.catchIf(\n (error) => error.reason._tag !== \"Unauthenticated\" || accountId !== null,\n () =>\n accountId === null\n ? Effect.succeed({ expiresAt: null, kind: \"user\" as const })\n : verify(`/accounts/${encodeURIComponent(accountId)}/tokens/verify`).pipe(\n Effect.map((expiresAt) => ({ expiresAt, kind: \"account\" as const })),\n Effect.catchIf(\n (error) => error.reason._tag !== \"Unauthenticated\",\n () => Effect.succeed({ expiresAt: null, kind: \"user\" as const }),\n ),\n ),\n ),\n );\n};\n\nexport const dns = (options: Options, zoneId: string): Provider.Dns => {\n const base = `/zones/${encodeURIComponent(zoneId)}/dns_records`;\n return {\n list: (zone) =>\n paginate(\n options,\n (page) => `${base}?${new URLSearchParams({ page: String(page), per_page: \"100\" })}`,\n Protocol.Record,\n 100,\n ).pipe(\n Effect.flatMap((records) =>\n Effect.forEach(\n records.filter((record) => Records.isWithinZone(record.name, zone)),\n Records.decode,\n ),\n ),\n ),\n create: (_zone, record: DnsRecord.Model) =>\n call(options, base, Protocol.Record, {\n method: \"POST\",\n body: JSON.stringify(Records.encode(record)),\n }).pipe(Effect.map(({ result }) => ({ providerRecordId: result.id }))),\n get: (_zone, providerRecordId) =>\n call(options, `${base}/${encodeURIComponent(providerRecordId)}`, Protocol.Record).pipe(\n Effect.flatMap(({ result }) => Records.decode(result)),\n Effect.map(({ record }) => record),\n Effect.catchIf(\n (error) => error.reason === notFound,\n () => Effect.succeed(null),\n ),\n ),\n delete: (_zone, providerRecordId) =>\n call(options, `${base}/${encodeURIComponent(providerRecordId)}`, S.Unknown, {\n method: \"DELETE\",\n }).pipe(Effect.asVoid),\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.mjs","names":["Reason.NotFound","Errors.fail","S","Protocol.BaseEnvelope","Reason.Unauthenticated","Reason.ProviderConflict","Errors.decode","Protocol.Envelope","Protocol.Zone","Protocol.Token","Protocol.Record","Records.isWithinZone","Records.decode","Records.encode"],"sources":["../../../src/internal/cloudflare/client.ts"],"sourcesContent":["import { DateTime, Effect, Schema as S } from \"effect\";\n\nimport type * as DnsRecord from \"../../DnsRecord.ts\";\nimport * as Errors from \"../error.ts\";\nimport * as Reason from \"../../Reason.ts\";\nimport type * as Provider from \"../../Provider.ts\";\nimport { bearer, classify, type Fetch, rejected, rejectedToken, requestJson } from \"../http.ts\";\nimport * as Protocol from \"./protocol.ts\";\nimport * as Records from \"./records.ts\";\n\nexport const provider = \"cloudflare\";\n\nconst notFound = new Reason.NotFound({ entity: \"zone\", id: \"cloudflare\" });\n\n/** Cloudflare's \"record already exists\" family (81056 identical, 81057 same name and content, 81058 CNAME). */\nconst conflictCodes = new Set([81056, 81057, 81058]);\n\n/** Cloudflare answers a malformed bearer token with HTTP 400: 6003 invalid request headers, 6111 invalid Authorization format. */\nconst rejectedTokenCodes = new Set([6003, 6111]);\n\nexport interface Options {\n readonly token: string;\n readonly fetch: Fetch;\n readonly baseUrl: string;\n}\n\ntype Fx<A> = Effect.Effect<A, Errors.DomainKitError>;\n\nconst call = <R extends S.ConstraintDecoder<unknown>>(\n options: Options,\n path: string,\n result: R,\n init?: RequestInit,\n): Fx<{ readonly result: R[\"Type\"]; readonly totalPages: number | undefined }> =>\n Effect.gen(function* () {\n const reply = yield* requestJson({\n fetch: options.fetch,\n provider,\n url: `${options.baseUrl}${path}`,\n init: bearer(options.token, init),\n });\n if (reply.status === 404) return yield* Errors.fail(notFound);\n const base = S.decodeUnknownOption(Protocol.BaseEnvelope)(reply.body);\n if (!reply.ok || base._tag === \"None\" || !base.value.success) {\n const detail = base._tag === \"Some\" ? base.value.errors[0] : undefined;\n if (detail !== undefined && rejectedTokenCodes.has(detail.code)) {\n return yield* Errors.fail(new Reason.Unauthenticated({ message: detail.message }));\n }\n if (detail !== undefined && conflictCodes.has(detail.code)) {\n return yield* Errors.fail(\n new Reason.ProviderConflict({\n provider,\n code: String(detail.code),\n message: detail.message,\n }),\n );\n }\n return yield* Errors.fail(\n classify(\n provider,\n reply.status,\n reply.headers,\n detail?.message ?? `Cloudflare request failed with HTTP ${reply.status}`,\n detail === undefined ? undefined : String(detail.code),\n ),\n );\n }\n const contract = () =>\n rejected(provider, \"Cloudflare response did not match its API contract\", \"response\");\n const envelope = yield* Errors.decode(Protocol.Envelope, reply.body).pipe(\n Effect.mapError(contract),\n );\n const decoded: R[\"Type\"] = yield* Errors.decode(result, envelope.result).pipe(\n Effect.mapError(contract),\n );\n return { result: decoded, totalPages: envelope.result_info?.total_pages };\n });\n\nconst paginate = <R extends S.ConstraintDecoder<unknown>>(\n options: Options,\n path: (page: number) => string,\n result: R,\n perPage: number,\n): Fx<ReadonlyArray<R[\"Type\"]>> =>\n Effect.gen(function* () {\n const rows: Array<R[\"Type\"]> = [];\n let page = 1;\n while (true) {\n const reply = yield* call(options, path(page), S.Array(result));\n rows.push(...reply.result);\n const last =\n reply.totalPages === undefined ? reply.result.length < perPage : page >= reply.totalPages;\n if (last) return rows;\n page += 1;\n }\n });\n\n/** Every zone the credential can see, across accounts; internal zones cannot host records. */\nexport const listZones = (options: Options, accountId?: string): Fx<ReadonlyArray<Protocol.Zone>> =>\n paginate(\n options,\n (page) => {\n const query = new URLSearchParams({ page: String(page), per_page: \"50\" });\n if (accountId !== undefined) query.set(\"account.id\", accountId);\n return `/zones?${query}`;\n },\n Protocol.Zone,\n 50,\n ).pipe(Effect.map((zones) => zones.filter((zone) => zone.type !== \"internal\")));\n\n/**\n * Token status and expiry from a verify endpoint; `null` expiry means the token never expires. An\n * inactive token, or a 403 from the verify endpoint, is `Unauthenticated`.\n */\nexport const verifyToken = (options: Options, path: string): Fx<DateTime.Utc | null> =>\n call(options, path, Protocol.Token).pipe(\n Effect.flatMap(({ result }) =>\n result.status === \"active\"\n ? Effect.succeed(\n typeof result.expires_on === \"string\" ? DateTime.makeUnsafe(result.expires_on) : null,\n )\n : Errors.fail(\n new Reason.Unauthenticated({\n message: `Cloudflare token is ${result.status}`,\n }),\n ),\n ),\n rejectedToken,\n );\n\nexport const tokenExpiry = (\n options: Options,\n accountId: string | null,\n): Fx<{ readonly expiresAt: DateTime.Utc | null; readonly kind: \"user\" | \"account\" }> => {\n const verify = (path: string) => verifyToken(options, path);\n return verify(\"/user/tokens/verify\").pipe(\n Effect.map((expiresAt) => ({ expiresAt, kind: \"user\" as const })),\n Effect.catchIf(\n (error) => error.reason._tag !== \"Unauthenticated\" || accountId !== null,\n () =>\n accountId === null\n ? Effect.succeed({ expiresAt: null, kind: \"user\" as const })\n : verify(`/accounts/${encodeURIComponent(accountId)}/tokens/verify`).pipe(\n Effect.map((expiresAt) => ({ expiresAt, kind: \"account\" as const })),\n Effect.catchIf(\n (error) => error.reason._tag !== \"Unauthenticated\",\n () => Effect.succeed({ expiresAt: null, kind: \"user\" as const }),\n ),\n ),\n ),\n );\n};\n\nexport const dns = (options: Options, zoneId: string): Provider.Dns => {\n const base = `/zones/${encodeURIComponent(zoneId)}/dns_records`;\n return {\n list: (zone) =>\n paginate(\n options,\n (page) => `${base}?${new URLSearchParams({ page: String(page), per_page: \"100\" })}`,\n Protocol.Record,\n 100,\n ).pipe(\n Effect.flatMap((records) =>\n Effect.forEach(\n records.filter((record) => Records.isWithinZone(record.name, zone)),\n Records.decode,\n ),\n ),\n ),\n create: (_zone, record: DnsRecord.Model) =>\n call(options, base, Protocol.Record, {\n method: \"POST\",\n body: JSON.stringify(Records.encode(record)),\n }).pipe(Effect.map(({ result }) => ({ providerRecordId: result.id }))),\n get: (_zone, providerRecordId) =>\n call(options, `${base}/${encodeURIComponent(providerRecordId)}`, Protocol.Record).pipe(\n Effect.flatMap(({ result }) => Records.decode(result)),\n Effect.map(({ record }) => record),\n Effect.catchIf(\n (error) => error.reason === notFound,\n () => Effect.succeed(null),\n ),\n ),\n delete: (_zone, providerRecordId) =>\n call(options, `${base}/${encodeURIComponent(providerRecordId)}`, S.Unknown, {\n method: \"DELETE\",\n }).pipe(Effect.asVoid),\n };\n};\n"],"mappings":"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"}
@@ -4,7 +4,7 @@ import { DomainKitError, fail } from "../error.mjs";
4
4
  import { Service } from "../../Custody.mjs";
5
5
  import { fresh } from "../ids.mjs";
6
6
  import { Service as Service$1, make } from "../../Principal.mjs";
7
- import { encodeContext } from "../../Provider.mjs";
7
+ import { encodeContext, tokenFields } from "../../Provider.mjs";
8
8
  import { Service as Service$2 } from "../../Resolver.mjs";
9
9
  import { applied } from "../../Receipt.mjs";
10
10
  import { Attachment, Authorization, Credential, Service as Service$3 } from "../../Storage.mjs";
@@ -12,7 +12,7 @@ import { attach, session } from "../../Connect.mjs";
12
12
  import { apply, approve, plan } from "../../Provision.mjs";
13
13
  import { apply as apply$1, approve as approve$1, plan as plan$1 } from "../../Cleanup.mjs";
14
14
  import { layerMemory } from "../../DomainKit.mjs";
15
- import { DateTime, Effect, Layer } from "effect";
15
+ import { DateTime, Effect, Layer, Redacted } from "effect";
16
16
  //#region src/internal/conformance/provider.ts
17
17
  const principal = make({
18
18
  ownerId: "conformance",
@@ -67,8 +67,9 @@ const txt = (zone, label, value) => txt$1({
67
67
  });
68
68
  const recordIds = (receipt) => applied(receipt).map(({ providerRecordId }) => providerRecordId);
69
69
  /**
70
- * Runs create/readback/cleanup, exact-noop, conflict, stale-plan, and partial-apply against a
71
- * real provider definition, through the same Provision and Cleanup services hosts use.
70
+ * Runs create/readback/cleanup, exact-noop, conflict, stale-plan, partial-apply, and
71
+ * rejected-token against a real provider definition, through the same Provision and Cleanup
72
+ * services hosts use.
72
73
  */
73
74
  const provider = (definition, credential, zone, options = {}) => {
74
75
  const prefix = options.prefix ?? "domainkit-conformance";
@@ -86,12 +87,12 @@ const provider = (definition, credential, zone, options = {}) => {
86
87
  const name = "create-readback-cleanup";
87
88
  return Effect.gen(function* () {
88
89
  const requirements = [cname(zone, `${prefix}-create`), txt(zone, `${prefix}-page`, "domainkit")];
89
- const plan$2 = yield* plan({
90
+ const plan$4 = yield* plan({
90
91
  domain: attachment.domain,
91
92
  requirements
92
93
  });
93
- yield* expect(name, plan$2.operations.every(({ _tag }) => _tag === "Create"), "expected two Create operations");
94
- const receipt = yield* apply(yield* approve(plan$2));
94
+ yield* expect(name, plan$4.operations.every(({ _tag }) => _tag === "Create"), "expected two Create operations");
95
+ const receipt = yield* apply(yield* approve(plan$4));
95
96
  created = recordIds(receipt);
96
97
  yield* expect(name, receipt.status === "complete" && created.length === 2, "apply must return a complete receipt with provider record ids");
97
98
  const observed = yield* dns.list(zone);
@@ -111,12 +112,12 @@ const provider = (definition, credential, zone, options = {}) => {
111
112
  const requirement = cname(zone, `${prefix}-exact`);
112
113
  const { providerRecordId } = yield* dns.create(zone, requirement);
113
114
  created = [providerRecordId];
114
- const plan$3 = yield* plan({
115
+ const plan$5 = yield* plan({
115
116
  domain: attachment.domain,
116
117
  requirements: [requirement]
117
118
  });
118
- yield* expect(name, plan$3.operations[0]?._tag === "Noop", "an exact record must plan as Noop");
119
- const receipt = yield* apply(yield* approve(plan$3));
119
+ yield* expect(name, plan$5.operations[0]?._tag === "Noop", "an exact record must plan as Noop");
120
+ const receipt = yield* apply(yield* approve(plan$5));
120
121
  yield* expect(name, applied(receipt).length === 0, "an exact plan must not create another record");
121
122
  }).pipe(Effect.ensuring(cleanup(dns, () => created)));
122
123
  });
@@ -126,12 +127,12 @@ const provider = (definition, credential, zone, options = {}) => {
126
127
  return Effect.gen(function* () {
127
128
  const { providerRecordId } = yield* dns.create(zone, txt(zone, `${prefix}-conflict`, "occupied"));
128
129
  created = [providerRecordId];
129
- const plan$6 = yield* plan({
130
+ const plan$3 = yield* plan({
130
131
  domain: attachment.domain,
131
132
  requirements: [cname(zone, `${prefix}-conflict`)]
132
133
  });
133
- yield* expect(name, plan$6.operations[0]?._tag === "Conflict", "a CNAME over an occupied name must plan as Conflict");
134
- const rejected = yield* approve(plan$6).pipe(Effect.result);
134
+ yield* expect(name, plan$3.operations[0]?._tag === "Conflict", "a CNAME over an occupied name must plan as Conflict");
135
+ const rejected = yield* approve(plan$3).pipe(Effect.result);
135
136
  yield* expect(name, rejected._tag === "Failure" && rejected.failure.reason._tag === "Conflict", "approve must fail closed on conflicts");
136
137
  }).pipe(Effect.ensuring(cleanup(dns, () => created)));
137
138
  });
@@ -140,11 +141,11 @@ const provider = (definition, credential, zone, options = {}) => {
140
141
  const name = "stale-plan";
141
142
  return Effect.gen(function* () {
142
143
  const requirement = cname(zone, `${prefix}-stale`);
143
- const plan$4 = yield* plan({
144
+ const plan$6 = yield* plan({
144
145
  domain: attachment.domain,
145
146
  requirements: [requirement]
146
147
  });
147
- const approval = yield* approve(plan$4);
148
+ const approval = yield* approve(plan$6);
148
149
  const { providerRecordId } = yield* dns.create(zone, requirement);
149
150
  created = [providerRecordId];
150
151
  const stale = yield* apply(approval).pipe(Effect.result);
@@ -177,11 +178,11 @@ const provider = (definition, credential, zone, options = {}) => {
177
178
  let created = [];
178
179
  const name = "partial-apply-cleanup";
179
180
  return Effect.gen(function* () {
180
- const plan$5 = yield* plan({
181
+ const plan$2 = yield* plan({
181
182
  domain: attachment.domain,
182
183
  requirements: [cname(zone, `${prefix}-partial-one`), txt(zone, `${prefix}-partial-two`, "domainkit")]
183
184
  });
184
- const receipt = yield* apply(yield* approve(plan$5));
185
+ const receipt = yield* apply(yield* approve(plan$2));
185
186
  created = recordIds(receipt);
186
187
  yield* expect(name, receipt.status === "partial" && created.length === 1, "a later write failure must preserve the first confirmed write in a partial receipt");
187
188
  const cleanupPlan = yield* plan$1({ receiptId: receipt.id });
@@ -191,12 +192,24 @@ const provider = (definition, credential, zone, options = {}) => {
191
192
  created = [];
192
193
  }).pipe(Effect.ensuring(cleanup(dns, () => created)));
193
194
  });
195
+ /** A token the provider rejects must read as `Unauthenticated`, so a host tells the customer the provider refused it. */
196
+ const rejectedToken = Effect.gen(function* () {
197
+ const name = "rejected-token";
198
+ const auth = definition.auth.token;
199
+ if (auth === void 0) return;
200
+ const required = tokenFields(auth).filter((field) => field.required);
201
+ if (options.rejectedToken === void 0 && required.some((field) => !field.secret)) return yield* Effect.fail(failure(name, `the token method requires ${required.filter((field) => !field.secret).map((field) => field.name).join(", ")}; pass options.rejectedToken with values the provider rejects`));
202
+ const values = options.rejectedToken ?? Object.fromEntries(required.map((field) => [field.name, Redacted.make("")]));
203
+ const outcome = yield* auth.authenticate(values).pipe(Effect.result);
204
+ yield* expect(name, outcome._tag === "Failure" && outcome.failure.reason._tag === "Unauthenticated", "authenticating a rejected token must fail Unauthenticated");
205
+ });
194
206
  return Effect.all([
195
207
  createReadbackCleanup,
196
208
  exactNoop,
197
209
  conflict,
198
210
  stalePlan,
199
- partialApplyCleanup
211
+ partialApplyCleanup,
212
+ rejectedToken
200
213
  ], { discard: true });
201
214
  };
202
215
  /** Verification is out of scope for the provider contract; keep the pool quiet. */
@@ -1 +1 @@
1
- {"version":3,"file":"provider.mjs","names":["Principal.make","Errors.DomainKitError","Reason.InvalidInput","Storage.Service","Custody.Service","Storage.Authorization","Provider.encodeContext","Storage.Credential","Connect.attach","Storage.Attachment","DnsRecord.cname","DnsRecord.txt","Receipt.applied","Connect.session","session","Principal.Service","DomainKit.layerMemory","Resolver.Service","plan","Provision.plan","Provision.apply","Provision.approve","DnsRecord.equals","Cleanup.plan","Cleanup.apply","Cleanup.approve","Errors.fail","Reason.ProviderUnavailable"],"sources":["../../../src/internal/conformance/provider.ts"],"sourcesContent":["import { DateTime, Effect, Layer } from \"effect\";\n\nimport * as Cleanup from \"../../Cleanup.ts\";\nimport * as Connect from \"../../Connect.ts\";\nimport * as Custody from \"../../Custody.ts\";\nimport * as DnsRecord from \"../../DnsRecord.ts\";\nimport * as Errors from \"../error.ts\";\nimport * as Reason from \"../../Reason.ts\";\nimport * as DomainKit from \"../../DomainKit.ts\";\nimport * as Principal from \"../../Principal.ts\";\nimport * as Provider from \"../../Provider.ts\";\nimport * as Provision from \"../../Provision.ts\";\nimport * as Receipt from \"../../Receipt.ts\";\nimport * as Resolver from \"../../Resolver.ts\";\nimport * as Storage from \"../../Storage.ts\";\nimport { fresh } from \"../ids.ts\";\n\nexport const cases = [\n \"create-readback-cleanup\",\n \"exact-noop\",\n \"conflict\",\n \"stale-plan\",\n \"partial-apply-cleanup\",\n] as const;\nexport type Case = (typeof cases)[number];\n\nconst principal = Principal.make({ ownerId: \"conformance\", actorId: \"conformance\" });\n\nconst failure = (name: Case, message: string) =>\n new Errors.DomainKitError({\n reason: new Reason.InvalidInput({\n message: `provider conformance ${name}: ${message}`,\n field: \"conformance\",\n }),\n });\n\nconst expect = (name: Case, condition: boolean, message: string) =>\n condition ? Effect.void : Effect.fail(failure(name, message));\n\ntype Env = Storage.Service | Connect.Service | Provision.Service | Cleanup.Service;\n\n/** Attach `zone` to a connection built straight from `credential`, bypassing `authenticate`. */\nconst attachZone = (\n definition: Provider.Definition,\n credential: Provider.Credential,\n zone: string,\n) =>\n Effect.gen(function* () {\n const storage = yield* Storage.Service;\n const custody = yield* Custody.Service;\n const now = yield* DateTime.now;\n const authorization = new Storage.Authorization({\n id: yield* fresh(\"auth\"),\n ownerId: principal.ownerId,\n provider: definition.id,\n method: \"token\",\n capabilities: [\"dns:read\", \"dns:write\"],\n context: yield* Provider.encodeContext(definition, credential.context),\n revocation: \"active\",\n createdBy: principal.actorId,\n createdAt: now,\n });\n yield* storage.authorizations.upsert({\n authorization,\n credential: new Storage.Credential({\n ciphertext: yield* custody.seal(credential.secret),\n expiresAt: null,\n rotatedAt: now,\n }),\n });\n const connection = yield* storage.connections.create(authorization.id);\n const attached = yield* Connect.attach({ connectionId: connection.id, domain: zone });\n if (!(attached instanceof Storage.Attachment)) {\n return yield* Effect.fail(\n failure(\"create-readback-cleanup\", `zone ${zone} needs a selection; pass an exact zone`),\n );\n }\n return attached;\n });\n\nconst cname = (zone: string, label: string) =>\n DnsRecord.cname({ name: `${label}.${zone}`, target: \"target.example.net\", ttl: 300 });\nconst txt = (zone: string, label: string, value: string) =>\n DnsRecord.txt({ name: `${label}.${zone}`, value, ttl: 300 });\n\nconst recordIds = (receipt: Receipt.Model) =>\n Receipt.applied(receipt).map(({ providerRecordId }) => providerRecordId);\n\n/**\n * Runs create/readback/cleanup, exact-noop, conflict, stale-plan, and partial-apply against a\n * real provider definition, through the same Provision and Cleanup services hosts use.\n */\nexport const provider = (\n definition: Provider.Definition,\n credential: Provider.Credential,\n zone: string,\n options: { readonly prefix?: string } = {},\n): Effect.Effect<void, Errors.DomainKitError> => {\n const prefix = options.prefix ?? \"domainkit-conformance\";\n const run = <A>(\n target: Provider.Definition,\n body: (\n dns: Provider.Dns,\n attachment: Storage.Attachment,\n ) => Effect.Effect<A, Errors.DomainKitError, Env | Principal.Service>,\n ) =>\n Effect.gen(function* () {\n const attachment = yield* attachZone(target, credential, zone);\n const { session, target: resolved } = yield* Connect.session(attachment.id);\n return yield* body(session.dns(resolved), attachment);\n }).pipe(\n Effect.provideService(Principal.Service, principal),\n Effect.provide(\n DomainKit.layerMemory({\n providers: [target],\n resolver: Layer.succeed(Resolver.Service)(silentResolver),\n }),\n ),\n );\n\n const cleanup = (dns: Provider.Dns, ids: () => ReadonlyArray<string>) =>\n Effect.suspend(() =>\n Effect.forEach(ids(), (id) => dns.delete(zone, id).pipe(Effect.ignore), { discard: true }),\n );\n\n const createReadbackCleanup = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"create-readback-cleanup\";\n return Effect.gen(function* () {\n const requirements = [\n cname(zone, `${prefix}-create`),\n txt(zone, `${prefix}-page`, \"domainkit\"),\n ];\n const plan = yield* Provision.plan({ domain: attachment.domain, requirements });\n yield* expect(\n name,\n plan.operations.every(({ _tag }) => _tag === \"Create\"),\n \"expected two Create operations\",\n );\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n created = recordIds(receipt);\n yield* expect(\n name,\n receipt.status === \"complete\" && created.length === 2,\n \"apply must return a complete receipt with provider record ids\",\n );\n const observed = yield* dns.list(zone);\n yield* expect(\n name,\n requirements.every((requirement) =>\n observed.some(({ record }) => DnsRecord.equals(record, requirement)),\n ),\n \"list must return every created record, including across provider pages\",\n );\n const cleanupPlan = yield* Cleanup.plan({ receiptId: receipt.id });\n yield* expect(\n name,\n cleanupPlan.operations.every(({ _tag }) => _tag === \"Delete\"),\n \"cleanup plan must delete both records\",\n );\n const cleaned = yield* Cleanup.apply(yield* Cleanup.approve(cleanupPlan));\n const remaining = yield* Effect.forEach(created, (id) => dns.get(zone, id));\n yield* expect(\n name,\n cleaned.status === \"complete\" && remaining.every((record) => record === null),\n \"receipt-bound cleanup must remove every created record\",\n );\n created = [];\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const exactNoop = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"exact-noop\";\n return Effect.gen(function* () {\n const requirement = cname(zone, `${prefix}-exact`);\n const { providerRecordId } = yield* dns.create(zone, requirement);\n created = [providerRecordId];\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [requirement],\n });\n yield* expect(name, plan.operations[0]?._tag === \"Noop\", \"an exact record must plan as Noop\");\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n yield* expect(\n name,\n Receipt.applied(receipt).length === 0,\n \"an exact plan must not create another record\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const conflict = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"conflict\";\n return Effect.gen(function* () {\n const { providerRecordId } = yield* dns.create(\n zone,\n txt(zone, `${prefix}-conflict`, \"occupied\"),\n );\n created = [providerRecordId];\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [cname(zone, `${prefix}-conflict`)],\n });\n yield* expect(\n name,\n plan.operations[0]?._tag === \"Conflict\",\n \"a CNAME over an occupied name must plan as Conflict\",\n );\n const rejected = yield* Provision.approve(plan).pipe(Effect.result);\n yield* expect(\n name,\n rejected._tag === \"Failure\" && rejected.failure.reason._tag === \"Conflict\",\n \"approve must fail closed on conflicts\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const stalePlan = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"stale-plan\";\n return Effect.gen(function* () {\n const requirement = cname(zone, `${prefix}-stale`);\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [requirement],\n });\n const approval = yield* Provision.approve(plan);\n const { providerRecordId } = yield* dns.create(zone, requirement);\n created = [providerRecordId];\n const stale = yield* Provision.apply(approval).pipe(Effect.result);\n yield* expect(\n name,\n stale._tag === \"Failure\" && stale.failure.reason._tag === \"Stale\",\n \"apply must reject provider state that changed after approval\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const flaky: Provider.Definition = {\n ...definition,\n session: (input) => {\n const session = definition.session(input);\n return {\n ...session,\n dns: (target) => {\n const dns = session.dns(target);\n let writes = 0;\n return {\n ...dns,\n create: (targetZone, record) => {\n writes += 1;\n return writes === 2\n ? Errors.fail(\n new Reason.ProviderUnavailable({\n provider: definition.id,\n message: \"injected conformance failure\",\n }),\n )\n : dns.create(targetZone, record);\n },\n };\n },\n };\n },\n };\n\n const partialApplyCleanup = run(flaky, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"partial-apply-cleanup\";\n return Effect.gen(function* () {\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [\n cname(zone, `${prefix}-partial-one`),\n txt(zone, `${prefix}-partial-two`, \"domainkit\"),\n ],\n });\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n created = recordIds(receipt);\n yield* expect(\n name,\n receipt.status === \"partial\" && created.length === 1,\n \"a later write failure must preserve the first confirmed write in a partial receipt\",\n );\n const cleanupPlan = yield* Cleanup.plan({ receiptId: receipt.id });\n yield* Cleanup.apply(yield* Cleanup.approve(cleanupPlan));\n const remaining = yield* Effect.forEach(created, (id) => dns.get(zone, id));\n yield* expect(\n name,\n remaining.every((record) => record === null),\n \"partial receipt cleanup must remove the confirmed write\",\n );\n created = [];\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n return Effect.all([createReadbackCleanup, exactNoop, conflict, stalePlan, partialApplyCleanup], {\n discard: true,\n });\n};\n\n/** Verification is out of scope for the provider contract; keep the pool quiet. */\nconst silentResolver: Resolver.Interface = {\n resolve: () => Effect.succeed([]),\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"provider.mjs","names":["Principal.make","Errors.DomainKitError","Reason.InvalidInput","Storage.Service","Custody.Service","Storage.Authorization","Provider.encodeContext","Storage.Credential","Connect.attach","Storage.Attachment","DnsRecord.cname","DnsRecord.txt","Receipt.applied","Connect.session","session","Principal.Service","DomainKit.layerMemory","Resolver.Service","plan","Provision.plan","Provision.apply","Provision.approve","DnsRecord.equals","Cleanup.plan","Cleanup.apply","Cleanup.approve","Errors.fail","Reason.ProviderUnavailable","Provider.tokenFields"],"sources":["../../../src/internal/conformance/provider.ts"],"sourcesContent":["import { DateTime, Effect, Layer, Redacted } from \"effect\";\n\nimport * as Cleanup from \"../../Cleanup.ts\";\nimport * as Connect from \"../../Connect.ts\";\nimport * as Custody from \"../../Custody.ts\";\nimport * as DnsRecord from \"../../DnsRecord.ts\";\nimport * as Errors from \"../error.ts\";\nimport * as Reason from \"../../Reason.ts\";\nimport * as DomainKit from \"../../DomainKit.ts\";\nimport * as Principal from \"../../Principal.ts\";\nimport * as Provider from \"../../Provider.ts\";\nimport * as Provision from \"../../Provision.ts\";\nimport * as Receipt from \"../../Receipt.ts\";\nimport * as Resolver from \"../../Resolver.ts\";\nimport * as Storage from \"../../Storage.ts\";\nimport { fresh } from \"../ids.ts\";\n\nexport const cases = [\n \"create-readback-cleanup\",\n \"exact-noop\",\n \"conflict\",\n \"stale-plan\",\n \"partial-apply-cleanup\",\n \"rejected-token\",\n] as const;\nexport type Case = (typeof cases)[number];\n\nconst principal = Principal.make({ ownerId: \"conformance\", actorId: \"conformance\" });\n\nconst failure = (name: Case, message: string) =>\n new Errors.DomainKitError({\n reason: new Reason.InvalidInput({\n message: `provider conformance ${name}: ${message}`,\n field: \"conformance\",\n }),\n });\n\nconst expect = (name: Case, condition: boolean, message: string) =>\n condition ? Effect.void : Effect.fail(failure(name, message));\n\ntype Env = Storage.Service | Connect.Service | Provision.Service | Cleanup.Service;\n\n/** Attach `zone` to a connection built straight from `credential`, bypassing `authenticate`. */\nconst attachZone = (\n definition: Provider.Definition,\n credential: Provider.Credential,\n zone: string,\n) =>\n Effect.gen(function* () {\n const storage = yield* Storage.Service;\n const custody = yield* Custody.Service;\n const now = yield* DateTime.now;\n const authorization = new Storage.Authorization({\n id: yield* fresh(\"auth\"),\n ownerId: principal.ownerId,\n provider: definition.id,\n method: \"token\",\n capabilities: [\"dns:read\", \"dns:write\"],\n context: yield* Provider.encodeContext(definition, credential.context),\n revocation: \"active\",\n createdBy: principal.actorId,\n createdAt: now,\n });\n yield* storage.authorizations.upsert({\n authorization,\n credential: new Storage.Credential({\n ciphertext: yield* custody.seal(credential.secret),\n expiresAt: null,\n rotatedAt: now,\n }),\n });\n const connection = yield* storage.connections.create(authorization.id);\n const attached = yield* Connect.attach({ connectionId: connection.id, domain: zone });\n if (!(attached instanceof Storage.Attachment)) {\n return yield* Effect.fail(\n failure(\"create-readback-cleanup\", `zone ${zone} needs a selection; pass an exact zone`),\n );\n }\n return attached;\n });\n\nconst cname = (zone: string, label: string) =>\n DnsRecord.cname({ name: `${label}.${zone}`, target: \"target.example.net\", ttl: 300 });\nconst txt = (zone: string, label: string, value: string) =>\n DnsRecord.txt({ name: `${label}.${zone}`, value, ttl: 300 });\n\nconst recordIds = (receipt: Receipt.Model) =>\n Receipt.applied(receipt).map(({ providerRecordId }) => providerRecordId);\n\n/**\n * Runs create/readback/cleanup, exact-noop, conflict, stale-plan, partial-apply, and\n * rejected-token against a real provider definition, through the same Provision and Cleanup\n * services hosts use.\n */\nexport const provider = (\n definition: Provider.Definition,\n credential: Provider.Credential,\n zone: string,\n options: {\n readonly prefix?: string;\n /**\n * Token values the provider rejects, for the `rejected-token` case. Default: every required\n * field set to an empty secret, which needs a token method whose required fields are all\n * secrets.\n */\n readonly rejectedToken?: Provider.TokenValues;\n } = {},\n): Effect.Effect<void, Errors.DomainKitError> => {\n const prefix = options.prefix ?? \"domainkit-conformance\";\n const run = <A>(\n target: Provider.Definition,\n body: (\n dns: Provider.Dns,\n attachment: Storage.Attachment,\n ) => Effect.Effect<A, Errors.DomainKitError, Env | Principal.Service>,\n ) =>\n Effect.gen(function* () {\n const attachment = yield* attachZone(target, credential, zone);\n const { session, target: resolved } = yield* Connect.session(attachment.id);\n return yield* body(session.dns(resolved), attachment);\n }).pipe(\n Effect.provideService(Principal.Service, principal),\n Effect.provide(\n DomainKit.layerMemory({\n providers: [target],\n resolver: Layer.succeed(Resolver.Service)(silentResolver),\n }),\n ),\n );\n\n const cleanup = (dns: Provider.Dns, ids: () => ReadonlyArray<string>) =>\n Effect.suspend(() =>\n Effect.forEach(ids(), (id) => dns.delete(zone, id).pipe(Effect.ignore), { discard: true }),\n );\n\n const createReadbackCleanup = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"create-readback-cleanup\";\n return Effect.gen(function* () {\n const requirements = [\n cname(zone, `${prefix}-create`),\n txt(zone, `${prefix}-page`, \"domainkit\"),\n ];\n const plan = yield* Provision.plan({ domain: attachment.domain, requirements });\n yield* expect(\n name,\n plan.operations.every(({ _tag }) => _tag === \"Create\"),\n \"expected two Create operations\",\n );\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n created = recordIds(receipt);\n yield* expect(\n name,\n receipt.status === \"complete\" && created.length === 2,\n \"apply must return a complete receipt with provider record ids\",\n );\n const observed = yield* dns.list(zone);\n yield* expect(\n name,\n requirements.every((requirement) =>\n observed.some(({ record }) => DnsRecord.equals(record, requirement)),\n ),\n \"list must return every created record, including across provider pages\",\n );\n const cleanupPlan = yield* Cleanup.plan({ receiptId: receipt.id });\n yield* expect(\n name,\n cleanupPlan.operations.every(({ _tag }) => _tag === \"Delete\"),\n \"cleanup plan must delete both records\",\n );\n const cleaned = yield* Cleanup.apply(yield* Cleanup.approve(cleanupPlan));\n const remaining = yield* Effect.forEach(created, (id) => dns.get(zone, id));\n yield* expect(\n name,\n cleaned.status === \"complete\" && remaining.every((record) => record === null),\n \"receipt-bound cleanup must remove every created record\",\n );\n created = [];\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const exactNoop = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"exact-noop\";\n return Effect.gen(function* () {\n const requirement = cname(zone, `${prefix}-exact`);\n const { providerRecordId } = yield* dns.create(zone, requirement);\n created = [providerRecordId];\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [requirement],\n });\n yield* expect(name, plan.operations[0]?._tag === \"Noop\", \"an exact record must plan as Noop\");\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n yield* expect(\n name,\n Receipt.applied(receipt).length === 0,\n \"an exact plan must not create another record\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const conflict = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"conflict\";\n return Effect.gen(function* () {\n const { providerRecordId } = yield* dns.create(\n zone,\n txt(zone, `${prefix}-conflict`, \"occupied\"),\n );\n created = [providerRecordId];\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [cname(zone, `${prefix}-conflict`)],\n });\n yield* expect(\n name,\n plan.operations[0]?._tag === \"Conflict\",\n \"a CNAME over an occupied name must plan as Conflict\",\n );\n const rejected = yield* Provision.approve(plan).pipe(Effect.result);\n yield* expect(\n name,\n rejected._tag === \"Failure\" && rejected.failure.reason._tag === \"Conflict\",\n \"approve must fail closed on conflicts\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const stalePlan = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"stale-plan\";\n return Effect.gen(function* () {\n const requirement = cname(zone, `${prefix}-stale`);\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [requirement],\n });\n const approval = yield* Provision.approve(plan);\n const { providerRecordId } = yield* dns.create(zone, requirement);\n created = [providerRecordId];\n const stale = yield* Provision.apply(approval).pipe(Effect.result);\n yield* expect(\n name,\n stale._tag === \"Failure\" && stale.failure.reason._tag === \"Stale\",\n \"apply must reject provider state that changed after approval\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const flaky: Provider.Definition = {\n ...definition,\n session: (input) => {\n const session = definition.session(input);\n return {\n ...session,\n dns: (target) => {\n const dns = session.dns(target);\n let writes = 0;\n return {\n ...dns,\n create: (targetZone, record) => {\n writes += 1;\n return writes === 2\n ? Errors.fail(\n new Reason.ProviderUnavailable({\n provider: definition.id,\n message: \"injected conformance failure\",\n }),\n )\n : dns.create(targetZone, record);\n },\n };\n },\n };\n },\n };\n\n const partialApplyCleanup = run(flaky, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"partial-apply-cleanup\";\n return Effect.gen(function* () {\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [\n cname(zone, `${prefix}-partial-one`),\n txt(zone, `${prefix}-partial-two`, \"domainkit\"),\n ],\n });\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n created = recordIds(receipt);\n yield* expect(\n name,\n receipt.status === \"partial\" && created.length === 1,\n \"a later write failure must preserve the first confirmed write in a partial receipt\",\n );\n const cleanupPlan = yield* Cleanup.plan({ receiptId: receipt.id });\n yield* Cleanup.apply(yield* Cleanup.approve(cleanupPlan));\n const remaining = yield* Effect.forEach(created, (id) => dns.get(zone, id));\n yield* expect(\n name,\n remaining.every((record) => record === null),\n \"partial receipt cleanup must remove the confirmed write\",\n );\n created = [];\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n /** A token the provider rejects must read as `Unauthenticated`, so a host tells the customer the provider refused it. */\n const rejectedToken = Effect.gen(function* () {\n const name: Case = \"rejected-token\";\n const auth = definition.auth.token;\n if (auth === undefined) return;\n const required = Provider.tokenFields(auth).filter((field) => field.required);\n if (options.rejectedToken === undefined && required.some((field) => !field.secret)) {\n return yield* Effect.fail(\n failure(\n name,\n `the token method requires ${required\n .filter((field) => !field.secret)\n .map((field) => field.name)\n .join(\", \")}; pass options.rejectedToken with values the provider rejects`,\n ),\n );\n }\n const values =\n options.rejectedToken ??\n Object.fromEntries(required.map((field) => [field.name, Redacted.make(\"\")]));\n const outcome = yield* auth.authenticate(values).pipe(Effect.result);\n yield* expect(\n name,\n outcome._tag === \"Failure\" && outcome.failure.reason._tag === \"Unauthenticated\",\n \"authenticating a rejected token must fail Unauthenticated\",\n );\n });\n\n return Effect.all(\n [createReadbackCleanup, exactNoop, conflict, stalePlan, partialApplyCleanup, rejectedToken],\n { discard: true },\n );\n};\n\n/** Verification is out of scope for the provider contract; keep the pool quiet. */\nconst silentResolver: Resolver.Interface = {\n resolve: () => Effect.succeed([]),\n};\n"],"mappings":"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"}
@@ -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":"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"}
1
+ {"version":3,"file":"http.mjs","names":["Errors.DomainKitError","Reason.ProviderRejected","Errors.isDomainKitError","Reason.ProviderUnavailable","Reason.Unauthenticated","Reason.Forbidden","Reason.ProviderConflict","Errors.fail"],"sources":["../../src/internal/http.ts"],"sourcesContent":["import { Effect } from \"effect\";\n\nimport * as Errors from \"./error.ts\";\nimport * as Reason from \"../Reason.ts\";\n\nexport type Fetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;\n\nexport interface Reply {\n readonly status: number;\n readonly ok: boolean;\n readonly headers: Headers;\n /** Parsed JSON, or `null` for an empty body. */\n readonly body: unknown;\n}\n\n/** One provider HTTP round trip: transport failures and non-JSON replies are already classified. */\nexport const requestJson = (input: {\n readonly fetch: Fetch;\n readonly provider: string;\n readonly url: URL | string;\n readonly init?: RequestInit;\n}): Effect.Effect<Reply, Errors.DomainKitError> =>\n Effect.tryPromise({\n try: async (signal): Promise<Reply> => {\n const { fetch: send } = input; // a free call keeps `this` undefined for a native fetch\n const response = await send(input.url, { ...input.init, signal });\n const text = await response.text();\n let body: unknown = null;\n if (text.length > 0) {\n try {\n body = JSON.parse(text);\n } catch {\n throw new Errors.DomainKitError({\n reason: response.ok\n ? new Reason.ProviderRejected({\n provider: input.provider,\n message: `${input.provider} returned a non-JSON response`,\n })\n : classify(\n input.provider,\n response.status,\n response.headers,\n `HTTP ${response.status}`,\n ),\n });\n }\n }\n return { status: response.status, ok: response.ok, headers: response.headers, body };\n },\n catch: (cause) =>\n Errors.isDomainKitError(cause)\n ? cause\n : new Errors.DomainKitError({\n reason: new Reason.ProviderUnavailable({\n provider: input.provider,\n message: cause instanceof Error ? cause.message : `${input.provider} request failed`,\n }),\n }),\n });\n\n/** Host-supplied request headers, given as a value or as a thunk that may await a fresh token. */\nexport const headersFrom = (\n source: HeadersInit | (() => HeadersInit | Promise<HeadersInit>) | undefined,\n): Effect.Effect<Headers, Errors.DomainKitError> =>\n source === undefined\n ? Effect.sync(() => new Headers())\n : typeof source !== \"function\"\n ? Effect.sync(() => new Headers(source))\n : Effect.tryPromise({\n try: async () => new Headers(await source()),\n catch: (cause) =>\n Errors.isDomainKitError(cause)\n ? cause\n : new Errors.DomainKitError({\n reason: new Reason.Unauthenticated({\n message:\n cause instanceof Error ? cause.message : \"Request headers were unavailable\",\n }),\n }),\n });\n\n/**\n * Map an HTTP failure to a reason: 401 -> Unauthenticated, 403 -> Forbidden, 409 or a provider\n * conflict code -> ProviderConflict, 429/5xx -> ProviderUnavailable, else ProviderRejected.\n */\nexport const classify = (\n provider: string,\n status: number,\n headers: Headers,\n message: string,\n code?: string,\n): Reason.Model => {\n if (status === 401) return new Reason.Unauthenticated({ message });\n if (status === 403) return new Reason.Forbidden({ message });\n if (status === 409 || code === \"conflict\") {\n return new Reason.ProviderConflict({\n provider,\n message,\n code: code ?? String(status),\n });\n }\n if (status === 429 || status >= 500) {\n const retryAfterMs = retryAfter(headers.get(\"retry-after\"));\n return new Reason.ProviderUnavailable({\n provider,\n message,\n ...(retryAfterMs === undefined ? {} : { retryAfterMs }),\n });\n }\n return new Reason.ProviderRejected({\n provider,\n message,\n code: code ?? String(status),\n });\n};\n\n/**\n * During token verification a `Forbidden` answer means the provider did not accept the token, so\n * it reads as `Unauthenticated` with the provider's own message.\n */\nexport const rejectedToken = <A, R>(\n verification: Effect.Effect<A, Errors.DomainKitError, R>,\n): Effect.Effect<A, Errors.DomainKitError, R> =>\n verification.pipe(\n Effect.catchIf(\n (error) => error.reason._tag === \"Forbidden\",\n (error) => Errors.fail(new Reason.Unauthenticated({ message: error.reason.message })),\n ),\n );\n\nexport const rejected = (provider: string, message: string, code?: string) =>\n new Errors.DomainKitError({\n reason: new Reason.ProviderRejected({\n provider,\n message,\n ...(code === undefined ? {} : { code }),\n }),\n });\n\nfunction retryAfter(value: string | null): number | undefined {\n if (value === null) return undefined;\n const seconds = Number(value);\n if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);\n const date = Date.parse(value);\n return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());\n}\n\nexport const bearer = (token: string, init?: RequestInit): RequestInit => ({\n ...init,\n headers: {\n Accept: \"application/json\",\n Authorization: `Bearer ${token}`,\n ...(init?.body === undefined ? {} : { \"Content-Type\": \"application/json\" }),\n ...init?.headers,\n },\n});\n"],"mappings":"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"}
@@ -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 requestOptions = (fetch, signal) => fetch === void 0 ? { signal } : {
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.customFetch]: (url, init) => fetch(url, {
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(input.fetch, signal));
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(input.fetch, signal));
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(input.fetch, signal));
130
+ const reply = await oauth.revocationRequest(server, client, clientAuth(input.client), input.token, requestOptions({
131
+ allowPlaintext: input.allowPlaintext,
132
+ endpoint: input.server.revocation_endpoint
133
+ }, input.fetch, signal));
106
134
  await oauth.processRevocationResponse(reply);
107
135
  },
108
136
  catch: (cause) => failure(input.provider, cause)
@@ -1 +1 @@
1
- {"version":3,"file":"oauth.mjs","names":["Errors.DomainKitError","Reason.CryptoFailed","Reason.Unauthenticated","Reason.ProviderRejected","Reason.ProviderUnavailable"],"sources":["../../src/internal/oauth.ts"],"sourcesContent":["import { DateTime, Effect, Redacted } from \"effect\";\nimport * as oauth from \"oauth4webapi\";\n\nimport * as Errors from \"./error.ts\";\nimport * as Reason from \"../Reason.ts\";\nimport type { Fetch } from \"./http.ts\";\n\nexport interface Server {\n readonly issuer: string;\n readonly authorization_endpoint: string;\n readonly token_endpoint: string;\n readonly revocation_endpoint?: string;\n}\n\nexport type ClientAuth = \"client_secret_basic\" | \"client_secret_post\" | \"none\";\n\nexport interface Client {\n readonly clientId: string;\n readonly clientSecret: Redacted.Redacted<string> | null;\n readonly clientAuth: ClientAuth;\n}\n\n/** A PKCE pair: the verifier stays in the continuation, the challenge goes to the provider. */\nexport const pkce = (): Effect.Effect<\n { readonly codeVerifier: string; readonly codeChallenge: string },\n Errors.DomainKitError\n> =>\n Effect.tryPromise({\n try: async () => {\n const codeVerifier = oauth.generateRandomCodeVerifier();\n return { codeVerifier, codeChallenge: await oauth.calculatePKCECodeChallenge(codeVerifier) };\n },\n catch: () =>\n new Errors.DomainKitError({\n reason: new Reason.CryptoFailed({ operation: \"digest\" }),\n }),\n });\n\nexport const authorizationUrl = (input: {\n readonly server: Server;\n readonly clientId: string;\n readonly scopes: ReadonlyArray<string>;\n readonly state: string;\n readonly callbackUrl: string;\n readonly codeChallenge: string;\n}): string => {\n const url = new URL(input.server.authorization_endpoint);\n url.search = new URLSearchParams({\n client_id: input.clientId,\n code_challenge: input.codeChallenge,\n code_challenge_method: \"S256\",\n redirect_uri: input.callbackUrl,\n response_type: \"code\",\n scope: input.scopes.join(\" \"),\n state: input.state,\n }).toString();\n return url.toString();\n};\n\nexport interface Tokens {\n readonly accessToken: string;\n readonly refreshToken: string | null;\n readonly expiresAt: DateTime.Utc | null;\n readonly scope: string | null;\n}\n\nconst clientAuth = (client: Client): oauth.ClientAuth => {\n if (client.clientAuth === \"none\") return oauth.None();\n const secret = client.clientSecret === null ? \"\" : Redacted.value(client.clientSecret);\n return client.clientAuth === \"client_secret_basic\"\n ? oauth.ClientSecretBasic(secret)\n : oauth.ClientSecretPost(secret);\n};\n\nconst requestOptions = (\n fetch: Fetch | undefined,\n signal: AbortSignal,\n): oauth.TokenEndpointRequestOptions =>\n fetch === undefined\n ? { signal }\n : { signal, [oauth.customFetch]: (url, init) => fetch(url, { ...init, body: init.body }) };\n\nconst tokens = (response: oauth.TokenEndpointResponse, now: DateTime.Utc): Tokens => ({\n accessToken: response.access_token,\n refreshToken: response.refresh_token ?? null,\n expiresAt:\n typeof response.expires_in === \"number\"\n ? DateTime.add(now, { seconds: response.expires_in })\n : null,\n scope: response.scope ?? null,\n});\n\nconst failure = (provider: string, cause: unknown): Errors.DomainKitError => {\n if (cause instanceof oauth.ResponseBodyError) {\n const terminal = [\"invalid_grant\", \"invalid_client\", \"unauthorized_client\"].includes(\n cause.error,\n );\n return new Errors.DomainKitError({\n reason: terminal\n ? new Reason.Unauthenticated({\n message: `${provider} rejected the grant: ${cause.error}`,\n })\n : new Reason.ProviderRejected({\n provider,\n code: cause.error,\n message: cause.error_description ?? cause.error,\n }),\n });\n }\n if (cause instanceof oauth.AuthorizationResponseError) {\n return new Errors.DomainKitError({\n reason: new Reason.Unauthenticated({\n message: `${provider} authorization failed: ${cause.error}`,\n }),\n });\n }\n return new Errors.DomainKitError({\n reason: new Reason.ProviderUnavailable({\n provider,\n message: cause instanceof Error ? cause.message : `${provider} OAuth request failed`,\n }),\n });\n};\n\nexport const exchangeCode = (input: {\n readonly provider: string;\n readonly server: Server;\n readonly client: Client;\n readonly code: string;\n readonly state: string;\n readonly callbackUrl: string;\n readonly codeVerifier: string;\n readonly fetch?: Fetch;\n}): Effect.Effect<Tokens, Errors.DomainKitError> =>\n Effect.gen(function* () {\n const now = yield* DateTime.now;\n const server = input.server as oauth.AuthorizationServer;\n const client: oauth.Client = { client_id: input.client.clientId };\n const response = yield* Effect.tryPromise({\n try: async (signal) => {\n const params = oauth.validateAuthResponse(\n server,\n client,\n new URLSearchParams({ code: input.code, state: input.state }),\n input.state,\n );\n const reply = await oauth.authorizationCodeGrantRequest(\n server,\n client,\n clientAuth(input.client),\n params,\n input.callbackUrl,\n input.codeVerifier,\n requestOptions(input.fetch, signal),\n );\n return oauth.processAuthorizationCodeResponse(server, client, reply);\n },\n catch: (cause) => failure(input.provider, cause),\n });\n return tokens(response, now);\n });\n\nexport const refresh = (input: {\n readonly provider: string;\n readonly server: Server;\n readonly client: Client;\n readonly refreshToken: string;\n readonly fetch?: Fetch;\n}): Effect.Effect<Tokens, Errors.DomainKitError> =>\n Effect.gen(function* () {\n const now = yield* DateTime.now;\n const server = input.server as oauth.AuthorizationServer;\n const client: oauth.Client = { client_id: input.client.clientId };\n const response = yield* Effect.tryPromise({\n try: async (signal) => {\n const reply = await oauth.refreshTokenGrantRequest(\n server,\n client,\n clientAuth(input.client),\n input.refreshToken,\n requestOptions(input.fetch, signal),\n );\n return oauth.processRefreshTokenResponse(server, client, reply);\n },\n catch: (cause) => failure(input.provider, cause),\n });\n const next = tokens(response, now);\n return { ...next, refreshToken: next.refreshToken ?? input.refreshToken };\n });\n\nexport const revoke = (input: {\n readonly provider: string;\n readonly server: Server;\n readonly client: Client;\n readonly token: string;\n readonly fetch?: Fetch;\n}): Effect.Effect<void, Errors.DomainKitError> =>\n Effect.tryPromise({\n try: async (signal) => {\n const server = input.server as oauth.AuthorizationServer;\n const client: oauth.Client = { client_id: input.client.clientId };\n const reply = await oauth.revocationRequest(\n server,\n client,\n clientAuth(input.client),\n input.token,\n requestOptions(input.fetch, signal),\n );\n await oauth.processRevocationResponse(reply);\n },\n catch: (cause) => failure(input.provider, cause),\n });\n"],"mappings":"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"}
1
+ {"version":3,"file":"oauth.mjs","names":["Errors.DomainKitError","Reason.CryptoFailed","Reason.Unauthenticated","Reason.ProviderRejected","Reason.ProviderUnavailable"],"sources":["../../src/internal/oauth.ts"],"sourcesContent":["import { DateTime, Effect, Redacted } from \"effect\";\nimport * as oauth from \"oauth4webapi\";\n\nimport * as Errors from \"./error.ts\";\nimport * as Reason from \"../Reason.ts\";\nimport type { Fetch } from \"./http.ts\";\n\nexport interface Server {\n readonly issuer: string;\n readonly authorization_endpoint: string;\n readonly token_endpoint: string;\n readonly revocation_endpoint?: string;\n}\n\nexport type ClientAuth = \"client_secret_basic\" | \"client_secret_post\" | \"none\";\n\nexport interface Client {\n readonly clientId: string;\n readonly clientSecret: Redacted.Redacted<string> | null;\n readonly clientAuth: ClientAuth;\n}\n\n/** A PKCE pair: the verifier stays in the continuation, the challenge goes to the provider. */\nexport const pkce = (): Effect.Effect<\n { readonly codeVerifier: string; readonly codeChallenge: string },\n Errors.DomainKitError\n> =>\n Effect.tryPromise({\n try: async () => {\n const codeVerifier = oauth.generateRandomCodeVerifier();\n return { codeVerifier, codeChallenge: await oauth.calculatePKCECodeChallenge(codeVerifier) };\n },\n catch: () =>\n new Errors.DomainKitError({\n reason: new Reason.CryptoFailed({ operation: \"digest\" }),\n }),\n });\n\nexport const authorizationUrl = (input: {\n readonly server: Server;\n readonly clientId: string;\n readonly scopes: ReadonlyArray<string>;\n readonly state: string;\n readonly callbackUrl: string;\n readonly codeChallenge: string;\n}): string => {\n const url = new URL(input.server.authorization_endpoint);\n url.search = new URLSearchParams({\n client_id: input.clientId,\n code_challenge: input.codeChallenge,\n code_challenge_method: \"S256\",\n redirect_uri: input.callbackUrl,\n response_type: \"code\",\n scope: input.scopes.join(\" \"),\n state: input.state,\n }).toString();\n return url.toString();\n};\n\nexport interface Tokens {\n readonly accessToken: string;\n readonly refreshToken: string | null;\n readonly expiresAt: DateTime.Utc | null;\n readonly scope: string | null;\n}\n\nconst clientAuth = (client: Client): oauth.ClientAuth => {\n if (client.clientAuth === \"none\") return oauth.None();\n const secret = client.clientSecret === null ? \"\" : Redacted.value(client.clientSecret);\n return client.clientAuth === \"client_secret_basic\"\n ? oauth.ClientSecretBasic(secret)\n : oauth.ClientSecretPost(secret);\n};\n\nconst plaintext = (endpoint: string | undefined): boolean =>\n endpoint !== undefined && URL.parse(endpoint)?.protocol === \"http:\";\n\n/**\n * A destination nothing can move: a loopback literal, or a name RFC 6761 reserves for one. Every\n * other name resolves wherever DNS or a hosts file says, so it is not evidence of anything.\n */\nconst loopback = (endpoint: string | undefined): boolean => {\n const url = endpoint === undefined ? null : URL.parse(endpoint);\n if (url === null || !plaintext(endpoint)) return false;\n const host = url.hostname.replace(/^\\[|\\]$/g, \"\");\n return (\n host === \"localhost\" ||\n host.endsWith(\".localhost\") ||\n host === \"::1\" ||\n /^127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/.test(host)\n );\n};\n\n/**\n * oauth4webapi refuses plaintext endpoints, which is right for every request that carries a client\n * secret, a code, or a token. Loopback is the one automatic exception: it goes nowhere and nothing\n * can repoint it. Any other plaintext endpoint takes a caller that said so out loud, because a name\n * like `host.docker.internal` resolves wherever the machine has been told to resolve it.\n */\nconst insecureAllowed = (endpoint: string | undefined, allowPlaintext: boolean | undefined) =>\n loopback(endpoint) || (allowPlaintext === true && plaintext(endpoint));\n\nconst requestOptions = (\n input: {\n readonly endpoint: string | undefined;\n readonly allowPlaintext: boolean | undefined;\n },\n fetch: Fetch | undefined,\n signal: AbortSignal,\n): oauth.TokenEndpointRequestOptions => ({\n signal,\n ...(insecureAllowed(input.endpoint, input.allowPlaintext)\n ? { [oauth.allowInsecureRequests]: true }\n : {}),\n ...(fetch === undefined\n ? {}\n : { [oauth.customFetch]: (url, init) => fetch(url, { ...init, body: init.body }) }),\n});\n\nconst tokens = (response: oauth.TokenEndpointResponse, now: DateTime.Utc): Tokens => ({\n accessToken: response.access_token,\n refreshToken: response.refresh_token ?? null,\n expiresAt:\n typeof response.expires_in === \"number\"\n ? DateTime.add(now, { seconds: response.expires_in })\n : null,\n scope: response.scope ?? null,\n});\n\nconst failure = (provider: string, cause: unknown): Errors.DomainKitError => {\n if (cause instanceof oauth.ResponseBodyError) {\n const terminal = [\"invalid_grant\", \"invalid_client\", \"unauthorized_client\"].includes(\n cause.error,\n );\n return new Errors.DomainKitError({\n reason: terminal\n ? new Reason.Unauthenticated({\n message: `${provider} rejected the grant: ${cause.error}`,\n })\n : new Reason.ProviderRejected({\n provider,\n code: cause.error,\n message: cause.error_description ?? cause.error,\n }),\n });\n }\n if (cause instanceof oauth.AuthorizationResponseError) {\n return new Errors.DomainKitError({\n reason: new Reason.Unauthenticated({\n message: `${provider} authorization failed: ${cause.error}`,\n }),\n });\n }\n return new Errors.DomainKitError({\n reason: new Reason.ProviderUnavailable({\n provider,\n message: cause instanceof Error ? cause.message : `${provider} OAuth request failed`,\n }),\n });\n};\n\nexport const exchangeCode = (input: {\n readonly provider: string;\n readonly server: Server;\n readonly client: Client;\n readonly code: string;\n readonly state: string;\n readonly callbackUrl: string;\n readonly codeVerifier: string;\n readonly fetch?: Fetch;\n /** Permit an `http:` endpoint that is not loopback. Development only; see `insecureAllowed`. */\n readonly allowPlaintext?: boolean;\n}): Effect.Effect<Tokens, Errors.DomainKitError> =>\n Effect.gen(function* () {\n const now = yield* DateTime.now;\n const server = input.server as oauth.AuthorizationServer;\n const client: oauth.Client = { client_id: input.client.clientId };\n const response = yield* Effect.tryPromise({\n try: async (signal) => {\n const params = oauth.validateAuthResponse(\n server,\n client,\n new URLSearchParams({ code: input.code, state: input.state }),\n input.state,\n );\n const reply = await oauth.authorizationCodeGrantRequest(\n server,\n client,\n clientAuth(input.client),\n params,\n input.callbackUrl,\n input.codeVerifier,\n requestOptions(\n { allowPlaintext: input.allowPlaintext, endpoint: input.server.token_endpoint },\n input.fetch,\n signal,\n ),\n );\n return oauth.processAuthorizationCodeResponse(server, client, reply);\n },\n catch: (cause) => failure(input.provider, cause),\n });\n return tokens(response, now);\n });\n\nexport const refresh = (input: {\n readonly provider: string;\n readonly server: Server;\n readonly client: Client;\n readonly refreshToken: string;\n readonly fetch?: Fetch;\n readonly allowPlaintext?: boolean;\n}): Effect.Effect<Tokens, Errors.DomainKitError> =>\n Effect.gen(function* () {\n const now = yield* DateTime.now;\n const server = input.server as oauth.AuthorizationServer;\n const client: oauth.Client = { client_id: input.client.clientId };\n const response = yield* Effect.tryPromise({\n try: async (signal) => {\n const reply = await oauth.refreshTokenGrantRequest(\n server,\n client,\n clientAuth(input.client),\n input.refreshToken,\n requestOptions(\n { allowPlaintext: input.allowPlaintext, endpoint: input.server.token_endpoint },\n input.fetch,\n signal,\n ),\n );\n return oauth.processRefreshTokenResponse(server, client, reply);\n },\n catch: (cause) => failure(input.provider, cause),\n });\n const next = tokens(response, now);\n return { ...next, refreshToken: next.refreshToken ?? input.refreshToken };\n });\n\nexport const revoke = (input: {\n readonly provider: string;\n readonly server: Server;\n readonly client: Client;\n readonly token: string;\n readonly fetch?: Fetch;\n readonly allowPlaintext?: boolean;\n}): Effect.Effect<void, Errors.DomainKitError> =>\n Effect.tryPromise({\n try: async (signal) => {\n const server = input.server as oauth.AuthorizationServer;\n const client: oauth.Client = { client_id: input.client.clientId };\n const reply = await oauth.revocationRequest(\n server,\n client,\n clientAuth(input.client),\n input.token,\n requestOptions(\n { allowPlaintext: input.allowPlaintext, endpoint: input.server.revocation_endpoint },\n input.fetch,\n signal,\n ),\n );\n await oauth.processRevocationResponse(reply);\n },\n catch: (cause) => failure(input.provider, cause),\n });\n"],"mappings":"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"}
package/dist/package.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.9.1";
2
+ var version = "0.11.0";
3
3
  //#endregion
4
4
  export { version };
5
5
 
@@ -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;
@@ -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;