domainkit 0.18.0 → 0.20.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 +10 -1
- package/dist/DnsRecord.mjs +15 -6
- package/dist/DnsRecord.mjs.map +1 -1
- package/dist/Plan.mjs +1 -0
- package/dist/Plan.mjs.map +1 -1
- package/dist/Server.mjs +8 -8
- package/dist/Server.mjs.map +1 -1
- package/dist/Verify.mjs +6 -6
- package/dist/Verify.mjs.map +1 -1
- package/dist/internal/planner.mjs +15 -14
- package/dist/internal/planner.mjs.map +1 -1
- package/dist/internal/reconciliation.mjs +46 -0
- package/dist/internal/reconciliation.mjs.map +1 -0
- package/dist/internal/spf.mjs +7 -0
- package/dist/internal/spf.mjs.map +1 -0
- package/dist/package.mjs +1 -1
- package/dist/types/DnsRecord.d.ts +10 -3
- package/dist/types/Plan.d.ts +1 -1
- package/dist/types/Server.d.ts +155 -55
- package/dist/types/Verify.d.ts +1 -1
- package/dist/types/internal/reconciliation.d.ts +11 -0
- package/dist/types/internal/spf.d.ts +2 -0
- package/package.json +4 -4
package/dist/Verify.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Verify.mjs","names":["Storage.RequirementStatus","DnsRecord.data","DnsRecord.equals","DnsRecord.sameSet","Storage.Service","Connect.Service","Resolver.Service","DomainName.decode","Errors.decode","Principal.Service","Storage.Readiness","Errors.fail","Reason.InvalidInput"],"sources":["../src/Verify.ts"],"sourcesContent":["/**\n * Observe provider and public DNS for an attachment, persist per-requirement readiness, and say\n * when to look again. The host keeps its own state machine (and any provider-side verification\n * such as SES) and feeds that in as `HostEvidence`.\n */\nimport { Context, DateTime, Duration, Effect, Layer, Option, Schema } from \"effect\";\n\nimport * as Connect from \"./Connect.ts\";\nimport * as DnsRecord from \"./DnsRecord.ts\";\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as DomainName from \"./DomainName.ts\";\nimport type * as Plan from \"./Plan.ts\";\nimport * as Principal from \"./Principal.ts\";\nimport * as Resolver from \"./Resolver.ts\";\nimport * as Storage from \"./Storage.ts\";\n\n/**\n * Every piece of evidence carries `values`, what the observer returned for the requirement's name\n * and type (empty when nothing was found, the lookup timed out, or the resolver failed), and\n * `detail`, `null` when the requirement is satisfied and otherwise the mismatch summary or the\n * resolver's error text, so a host can render \"found X, expected Y\".\n */\n// Rows written before these fields existed decode with the empty values.\nconst Values = Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed([])));\nconst Detail = Schema.NullOr(Schema.String).pipe(\n Schema.withDecodingDefaultKey(Effect.succeed(null)),\n);\n\nexport class ProviderEvidence extends Schema.TaggedClass<ProviderEvidence>(\n \"@domainkit/Evidence/Provider\",\n)(\"Provider\", {\n provider: Schema.String,\n status: Storage.RequirementStatus,\n values: Values,\n detail: Detail,\n observedAt: Schema.DateTimeUtcFromString,\n}) {}\nexport class PublicDnsEvidence extends Schema.TaggedClass<PublicDnsEvidence>(\n \"@domainkit/Evidence/PublicDns\",\n)(\"PublicDns\", {\n resolver: Schema.String,\n status: Storage.RequirementStatus,\n values: Values,\n detail: Detail,\n observedAt: Schema.DateTimeUtcFromString,\n}) {}\n/** Anything the host knows that DomainKit cannot observe: SES identity status, a CDN cert, ... */\nexport class HostEvidence extends Schema.TaggedClass<HostEvidence>(\"@domainkit/Evidence/Host\")(\n \"Host\",\n {\n source: Schema.String,\n status: Schema.Literals([\"ok\", \"pending\", \"failed\"]),\n label: Schema.String,\n detail: Detail,\n observedAt: Schema.DateTimeUtcFromString,\n },\n) {}\nexport const Evidence = Schema.Union([ProviderEvidence, PublicDnsEvidence, HostEvidence]);\nexport type Evidence = typeof Evidence.Type;\n\nexport interface Requirement {\n /** `requirementKey(record)`: what a host pairs its own row against, by content. */\n readonly key: string;\n readonly operationId: Plan.OperationId | null;\n readonly record: DnsRecord.Model;\n readonly status: Storage.RequirementStatus;\n readonly evidence: ReadonlyArray<Evidence>;\n}\n\nexport interface Readiness {\n readonly domain: string;\n /** `null` for observe-only domains that have no attachment. */\n readonly attachmentId: string | null;\n readonly overall: Storage.Overall;\n readonly requirements: ReadonlyArray<Requirement>;\n readonly host: ReadonlyArray<HostEvidence>;\n readonly checkedAt: DateTime.Utc;\n readonly nextCheckAt: DateTime.Utc | null;\n}\n\ntype Fx<A> = Effect.Effect<A, Errors.DomainKitError, Principal.Service>;\n\nexport interface Interface {\n /**\n * What to observe, in order of precedence: `requirements` when supplied (an attached domain\n * without a provisioning receipt, or a domain with no attachment whose records the customer\n * applies by hand), else the latest provisioning receipt of the domain's attachment (records it\n * applied or found in place). Neither is `InvalidInput`. Provider evidence is added when the\n * attachment's session can be built; public DNS is always observed, and readiness is stored\n * per domain either way.\n */\n readonly observe: (input: {\n readonly domain: string;\n readonly requirements?: ReadonlyArray<DnsRecord.Model>;\n }) => Fx<Readiness>;\n /** Merge host evidence into stored readiness without re-observing DNS. */\n readonly attachEvidence: (input: {\n readonly domain: string;\n readonly evidence: ReadonlyArray<HostEvidence>;\n }) => Fx<Readiness>;\n readonly latest: (domain: string) => Fx<Readiness | null>;\n}\n\nexport class Service extends Context.Service<Service, Interface>()(\"@domainkit/Verify\") {}\n\n/**\n * A requirement's identity: the record's type, name, and data, and nothing else. It is what a host\n * pairs its own rows against readiness by, instead of trusting the order of the array.\n * `@domainkit/react`'s `Records.identity` is this function.\n */\nexport const requirementKey = (record: DnsRecord.Model): string =>\n [record._tag, record.name, DnsRecord.data(record)].join(\":\");\n\n/** Counts across a readiness's requirements. `observed` is false when there is no readiness yet. */\nexport interface Summary {\n readonly observed: boolean;\n readonly total: number;\n readonly satisfied: number;\n readonly missing: number;\n readonly mismatch: number;\n readonly unknown: number;\n}\n\n/**\n * How a readiness stands, as counts. Pure and total: a host renders \"3 of 4 found\" from the stored\n * fact without deciding what an absent readiness means, because `observed` says so. The parameter\n * is the least a value has to carry to be summarised, so core `Readiness` and the wire shape both\n * pass without naming a type between them.\n */\nexport const summary = (\n readiness: {\n readonly requirements: ReadonlyArray<{ readonly status: Storage.RequirementStatus }>;\n } | null,\n): Summary => {\n const statuses = readiness?.requirements.map(({ status }) => status) ?? [];\n const count = (status: Storage.RequirementStatus): number =>\n statuses.filter((candidate) => candidate === status).length;\n return {\n observed: readiness !== null,\n total: statuses.length,\n satisfied: count(\"satisfied\"),\n missing: count(\"missing\"),\n mismatch: count(\"mismatch\"),\n unknown: count(\"unknown\"),\n };\n};\n\nexport interface PolicyShape {\n /** Delay before the next check, given time since the first pending observation. Default ladder: 15s, 1m, 5m, 30m. */\n readonly backoff: (pendingForMs: number) => number;\n /** Which resolvers must agree for `satisfied`: default `any`. */\n readonly quorum: \"any\" | \"all\" | { readonly minimum: number };\n}\nexport const defaults: PolicyShape = {\n backoff: (pendingForMs) =>\n pendingForMs < 60_000\n ? 15_000\n : pendingForMs < 10 * 60_000\n ? 60_000\n : pendingForMs < 60 * 60_000\n ? 5 * 60_000\n : 30 * 60_000,\n quorum: \"any\",\n};\nexport class Policy extends Context.Reference<PolicyShape>(\"@domainkit/Verify/Policy\", {\n defaultValue: () => defaults,\n}) {}\n\n/** Why readiness was written: a DNS observation, or host evidence merged into the stored row. */\nexport type Cause = \"observe\" | \"evidence\";\n\nexport interface ReadinessChanged {\n /** The tenant the row was written under, from the `Principal` the write ran as. */\n readonly ownerId: string;\n readonly domain: string;\n readonly readiness: Readiness;\n readonly cause: Cause;\n}\n\nexport interface ObserverShape {\n /**\n * Called once per stored readiness, after the write. A host that projects readiness onto its own\n * rows, wakes a durable job at `nextCheckAt`, or notifies a customer hangs it here instead of\n * mirroring the fact at every call site.\n *\n * Readiness is stored per owner and domain, and an observer provided over `DomainKit.layer` is\n * scoped to the layer rather than to one request, so a host that serves more than one tenant\n * keys its projection by `ownerId` and `domain`: the same domain name can be attached in two\n * tenants at once.\n *\n * Events for one domain are not ordered against each other: two observations that overlap both\n * store, last write wins in `Storage`, and their callbacks can finish in either order. A\n * projection compares `readiness.checkedAt`, which is the moment the row was written, and keeps\n * the later one; `latest` remains the authority.\n */\n readonly readinessChanged: (event: ReadinessChanged) => Effect.Effect<void, unknown>;\n}\n\n/**\n * The host seam for \"DomainKit wrote readiness\". The default does nothing; provide one with\n * `Effect.provideService(Verify.Observer, ...)` or `Layer.succeed(Verify.Observer, ...)` over\n * `DomainKit.layer`, the same way `Policy` is overridden.\n *\n * It runs after `storage.readiness.put` returns, outside any transaction, because `Storage`\n * exposes none. A failure or defect in the observer is logged and swallowed: the observation is\n * already durable, and losing it because a projection failed would make the stored fact depend on\n * the host's side effects. A host that needs the projection to be reliable enqueues durable work\n * here rather than doing the work inline.\n */\nexport class Observer extends Context.Reference<ObserverShape>(\"@domainkit/Verify/Observer\", {\n defaultValue: (): ObserverShape => ({ readinessChanged: () => Effect.void }),\n}) {}\n\n// ---------------------------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------------------------\n\nconst StoredEvidence = Schema.Array(Evidence);\n\ninterface ProviderSide {\n readonly provider: string;\n readonly records: ReadonlyArray<DnsRecord.Observed>;\n}\nconst StoredHost = Schema.Array(HostEvidence);\n\n/** A requirement is satisfied by an exact match; an exclusive one is contradicted by any same-set record. */\nexport const statusAgainst = (\n record: DnsRecord.Model,\n observed: ReadonlyArray<DnsRecord.Observed>,\n): Storage.RequirementStatus => {\n if (observed.some((candidate) => DnsRecord.equals(candidate, record))) return \"satisfied\";\n if (\n record.policy === \"exclusive\" &&\n observed.some((candidate) => DnsRecord.sameSet(candidate, record))\n ) {\n return \"mismatch\";\n }\n return \"missing\";\n};\n\ninterface Observation {\n readonly status: Storage.RequirementStatus;\n readonly values: ReadonlyArray<string>;\n readonly detail: string | null;\n}\n\nconst render = (observed: DnsRecord.Observed): string =>\n observed._tag === \"Opaque\" ? JSON.stringify(observed.raw) : DnsRecord.data(observed);\n\n/** Status plus what the observer holds for the requirement's name and type. */\nconst observation = (\n record: DnsRecord.Model,\n observed: ReadonlyArray<DnsRecord.Observed>,\n): Observation => {\n const status = statusAgainst(record, observed);\n const values = observed.filter((candidate) => DnsRecord.sameSet(candidate, record)).map(render);\n const detail =\n status === \"satisfied\"\n ? null\n : values.length === 0\n ? `no ${record._tag} record at ${record.name}`\n : `expected ${DnsRecord.data(record)}; found ${values.join(\", \")}`;\n return { status, values, detail };\n};\n\nconst combine = (statuses: ReadonlyArray<Storage.RequirementStatus>): Storage.RequirementStatus => {\n const known = statuses.filter((status) => status !== \"unknown\");\n if (known.length === 0) return \"unknown\";\n if (known.includes(\"mismatch\")) return \"mismatch\";\n return known.every((status) => status === \"satisfied\") ? \"satisfied\" : \"missing\";\n};\n\nconst quorumStatus = (\n quorum: PolicyShape[\"quorum\"],\n statuses: ReadonlyArray<Storage.RequirementStatus>,\n): Storage.RequirementStatus => {\n const known = statuses.filter((status) => status !== \"unknown\");\n const satisfied = known.filter((status) => status === \"satisfied\").length;\n const needed =\n quorum === \"any\" ? 1 : quorum === \"all\" ? Math.max(1, known.length) : quorum.minimum;\n if (known.includes(\"mismatch\")) return \"mismatch\";\n if (known.length > 0 && satisfied >= needed) return \"satisfied\";\n return known.length === 0 ? \"unknown\" : \"missing\";\n};\n\nconst overallOf = (\n requirements: ReadonlyArray<{ readonly status: Storage.RequirementStatus }>,\n host: ReadonlyArray<HostEvidence>,\n): Storage.Overall => {\n if (\n requirements.some(({ status }) => status === \"mismatch\") ||\n host.some(({ status }) => status === \"failed\")\n ) {\n return \"failed\";\n }\n const ready =\n requirements.length + host.length > 0 &&\n requirements.every(({ status }) => status === \"satisfied\") &&\n host.every(({ status }) => status === \"ok\");\n return ready ? \"ready\" : \"pending\";\n};\n\n/** The identity of a requirement set: type, name, data, and policy, independent of labels and order. */\nconst requirementSetKey = (requirements: ReadonlyArray<{ readonly record: DnsRecord.Model }>) =>\n requirements\n .map(({ record }) => `${record._tag} ${record.name} ${DnsRecord.data(record)} ${record.policy}`)\n .sort()\n .join(\"\\n\");\n\nexport const make: Effect.Effect<\n Interface,\n never,\n Storage.Service | Connect.Service | Resolver.Service\n> = Effect.gen(function* () {\n const storage = yield* Storage.Service;\n const connect = yield* Connect.Service;\n const resolver = yield* Resolver.Service;\n\n const attachmentFor = (\n input: string,\n ): Fx<{\n readonly domain: DomainName.Model;\n readonly attachment: Storage.Attachment | null;\n }> =>\n Effect.gen(function* () {\n const domain = yield* DomainName.decode(input);\n const attachment = yield* storage.attachments.byDomain(domain);\n return { domain, attachment: Option.getOrNull(attachment) };\n });\n\n const decodeRow = (row: Storage.Readiness): Effect.Effect<Readiness, Errors.DomainKitError> =>\n Effect.gen(function* () {\n const requirements = yield* Effect.forEach(row.requirements, (requirement) =>\n Errors.decode(StoredEvidence, requirement.evidence, \"evidence\").pipe(\n Effect.map((evidence): Requirement => ({\n key: requirementKey(requirement.record),\n operationId: requirement.operationId,\n record: requirement.record,\n status: requirement.status,\n evidence,\n })),\n ),\n );\n const host = yield* Errors.decode(StoredHost, row.host, \"host\");\n return {\n domain: row.domain,\n attachmentId: row.attachmentId,\n overall: row.overall,\n requirements,\n host,\n checkedAt: row.checkedAt,\n nextCheckAt: row.nextCheckAt,\n };\n });\n\n /** Persist readiness, carrying the pending streak forward for the backoff ladder. */\n const store = (input: {\n readonly domain: string;\n readonly attachment: Storage.Attachment | null;\n readonly requirements: ReadonlyArray<Requirement>;\n readonly host: ReadonlyArray<HostEvidence>;\n readonly previous: Option.Option<Storage.Readiness>;\n readonly cause: Cause;\n }): Fx<Readiness> =>\n Effect.gen(function* () {\n const principal = yield* Principal.Service;\n const policy = yield* Policy;\n const observer = yield* Observer;\n const now = yield* DateTime.now;\n const overall = overallOf(input.requirements, input.host);\n const sameRequirements =\n Option.isSome(input.previous) &&\n requirementSetKey(input.previous.value.requirements) ===\n requirementSetKey(input.requirements);\n const pendingSince =\n overall === \"ready\"\n ? null\n : sameRequirements &&\n Option.isSome(input.previous) &&\n input.previous.value.pendingSince !== null\n ? input.previous.value.pendingSince\n : now;\n const nextCheckAt =\n pendingSince === null\n ? null\n : DateTime.addDuration(\n now,\n Duration.millis(\n policy.backoff(DateTime.toEpochMillis(now) - DateTime.toEpochMillis(pendingSince)),\n ),\n );\n const row = new Storage.Readiness({\n domain: input.domain,\n attachmentId: input.attachment?.id ?? null,\n ownerId: principal.ownerId,\n overall,\n requirements: input.requirements.map((requirement) => ({\n operationId: requirement.operationId,\n record: requirement.record,\n status: requirement.status,\n evidence: Schema.encodeSync(StoredEvidence)(requirement.evidence),\n })),\n host: Schema.encodeSync(StoredHost)(input.host),\n pendingSince,\n checkedAt: now,\n nextCheckAt,\n });\n yield* storage.readiness.put(row);\n const readiness: Readiness = {\n domain: row.domain,\n attachmentId: row.attachmentId,\n overall,\n requirements: input.requirements,\n host: input.host,\n checkedAt: now,\n nextCheckAt,\n };\n yield* observer\n .readinessChanged({\n ownerId: principal.ownerId,\n domain: row.domain,\n readiness,\n cause: input.cause,\n })\n .pipe(\n Effect.catchCause((cause) =>\n Effect.logWarning(`Verify.Observer failed for ${row.domain}`, cause),\n ),\n );\n return readiness;\n });\n\n const defaultRequirements = (\n attachment: Storage.Attachment,\n ): Fx<\n ReadonlyArray<{\n readonly operationId: Plan.OperationId | null;\n readonly record: DnsRecord.Model;\n }>\n > =>\n Effect.gen(function* () {\n const latest = yield* storage.attempts.latest(attachment.id, \"provisioning\");\n if (Option.isNone(latest) || latest.value.receipt === null) {\n return yield* Errors.fail(\n new Reason.InvalidInput({\n message: `${attachment.domain} has no provisioning receipt; pass requirements to observe`,\n field: \"requirements\",\n }),\n );\n }\n const receipt = latest.value.receipt;\n const inPlace = new Set(\n receipt.outcomes.flatMap((outcome) =>\n outcome._tag === \"Applied\" || (outcome._tag === \"Skipped\" && outcome.reason === \"noop\")\n ? [outcome.operationId]\n : [],\n ),\n );\n return latest.value.plan.operations\n .filter((operation) => inPlace.has(operation.id))\n .map((operation) => ({ operationId: operation.id, record: operation.record }));\n });\n\n const observe: Interface[\"observe\"] = (input) =>\n Effect.gen(function* () {\n const policy = yield* Policy;\n const { domain, attachment } = yield* attachmentFor(input.domain);\n if (input.requirements === undefined && attachment === null) {\n return yield* Errors.fail(\n new Reason.InvalidInput({\n message: `${domain} is not attached; pass requirements to observe public DNS`,\n field: \"requirements\",\n }),\n );\n }\n const requirements =\n input.requirements === undefined && attachment !== null\n ? yield* defaultRequirements(attachment)\n : (input.requirements ?? []).map((record) => ({ operationId: null, record }));\n if (requirements.length === 0) {\n return yield* Errors.fail(\n new Reason.InvalidInput({\n message: \"Nothing to observe: pass at least one requirement\",\n field: \"requirements\",\n }),\n );\n }\n const now = yield* DateTime.now;\n // Provider readback is evidence when the attachment's session can be built; when it\n // cannot (no attachment, revoked credential, provider outage) public DNS stands alone.\n const providerSide = yield* attachment === null\n ? Effect.succeed(Option.none<ProviderSide>())\n : Effect.gen(function* () {\n const connection = yield* storage.connections.get(attachment.connectionId);\n const authorization = yield* storage.authorizations.get(connection.authorizationId);\n const { session, target } = yield* connect.session(attachment.id);\n const observed = yield* session.dns(target).list(target.zone);\n return Option.some<ProviderSide>({\n provider: authorization.provider,\n records: observed.map(({ record }) => record),\n });\n }).pipe(Effect.catch(() => Effect.succeed(Option.none<ProviderSide>())));\n const observed = yield* Effect.forEach(\n requirements,\n ({ operationId, record }) =>\n Effect.gen(function* () {\n const providerEvidence = Option.isSome(providerSide)\n ? [\n new ProviderEvidence({\n provider: providerSide.value.provider,\n ...observation(record, providerSide.value.records),\n observedAt: now,\n }),\n ]\n : [];\n const outcomes = yield* resolver.resolve(record.name, record._tag);\n const publicEvidence = outcomes.map((outcome) =>\n outcome._tag === \"Answered\"\n ? new PublicDnsEvidence({\n resolver: outcome.answer.resolver,\n ...observation(record, outcome.answer.records),\n observedAt: now,\n })\n : new PublicDnsEvidence({\n resolver: outcome.resolver,\n status: \"unknown\",\n values: [],\n detail:\n outcome._tag === \"TimedOut\"\n ? `${outcome.resolver} timed out`\n : outcome.message,\n observedAt: now,\n }),\n );\n const publicStatus = quorumStatus(\n policy.quorum,\n publicEvidence.map(({ status }) => status),\n );\n const status = combine([\n ...providerEvidence.map((evidence) => evidence.status),\n publicStatus,\n ]);\n return {\n key: requirementKey(record),\n operationId,\n record,\n status,\n evidence: [...providerEvidence, ...publicEvidence],\n } satisfies Requirement;\n }),\n { concurrency: \"unbounded\" },\n );\n const previous = yield* storage.readiness.get(domain);\n const host = Option.isSome(previous)\n ? yield* Errors.decode(StoredHost, previous.value.host, \"host\")\n : [];\n return yield* store({\n domain,\n attachment,\n requirements: observed,\n host,\n previous,\n cause: \"observe\",\n });\n });\n\n const attachEvidence: Interface[\"attachEvidence\"] = (input) =>\n Effect.gen(function* () {\n const { domain, attachment } = yield* attachmentFor(input.domain);\n const previous = yield* storage.readiness.get(domain);\n const current = Option.isSome(previous)\n ? yield* decodeRow(previous.value)\n : {\n requirements: [] as ReadonlyArray<Requirement>,\n host: [] as ReadonlyArray<HostEvidence>,\n };\n const bySource = new Map(current.host.map((evidence) => [evidence.source, evidence]));\n for (const evidence of input.evidence) bySource.set(evidence.source, evidence);\n return yield* store({\n domain,\n attachment,\n requirements: current.requirements,\n host: [...bySource.values()],\n previous,\n cause: \"evidence\",\n });\n });\n\n const latest: Interface[\"latest\"] = (domain) =>\n Effect.gen(function* () {\n const { domain: name } = yield* attachmentFor(domain);\n const stored = yield* storage.readiness.get(name);\n return Option.isSome(stored) ? yield* decodeRow(stored.value) : null;\n });\n\n return { observe, attachEvidence, latest };\n});\n\nexport const layer: Layer.Layer<\n Service,\n never,\n Storage.Service | Connect.Service | Resolver.Service\n> = Layer.effect(Service)(make);\n\nconst accessor =\n <Args extends ReadonlyArray<unknown>, A>(\n pick: (service: Interface) => (...args: Args) => Fx<A>,\n ): ((...args: Args) => Effect.Effect<A, Errors.DomainKitError, Principal.Service | Service>) =>\n (...args) =>\n Effect.flatMap(Service, (service) => pick(service)(...args));\n\nexport const observe = accessor((service) => service.observe);\nexport const attachEvidence = accessor((service) => service.attachEvidence);\nexport const latest = accessor((service) => service.latest);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Verify.mjs","names":["Storage.RequirementStatus","DnsRecord.data","DnsRecord.equals","DnsRecord.sameSet","Storage.Service","Connect.Service","Resolver.Service","DomainName.decode","Errors.decode","Principal.Service","Storage.Readiness","Errors.fail","Reason.InvalidInput"],"sources":["../src/Verify.ts"],"sourcesContent":["/**\n * Observe provider and public DNS for an attachment, persist per-requirement readiness, and say\n * when to look again. The host keeps its own state machine (and any provider-side verification\n * such as SES) and feeds that in as `HostEvidence`.\n */\nimport { Context, DateTime, Duration, Effect, Layer, Option, Schema } from \"effect\";\n\nimport * as Connect from \"./Connect.ts\";\nimport * as DnsRecord from \"./DnsRecord.ts\";\nimport * as Errors from \"./internal/error.ts\";\nimport { collision } from \"./internal/reconciliation.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as DomainName from \"./DomainName.ts\";\nimport type * as Plan from \"./Plan.ts\";\nimport * as Principal from \"./Principal.ts\";\nimport * as Resolver from \"./Resolver.ts\";\nimport * as Storage from \"./Storage.ts\";\n\n/**\n * Every piece of evidence carries `values`, what the observer returned for the requirement's name\n * and type (empty when nothing was found, the lookup timed out, or the resolver failed), and\n * `detail`, `null` when the requirement is satisfied and otherwise the mismatch summary or the\n * resolver's error text, so a host can render \"found X, expected Y\".\n */\n// Rows written before these fields existed decode with the empty values.\nconst Values = Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed([])));\nconst Detail = Schema.NullOr(Schema.String).pipe(\n Schema.withDecodingDefaultKey(Effect.succeed(null)),\n);\n\nexport class ProviderEvidence extends Schema.TaggedClass<ProviderEvidence>(\n \"@domainkit/Evidence/Provider\",\n)(\"Provider\", {\n provider: Schema.String,\n status: Storage.RequirementStatus,\n values: Values,\n detail: Detail,\n observedAt: Schema.DateTimeUtcFromString,\n}) {}\nexport class PublicDnsEvidence extends Schema.TaggedClass<PublicDnsEvidence>(\n \"@domainkit/Evidence/PublicDns\",\n)(\"PublicDns\", {\n resolver: Schema.String,\n status: Storage.RequirementStatus,\n values: Values,\n detail: Detail,\n observedAt: Schema.DateTimeUtcFromString,\n}) {}\n/** Anything the host knows that DomainKit cannot observe: SES identity status, a CDN cert, ... */\nexport class HostEvidence extends Schema.TaggedClass<HostEvidence>(\"@domainkit/Evidence/Host\")(\n \"Host\",\n {\n source: Schema.String,\n status: Schema.Literals([\"ok\", \"pending\", \"failed\"]),\n label: Schema.String,\n detail: Detail,\n observedAt: Schema.DateTimeUtcFromString,\n },\n) {}\nexport const Evidence = Schema.Union([ProviderEvidence, PublicDnsEvidence, HostEvidence]);\nexport type Evidence = typeof Evidence.Type;\n\nexport interface Requirement {\n /** `requirementKey(record)`: what a host pairs its own row against, by content. */\n readonly key: string;\n readonly operationId: Plan.OperationId | null;\n readonly record: DnsRecord.Model;\n readonly status: Storage.RequirementStatus;\n readonly evidence: ReadonlyArray<Evidence>;\n}\n\nexport interface Readiness {\n readonly domain: string;\n /** `null` for observe-only domains that have no attachment. */\n readonly attachmentId: string | null;\n readonly overall: Storage.Overall;\n readonly requirements: ReadonlyArray<Requirement>;\n readonly host: ReadonlyArray<HostEvidence>;\n readonly checkedAt: DateTime.Utc;\n readonly nextCheckAt: DateTime.Utc | null;\n}\n\ntype Fx<A> = Effect.Effect<A, Errors.DomainKitError, Principal.Service>;\n\nexport interface Interface {\n /**\n * What to observe, in order of precedence: `requirements` when supplied (an attached domain\n * without a provisioning receipt, or a domain with no attachment whose records the customer\n * applies by hand), else the latest provisioning receipt of the domain's attachment (records it\n * applied or found in place). Neither is `InvalidInput`. Provider evidence is added when the\n * attachment's session can be built; public DNS is always observed, and readiness is stored\n * per domain either way.\n */\n readonly observe: (input: {\n readonly domain: string;\n readonly requirements?: ReadonlyArray<DnsRecord.Model>;\n }) => Fx<Readiness>;\n /** Merge host evidence into stored readiness without re-observing DNS. */\n readonly attachEvidence: (input: {\n readonly domain: string;\n readonly evidence: ReadonlyArray<HostEvidence>;\n }) => Fx<Readiness>;\n readonly latest: (domain: string) => Fx<Readiness | null>;\n}\n\nexport class Service extends Context.Service<Service, Interface>()(\"@domainkit/Verify\") {}\n\n/**\n * A requirement's identity: the record's type, name, and data, and nothing else. It is what a host\n * pairs its own rows against readiness by, instead of trusting the order of the array.\n * `@domainkit/react`'s `Records.identity` is this function.\n */\nexport const requirementKey = (record: DnsRecord.Model): string =>\n [record._tag, record.name, DnsRecord.data(record)].join(\":\");\n\n/** Counts across a readiness's requirements. `observed` is false when there is no readiness yet. */\nexport interface Summary {\n readonly observed: boolean;\n readonly total: number;\n readonly satisfied: number;\n readonly missing: number;\n readonly mismatch: number;\n readonly unknown: number;\n}\n\n/**\n * How a readiness stands, as counts. Pure and total: a host renders \"3 of 4 found\" from the stored\n * fact without deciding what an absent readiness means, because `observed` says so. The parameter\n * is the least a value has to carry to be summarised, so core `Readiness` and the wire shape both\n * pass without naming a type between them.\n */\nexport const summary = (\n readiness: {\n readonly requirements: ReadonlyArray<{ readonly status: Storage.RequirementStatus }>;\n } | null,\n): Summary => {\n const statuses = readiness?.requirements.map(({ status }) => status) ?? [];\n const count = (status: Storage.RequirementStatus): number =>\n statuses.filter((candidate) => candidate === status).length;\n return {\n observed: readiness !== null,\n total: statuses.length,\n satisfied: count(\"satisfied\"),\n missing: count(\"missing\"),\n mismatch: count(\"mismatch\"),\n unknown: count(\"unknown\"),\n };\n};\n\nexport interface PolicyShape {\n /** Delay before the next check, given time since the first pending observation. Default ladder: 15s, 1m, 5m, 30m. */\n readonly backoff: (pendingForMs: number) => number;\n /** Which resolvers must agree for `satisfied`: default `any`. */\n readonly quorum: \"any\" | \"all\" | { readonly minimum: number };\n}\nexport const defaults: PolicyShape = {\n backoff: (pendingForMs) =>\n pendingForMs < 60_000\n ? 15_000\n : pendingForMs < 10 * 60_000\n ? 60_000\n : pendingForMs < 60 * 60_000\n ? 5 * 60_000\n : 30 * 60_000,\n quorum: \"any\",\n};\nexport class Policy extends Context.Reference<PolicyShape>(\"@domainkit/Verify/Policy\", {\n defaultValue: () => defaults,\n}) {}\n\n/** Why readiness was written: a DNS observation, or host evidence merged into the stored row. */\nexport type Cause = \"observe\" | \"evidence\";\n\nexport interface ReadinessChanged {\n /** The tenant the row was written under, from the `Principal` the write ran as. */\n readonly ownerId: string;\n readonly domain: string;\n readonly readiness: Readiness;\n readonly cause: Cause;\n}\n\nexport interface ObserverShape {\n /**\n * Called once per stored readiness, after the write. A host that projects readiness onto its own\n * rows, wakes a durable job at `nextCheckAt`, or notifies a customer hangs it here instead of\n * mirroring the fact at every call site.\n *\n * Readiness is stored per owner and domain, and an observer provided over `DomainKit.layer` is\n * scoped to the layer rather than to one request, so a host that serves more than one tenant\n * keys its projection by `ownerId` and `domain`: the same domain name can be attached in two\n * tenants at once.\n *\n * Events for one domain are not ordered against each other: two observations that overlap both\n * store, last write wins in `Storage`, and their callbacks can finish in either order. A\n * projection compares `readiness.checkedAt`, which is the moment the row was written, and keeps\n * the later one; `latest` remains the authority.\n */\n readonly readinessChanged: (event: ReadinessChanged) => Effect.Effect<void, unknown>;\n}\n\n/**\n * The host seam for \"DomainKit wrote readiness\". The default does nothing; provide one with\n * `Effect.provideService(Verify.Observer, ...)` or `Layer.succeed(Verify.Observer, ...)` over\n * `DomainKit.layer`, the same way `Policy` is overridden.\n *\n * It runs after `storage.readiness.put` returns, outside any transaction, because `Storage`\n * exposes none. A failure or defect in the observer is logged and swallowed: the observation is\n * already durable, and losing it because a projection failed would make the stored fact depend on\n * the host's side effects. A host that needs the projection to be reliable enqueues durable work\n * here rather than doing the work inline.\n */\nexport class Observer extends Context.Reference<ObserverShape>(\"@domainkit/Verify/Observer\", {\n defaultValue: (): ObserverShape => ({ readinessChanged: () => Effect.void }),\n}) {}\n\n// ---------------------------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------------------------\n\nconst StoredEvidence = Schema.Array(Evidence);\n\ninterface ProviderSide {\n readonly provider: string;\n readonly records: ReadonlyArray<DnsRecord.Observed>;\n}\nconst StoredHost = Schema.Array(HostEvidence);\n\n/** A requirement is satisfied only when its exact match also meets its safety constraints. */\nexport const statusAgainst = (\n record: DnsRecord.Model,\n observed: ReadonlyArray<DnsRecord.Observed>,\n): Storage.RequirementStatus => {\n if (collision(record, observed) !== undefined) return \"mismatch\";\n return observed.some((candidate) => DnsRecord.equals(candidate, record))\n ? \"satisfied\"\n : \"missing\";\n};\n\ninterface Observation {\n readonly status: Storage.RequirementStatus;\n readonly values: ReadonlyArray<string>;\n readonly detail: string | null;\n}\n\nconst render = (observed: DnsRecord.Observed): string =>\n observed._tag === \"Opaque\" ? JSON.stringify(observed.raw) : DnsRecord.data(observed);\n\n/** Status plus what the observer holds for the requirement's name and type. */\nconst observation = (\n record: DnsRecord.Model,\n observed: ReadonlyArray<DnsRecord.Observed>,\n): Observation => {\n const status = statusAgainst(record, observed);\n const values = observed.filter((candidate) => DnsRecord.sameSet(candidate, record)).map(render);\n const detail =\n status === \"satisfied\"\n ? null\n : values.length === 0\n ? `no ${record._tag} record at ${record.name}`\n : `expected ${DnsRecord.data(record)}; found ${values.join(\", \")}`;\n return { status, values, detail };\n};\n\nconst combine = (statuses: ReadonlyArray<Storage.RequirementStatus>): Storage.RequirementStatus => {\n const known = statuses.filter((status) => status !== \"unknown\");\n if (known.length === 0) return \"unknown\";\n if (known.includes(\"mismatch\")) return \"mismatch\";\n return known.every((status) => status === \"satisfied\") ? \"satisfied\" : \"missing\";\n};\n\nconst quorumStatus = (\n quorum: PolicyShape[\"quorum\"],\n statuses: ReadonlyArray<Storage.RequirementStatus>,\n): Storage.RequirementStatus => {\n const known = statuses.filter((status) => status !== \"unknown\");\n const satisfied = known.filter((status) => status === \"satisfied\").length;\n const needed =\n quorum === \"any\" ? 1 : quorum === \"all\" ? Math.max(1, known.length) : quorum.minimum;\n if (known.includes(\"mismatch\")) return \"mismatch\";\n if (known.length > 0 && satisfied >= needed) return \"satisfied\";\n return known.length === 0 ? \"unknown\" : \"missing\";\n};\n\nconst overallOf = (\n requirements: ReadonlyArray<{ readonly status: Storage.RequirementStatus }>,\n host: ReadonlyArray<HostEvidence>,\n): Storage.Overall => {\n if (\n requirements.some(({ status }) => status === \"mismatch\") ||\n host.some(({ status }) => status === \"failed\")\n ) {\n return \"failed\";\n }\n const ready =\n requirements.length + host.length > 0 &&\n requirements.every(({ status }) => status === \"satisfied\") &&\n host.every(({ status }) => status === \"ok\");\n return ready ? \"ready\" : \"pending\";\n};\n\n/** The identity of a requirement set: type, name, data, policy, and constraint, independent of labels and order. */\nconst requirementSetKey = (requirements: ReadonlyArray<{ readonly record: DnsRecord.Model }>) =>\n requirements\n .map(\n ({ record }) =>\n `${record._tag} ${record.name} ${DnsRecord.data(record)} ${record.policy} ${record._tag === \"TXT\" ? (record.constraint ?? \"\") : \"\"}`,\n )\n .sort()\n .join(\"\\n\");\n\nexport const make: Effect.Effect<\n Interface,\n never,\n Storage.Service | Connect.Service | Resolver.Service\n> = Effect.gen(function* () {\n const storage = yield* Storage.Service;\n const connect = yield* Connect.Service;\n const resolver = yield* Resolver.Service;\n\n const attachmentFor = (\n input: string,\n ): Fx<{\n readonly domain: DomainName.Model;\n readonly attachment: Storage.Attachment | null;\n }> =>\n Effect.gen(function* () {\n const domain = yield* DomainName.decode(input);\n const attachment = yield* storage.attachments.byDomain(domain);\n return { domain, attachment: Option.getOrNull(attachment) };\n });\n\n const decodeRow = (row: Storage.Readiness): Effect.Effect<Readiness, Errors.DomainKitError> =>\n Effect.gen(function* () {\n const requirements = yield* Effect.forEach(row.requirements, (requirement) =>\n Errors.decode(StoredEvidence, requirement.evidence, \"evidence\").pipe(\n Effect.map((evidence): Requirement => ({\n key: requirementKey(requirement.record),\n operationId: requirement.operationId,\n record: requirement.record,\n status: requirement.status,\n evidence,\n })),\n ),\n );\n const host = yield* Errors.decode(StoredHost, row.host, \"host\");\n return {\n domain: row.domain,\n attachmentId: row.attachmentId,\n overall: row.overall,\n requirements,\n host,\n checkedAt: row.checkedAt,\n nextCheckAt: row.nextCheckAt,\n };\n });\n\n /** Persist readiness, carrying the pending streak forward for the backoff ladder. */\n const store = (input: {\n readonly domain: string;\n readonly attachment: Storage.Attachment | null;\n readonly requirements: ReadonlyArray<Requirement>;\n readonly host: ReadonlyArray<HostEvidence>;\n readonly previous: Option.Option<Storage.Readiness>;\n readonly cause: Cause;\n }): Fx<Readiness> =>\n Effect.gen(function* () {\n const principal = yield* Principal.Service;\n const policy = yield* Policy;\n const observer = yield* Observer;\n const now = yield* DateTime.now;\n const overall = overallOf(input.requirements, input.host);\n const sameRequirements =\n Option.isSome(input.previous) &&\n requirementSetKey(input.previous.value.requirements) ===\n requirementSetKey(input.requirements);\n const pendingSince =\n overall === \"ready\"\n ? null\n : sameRequirements &&\n Option.isSome(input.previous) &&\n input.previous.value.pendingSince !== null\n ? input.previous.value.pendingSince\n : now;\n const nextCheckAt =\n pendingSince === null\n ? null\n : DateTime.addDuration(\n now,\n Duration.millis(\n policy.backoff(DateTime.toEpochMillis(now) - DateTime.toEpochMillis(pendingSince)),\n ),\n );\n const row = new Storage.Readiness({\n domain: input.domain,\n attachmentId: input.attachment?.id ?? null,\n ownerId: principal.ownerId,\n overall,\n requirements: input.requirements.map((requirement) => ({\n operationId: requirement.operationId,\n record: requirement.record,\n status: requirement.status,\n evidence: Schema.encodeSync(StoredEvidence)(requirement.evidence),\n })),\n host: Schema.encodeSync(StoredHost)(input.host),\n pendingSince,\n checkedAt: now,\n nextCheckAt,\n });\n yield* storage.readiness.put(row);\n const readiness: Readiness = {\n domain: row.domain,\n attachmentId: row.attachmentId,\n overall,\n requirements: input.requirements,\n host: input.host,\n checkedAt: now,\n nextCheckAt,\n };\n yield* observer\n .readinessChanged({\n ownerId: principal.ownerId,\n domain: row.domain,\n readiness,\n cause: input.cause,\n })\n .pipe(\n Effect.catchCause((cause) =>\n Effect.logWarning(`Verify.Observer failed for ${row.domain}`, cause),\n ),\n );\n return readiness;\n });\n\n const defaultRequirements = (\n attachment: Storage.Attachment,\n ): Fx<\n ReadonlyArray<{\n readonly operationId: Plan.OperationId | null;\n readonly record: DnsRecord.Model;\n }>\n > =>\n Effect.gen(function* () {\n const latest = yield* storage.attempts.latest(attachment.id, \"provisioning\");\n if (Option.isNone(latest) || latest.value.receipt === null) {\n return yield* Errors.fail(\n new Reason.InvalidInput({\n message: `${attachment.domain} has no provisioning receipt; pass requirements to observe`,\n field: \"requirements\",\n }),\n );\n }\n const receipt = latest.value.receipt;\n const inPlace = new Set(\n receipt.outcomes.flatMap((outcome) =>\n outcome._tag === \"Applied\" || (outcome._tag === \"Skipped\" && outcome.reason === \"noop\")\n ? [outcome.operationId]\n : [],\n ),\n );\n return latest.value.plan.operations\n .filter((operation) => inPlace.has(operation.id))\n .map((operation) => ({ operationId: operation.id, record: operation.record }));\n });\n\n const observe: Interface[\"observe\"] = (input) =>\n Effect.gen(function* () {\n const policy = yield* Policy;\n const { domain, attachment } = yield* attachmentFor(input.domain);\n if (input.requirements === undefined && attachment === null) {\n return yield* Errors.fail(\n new Reason.InvalidInput({\n message: `${domain} is not attached; pass requirements to observe public DNS`,\n field: \"requirements\",\n }),\n );\n }\n const requirements =\n input.requirements === undefined && attachment !== null\n ? yield* defaultRequirements(attachment)\n : (input.requirements ?? []).map((record) => ({ operationId: null, record }));\n if (requirements.length === 0) {\n return yield* Errors.fail(\n new Reason.InvalidInput({\n message: \"Nothing to observe: pass at least one requirement\",\n field: \"requirements\",\n }),\n );\n }\n const now = yield* DateTime.now;\n // Provider readback is evidence when the attachment's session can be built; when it\n // cannot (no attachment, revoked credential, provider outage) public DNS stands alone.\n const providerSide = yield* attachment === null\n ? Effect.succeed(Option.none<ProviderSide>())\n : Effect.gen(function* () {\n const connection = yield* storage.connections.get(attachment.connectionId);\n const authorization = yield* storage.authorizations.get(connection.authorizationId);\n const { session, target } = yield* connect.session(attachment.id);\n const observed = yield* session.dns(target).list(target.zone);\n return Option.some<ProviderSide>({\n provider: authorization.provider,\n records: observed.map(({ record }) => record),\n });\n }).pipe(Effect.catch(() => Effect.succeed(Option.none<ProviderSide>())));\n const observed = yield* Effect.forEach(\n requirements,\n ({ operationId, record }) =>\n Effect.gen(function* () {\n const providerEvidence = Option.isSome(providerSide)\n ? [\n new ProviderEvidence({\n provider: providerSide.value.provider,\n ...observation(record, providerSide.value.records),\n observedAt: now,\n }),\n ]\n : [];\n const outcomes = yield* resolver.resolve(record.name, record._tag);\n const publicEvidence = outcomes.map((outcome) =>\n outcome._tag === \"Answered\"\n ? new PublicDnsEvidence({\n resolver: outcome.answer.resolver,\n ...observation(record, outcome.answer.records),\n observedAt: now,\n })\n : new PublicDnsEvidence({\n resolver: outcome.resolver,\n status: \"unknown\",\n values: [],\n detail:\n outcome._tag === \"TimedOut\"\n ? `${outcome.resolver} timed out`\n : outcome.message,\n observedAt: now,\n }),\n );\n const publicStatus = quorumStatus(\n policy.quorum,\n publicEvidence.map(({ status }) => status),\n );\n const status = combine([\n ...providerEvidence.map((evidence) => evidence.status),\n publicStatus,\n ]);\n return {\n key: requirementKey(record),\n operationId,\n record,\n status,\n evidence: [...providerEvidence, ...publicEvidence],\n } satisfies Requirement;\n }),\n { concurrency: \"unbounded\" },\n );\n const previous = yield* storage.readiness.get(domain);\n const host = Option.isSome(previous)\n ? yield* Errors.decode(StoredHost, previous.value.host, \"host\")\n : [];\n return yield* store({\n domain,\n attachment,\n requirements: observed,\n host,\n previous,\n cause: \"observe\",\n });\n });\n\n const attachEvidence: Interface[\"attachEvidence\"] = (input) =>\n Effect.gen(function* () {\n const { domain, attachment } = yield* attachmentFor(input.domain);\n const previous = yield* storage.readiness.get(domain);\n const current = Option.isSome(previous)\n ? yield* decodeRow(previous.value)\n : {\n requirements: [] as ReadonlyArray<Requirement>,\n host: [] as ReadonlyArray<HostEvidence>,\n };\n const bySource = new Map(current.host.map((evidence) => [evidence.source, evidence]));\n for (const evidence of input.evidence) bySource.set(evidence.source, evidence);\n return yield* store({\n domain,\n attachment,\n requirements: current.requirements,\n host: [...bySource.values()],\n previous,\n cause: \"evidence\",\n });\n });\n\n const latest: Interface[\"latest\"] = (domain) =>\n Effect.gen(function* () {\n const { domain: name } = yield* attachmentFor(domain);\n const stored = yield* storage.readiness.get(name);\n return Option.isSome(stored) ? yield* decodeRow(stored.value) : null;\n });\n\n return { observe, attachEvidence, latest };\n});\n\nexport const layer: Layer.Layer<\n Service,\n never,\n Storage.Service | Connect.Service | Resolver.Service\n> = Layer.effect(Service)(make);\n\nconst accessor =\n <Args extends ReadonlyArray<unknown>, A>(\n pick: (service: Interface) => (...args: Args) => Fx<A>,\n ): ((...args: Args) => Effect.Effect<A, Errors.DomainKitError, Principal.Service | Service>) =>\n (...args) =>\n Effect.flatMap(Service, (service) => pick(service)(...args));\n\nexport const observe = accessor((service) => service.observe);\nexport const attachEvidence = accessor((service) => service.attachEvidence);\nexport const latest = accessor((service) => service.latest);\n"],"mappings":"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"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { Model, Observed, equals
|
|
1
|
+
import { Model, Observed, equals } from "../DnsRecord.mjs";
|
|
2
2
|
import { Conflict, Create, Digest, Kind, Noop, Operation, OperationId, Version } from "../Plan.mjs";
|
|
3
3
|
import { CryptoFailed } from "../Reason.mjs";
|
|
4
4
|
import { DomainKitError } from "./error.mjs";
|
|
5
|
+
import { collision, projectedCollision } from "./reconciliation.mjs";
|
|
5
6
|
import { sha256Hex, stringify } from "./digest.mjs";
|
|
6
7
|
import { Effect, Schema } from "effect";
|
|
7
8
|
//#region src/internal/planner.ts
|
|
@@ -40,31 +41,31 @@ const reconcile = (requirements, existing) => Effect.gen(function* () {
|
|
|
40
41
|
const operations = [];
|
|
41
42
|
for (const record of sorted) {
|
|
42
43
|
const id = yield* operationId(record);
|
|
43
|
-
const
|
|
44
|
+
const incompatible = projectedCollision(record, sorted);
|
|
45
|
+
const operation = incompatible === void 0 ? reconcileOne(id, record, projected) : new Conflict({
|
|
46
|
+
id,
|
|
47
|
+
record,
|
|
48
|
+
...incompatible
|
|
49
|
+
});
|
|
44
50
|
operations.push(operation);
|
|
45
51
|
if (operation._tag === "Create") projected.push(record);
|
|
46
52
|
}
|
|
47
53
|
return operations;
|
|
48
54
|
});
|
|
49
55
|
const reconcileOne = (id, record, existing) => {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
if (exact !== void 0) return new Noop({
|
|
56
|
+
const incompatible = collision(record, existing);
|
|
57
|
+
if (incompatible !== void 0) return new Conflict({
|
|
53
58
|
id,
|
|
54
59
|
record,
|
|
55
|
-
|
|
56
|
-
ttlDrift: exact._tag !== "Opaque" && exact.ttl !== record.ttl
|
|
60
|
+
...incompatible
|
|
57
61
|
});
|
|
58
|
-
const
|
|
62
|
+
const exact = existing.find((candidate) => equals(candidate, record));
|
|
63
|
+
if (exact !== void 0) return new Noop({
|
|
59
64
|
id,
|
|
60
65
|
record,
|
|
61
|
-
existing:
|
|
62
|
-
|
|
66
|
+
existing: exact,
|
|
67
|
+
ttlDrift: exact._tag !== "Opaque" && exact.ttl !== record.ttl
|
|
63
68
|
});
|
|
64
|
-
if (sameName.length > 0 && (record._tag === "CNAME" || sameName.some((candidate) => candidate._tag === "CNAME"))) return conflict("cname-collision", sameName);
|
|
65
|
-
const sameSet$1 = sameName.filter((candidate) => sameSet(candidate, record));
|
|
66
|
-
if (sameSet$1.some((candidate) => candidate._tag === "Opaque")) return conflict("opaque", sameSet$1);
|
|
67
|
-
if (record.policy === "exclusive" && sameSet$1.length > 0) return conflict("exclusive-name", sameSet$1);
|
|
68
69
|
return new Create({
|
|
69
70
|
id,
|
|
70
71
|
record
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"planner.mjs","names":["Plan.Version","Plan.Kind","Plan.Operation","Errors.DomainKitError","Reason.CryptoFailed","
|
|
1
|
+
{"version":3,"file":"planner.mjs","names":["Plan.Version","Plan.Kind","Plan.Operation","Errors.DomainKitError","Reason.CryptoFailed","Plan.Conflict","DnsRecord.equals","Plan.Noop","Plan.Create","DnsRecord.Model","DnsRecord.Observed"],"sources":["../../src/internal/planner.ts"],"sourcesContent":["import { Effect, Schema } from \"effect\";\n\nimport * as DnsRecord from \"../DnsRecord.ts\";\nimport * as Errors from \"./error.ts\";\nimport * as Reason from \"../Reason.ts\";\nimport * as Plan from \"../Plan.ts\";\nimport { collision, projectedCollision } from \"./reconciliation.ts\";\nimport { sha256Hex, stringify } from \"./digest.ts\";\n\nconst Unsigned = Schema.Struct({\n version: Plan.Version,\n kind: Plan.Kind,\n domain: Schema.String,\n zone: Schema.String,\n provider: Schema.String,\n attachmentId: Schema.String,\n operations: Schema.Array(Plan.Operation),\n});\nexport type Unsigned = typeof Unsigned.Type;\n\nexport const operationId = (\n record: DnsRecord.Model,\n): Effect.Effect<Plan.OperationId, Errors.DomainKitError> =>\n sha256Hex(stringify({ record: strip(record) })).pipe(Effect.map(Plan.OperationId.make));\n\n/** The digest covers every operation but not requirement labels, which are display metadata. */\nexport const digest = (unsigned: Unsigned): Effect.Effect<Plan.Digest, Errors.DomainKitError> =>\n Effect.try({\n try: () => {\n const encoded = Schema.encodeSync(Unsigned)(unsigned);\n return stringify({\n ...encoded,\n operations: encoded.operations.map((operation) => ({\n ...operation,\n record: withoutPurpose(operation.record),\n })),\n });\n },\n catch: () =>\n new Errors.DomainKitError({\n reason: new Reason.CryptoFailed({ operation: \"digest\" }),\n }),\n }).pipe(Effect.flatMap(sha256Hex), Effect.map(Plan.Digest.make));\n\nconst withoutPurpose = (record: DnsRecord.Encoded): DnsRecord.Encoded => {\n const { purpose: _purpose, ...rest } = record;\n return rest;\n};\n\n/** Additive, fail-closed reconciliation of requirements against what the provider holds. */\nexport const reconcile = (\n requirements: ReadonlyArray<DnsRecord.Model>,\n existing: ReadonlyArray<DnsRecord.Observed>,\n): Effect.Effect<ReadonlyArray<Plan.Create | Plan.Noop | Plan.Conflict>, Errors.DomainKitError> =>\n Effect.gen(function* () {\n const sorted = [...requirements].sort(compare);\n const projected: Array<DnsRecord.Observed> = [...existing].sort(compare);\n const operations: Array<Plan.Create | Plan.Noop | Plan.Conflict> = [];\n for (const record of sorted) {\n const id = yield* operationId(record);\n const incompatible = projectedCollision(record, sorted);\n const operation =\n incompatible === undefined\n ? reconcileOne(id, record, projected)\n : new Plan.Conflict({ id, record, ...incompatible });\n operations.push(operation);\n if (operation._tag === \"Create\") projected.push(record);\n }\n return operations;\n });\n\nconst reconcileOne = (\n id: Plan.OperationId,\n record: DnsRecord.Model,\n existing: ReadonlyArray<DnsRecord.Observed>,\n): Plan.Create | Plan.Noop | Plan.Conflict => {\n const incompatible = collision(record, existing);\n if (incompatible !== undefined) return new Plan.Conflict({ id, record, ...incompatible });\n const exact = existing.find((candidate) => DnsRecord.equals(candidate, record));\n if (exact !== undefined) {\n return new Plan.Noop({\n id,\n record,\n existing: exact,\n ttlDrift: exact._tag !== \"Opaque\" && exact.ttl !== record.ttl,\n });\n }\n return new Plan.Create({ id, record });\n};\n\n/** Requirements a plan was built from, for re-planning at apply time. */\nexport const requirements = (plan: Plan.Model): ReadonlyArray<DnsRecord.Model> =>\n plan.operations.map((operation) => operation.record);\n\n/** Operation ids hash the encoded requirement without its label, so relabeling never invalidates a plan. */\nfunction strip(record: DnsRecord.Model): DnsRecord.Encoded {\n return withoutPurpose(Schema.encodeSync(DnsRecord.Model)(record));\n}\n\nfunction compare(left: DnsRecord.Observed, right: DnsRecord.Observed): number {\n const encode = Schema.encodeSync(DnsRecord.Observed);\n const leftKey = stringify(encode(left));\n const rightKey = stringify(encode(right));\n return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;\n}\n"],"mappings":";;;;;;;;AASA,MAAM,WAAW,OAAO,OAAO;CAC7B,SAASA;CACT,MAAMC;CACN,QAAQ,OAAO;CACf,MAAM,OAAO;CACb,UAAU,OAAO;CACjB,cAAc,OAAO;CACrB,YAAY,OAAO,MAAMC,SAAc;AACzC,CAAC;AAGD,MAAa,eACX,WAEA,UAAU,UAAU,EAAE,QAAQ,MAAM,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,IAAA,YAAqB,IAAI,CAAC;;AAGxF,MAAa,UAAU,aACrB,OAAO,IAAI;CACT,WAAW;EACT,MAAM,UAAU,OAAO,WAAW,QAAQ,CAAC,CAAC,QAAQ;EACpD,OAAO,UAAU;GACf,GAAG;GACH,YAAY,QAAQ,WAAW,KAAK,eAAe;IACjD,GAAG;IACH,QAAQ,eAAe,UAAU,MAAM;GACzC,EAAE;EACJ,CAAC;CACH;CACA,aACE,IAAIC,eAAsB,EACxB,QAAQ,IAAIC,aAAoB,EAAE,WAAW,SAAS,CAAC,EACzD,CAAC;AACL,CAAC,CAAC,CAAC,KAAK,OAAO,QAAQ,SAAS,GAAG,OAAO,IAAA,OAAgB,IAAI,CAAC;AAEjE,MAAM,kBAAkB,WAAiD;CACvE,MAAM,EAAE,SAAS,UAAU,GAAG,SAAS;CACvC,OAAO;AACT;;AAGA,MAAa,aACX,cACA,aAEA,OAAO,IAAI,aAAa;CACtB,MAAM,SAAS,CAAC,GAAG,YAAY,CAAC,CAAC,KAAK,OAAO;CAC7C,MAAM,YAAuC,CAAC,GAAG,QAAQ,CAAC,CAAC,KAAK,OAAO;CACvE,MAAM,aAA6D,CAAC;CACpE,KAAK,MAAM,UAAU,QAAQ;EAC3B,MAAM,KAAK,OAAO,YAAY,MAAM;EACpC,MAAM,eAAe,mBAAmB,QAAQ,MAAM;EACtD,MAAM,YACJ,iBAAiB,KAAA,IACb,aAAa,IAAI,QAAQ,SAAS,IAClC,IAAIC,SAAc;GAAE;GAAI;GAAQ,GAAG;EAAa,CAAC;EACvD,WAAW,KAAK,SAAS;EACzB,IAAI,UAAU,SAAS,UAAU,UAAU,KAAK,MAAM;CACxD;CACA,OAAO;AACT,CAAC;AAEH,MAAM,gBACJ,IACA,QACA,aAC4C;CAC5C,MAAM,eAAe,UAAU,QAAQ,QAAQ;CAC/C,IAAI,iBAAiB,KAAA,GAAW,OAAO,IAAIA,SAAc;EAAE;EAAI;EAAQ,GAAG;CAAa,CAAC;CACxF,MAAM,QAAQ,SAAS,MAAM,cAAcC,OAAiB,WAAW,MAAM,CAAC;CAC9E,IAAI,UAAU,KAAA,GACZ,OAAO,IAAIC,KAAU;EACnB;EACA;EACA,UAAU;EACV,UAAU,MAAM,SAAS,YAAY,MAAM,QAAQ,OAAO;CAC5D,CAAC;CAEH,OAAO,IAAIC,OAAY;EAAE;EAAI;CAAO,CAAC;AACvC;;AAGA,MAAa,gBAAgB,SAC3B,KAAK,WAAW,KAAK,cAAc,UAAU,MAAM;;AAGrD,SAAS,MAAM,QAA4C;CACzD,OAAO,eAAe,OAAO,WAAWC,KAAe,CAAC,CAAC,MAAM,CAAC;AAClE;AAEA,SAAS,QAAQ,MAA0B,OAAmC;CAC5E,MAAM,SAAS,OAAO,WAAWC,QAAkB;CACnD,MAAM,UAAU,UAAU,OAAO,IAAI,CAAC;CACtC,MAAM,WAAW,UAAU,OAAO,KAAK,CAAC;CACxC,OAAO,UAAU,WAAW,KAAK,UAAU,WAAW,IAAI;AAC5D"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { isSpf } from "./spf.mjs";
|
|
2
|
+
import { equals, sameSet } from "../DnsRecord.mjs";
|
|
3
|
+
//#region src/internal/reconciliation.ts
|
|
4
|
+
/** Safety constraints precede exact matches in both planning and observation. */
|
|
5
|
+
const collision = (record, observed) => {
|
|
6
|
+
const sameName = observed.filter((candidate) => candidate.name === record.name);
|
|
7
|
+
const cnameCollisions = sameName.filter((candidate) => (record._tag === "CNAME" || candidate._tag === "CNAME" || candidate._tag === "Opaque" && candidate.type === "CNAME") && !equals(candidate, record));
|
|
8
|
+
if (cnameCollisions.length > 0) return {
|
|
9
|
+
reason: "cname-collision",
|
|
10
|
+
existing: cnameCollisions
|
|
11
|
+
};
|
|
12
|
+
const sameSet$1 = sameName.filter((candidate) => sameSet(candidate, record));
|
|
13
|
+
if (sameSet$1.some((candidate) => candidate._tag === "Opaque")) return {
|
|
14
|
+
reason: "opaque",
|
|
15
|
+
existing: sameSet$1
|
|
16
|
+
};
|
|
17
|
+
if (record._tag === "TXT" && record.constraint === "spf") {
|
|
18
|
+
const spf = sameSet$1.filter((candidate) => candidate._tag === "TXT" && isSpf(candidate.value));
|
|
19
|
+
if (spf.length > 1 || spf.some((candidate) => !equals(candidate, record))) return {
|
|
20
|
+
reason: "spf-conflict",
|
|
21
|
+
existing: spf
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const incompatible = sameSet$1.filter((candidate) => !equals(candidate, record));
|
|
25
|
+
if (record.policy === "exclusive" && incompatible.length > 0) return {
|
|
26
|
+
reason: "exclusive-name",
|
|
27
|
+
existing: incompatible
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
/** Both sides' constraints apply, regardless of requirement order or canonical sorting. */
|
|
31
|
+
const projectedCollision = (record, requirements) => {
|
|
32
|
+
for (const other of requirements) {
|
|
33
|
+
if (equals(record, other)) continue;
|
|
34
|
+
const forward = collision(record, [other]);
|
|
35
|
+
if (forward !== void 0) return forward;
|
|
36
|
+
const reverse = collision(other, [record]);
|
|
37
|
+
if (reverse !== void 0) return {
|
|
38
|
+
reason: reverse.reason,
|
|
39
|
+
existing: [other]
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
//#endregion
|
|
44
|
+
export { collision, projectedCollision };
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=reconciliation.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconciliation.mjs","names":["DnsRecord.equals","sameSet","DnsRecord.sameSet"],"sources":["../../src/internal/reconciliation.ts"],"sourcesContent":["import * as DnsRecord from \"../DnsRecord.ts\";\nimport type * as Plan from \"../Plan.ts\";\nimport { isSpf } from \"./spf.ts\";\n\ninterface Collision {\n readonly reason: Plan.Conflict[\"reason\"];\n readonly existing: ReadonlyArray<DnsRecord.Observed>;\n}\n\n/** Safety constraints precede exact matches in both planning and observation. */\nexport const collision = (\n record: DnsRecord.Model,\n observed: ReadonlyArray<DnsRecord.Observed>,\n): Collision | undefined => {\n const sameName = observed.filter((candidate) => candidate.name === record.name);\n const cnameCollisions = sameName.filter(\n (candidate) =>\n (record._tag === \"CNAME\" ||\n candidate._tag === \"CNAME\" ||\n (candidate._tag === \"Opaque\" && candidate.type === \"CNAME\")) &&\n !DnsRecord.equals(candidate, record),\n );\n if (cnameCollisions.length > 0) return { reason: \"cname-collision\", existing: cnameCollisions };\n const sameSet = sameName.filter((candidate) => DnsRecord.sameSet(candidate, record));\n if (sameSet.some((candidate) => candidate._tag === \"Opaque\"))\n return { reason: \"opaque\", existing: sameSet };\n if (record._tag === \"TXT\" && record.constraint === \"spf\") {\n const spf = sameSet.filter((candidate) => candidate._tag === \"TXT\" && isSpf(candidate.value));\n if (spf.length > 1 || spf.some((candidate) => !DnsRecord.equals(candidate, record)))\n return { reason: \"spf-conflict\", existing: spf };\n }\n const incompatible = sameSet.filter((candidate) => !DnsRecord.equals(candidate, record));\n if (record.policy === \"exclusive\" && incompatible.length > 0)\n return { reason: \"exclusive-name\", existing: incompatible };\n return undefined;\n};\n\n/** Both sides' constraints apply, regardless of requirement order or canonical sorting. */\nexport const projectedCollision = (\n record: DnsRecord.Model,\n requirements: ReadonlyArray<DnsRecord.Model>,\n): Collision | undefined => {\n for (const other of requirements) {\n if (DnsRecord.equals(record, other)) continue;\n const forward = collision(record, [other]);\n if (forward !== undefined) return forward;\n const reverse = collision(other, [record]);\n if (reverse !== undefined) return { reason: reverse.reason, existing: [other] };\n }\n return undefined;\n};\n"],"mappings":";;;;AAUA,MAAa,aACX,QACA,aAC0B;CAC1B,MAAM,WAAW,SAAS,QAAQ,cAAc,UAAU,SAAS,OAAO,IAAI;CAC9E,MAAM,kBAAkB,SAAS,QAC9B,eACE,OAAO,SAAS,WACf,UAAU,SAAS,WAClB,UAAU,SAAS,YAAY,UAAU,SAAS,YACrD,CAACA,OAAiB,WAAW,MAAM,CACvC;CACA,IAAI,gBAAgB,SAAS,GAAG,OAAO;EAAE,QAAQ;EAAmB,UAAU;CAAgB;CAC9F,MAAMC,YAAU,SAAS,QAAQ,cAAcC,QAAkB,WAAW,MAAM,CAAC;CACnF,IAAID,UAAQ,MAAM,cAAc,UAAU,SAAS,QAAQ,GACzD,OAAO;EAAE,QAAQ;EAAU,UAAUA;CAAQ;CAC/C,IAAI,OAAO,SAAS,SAAS,OAAO,eAAe,OAAO;EACxD,MAAM,MAAMA,UAAQ,QAAQ,cAAc,UAAU,SAAS,SAAS,MAAM,UAAU,KAAK,CAAC;EAC5F,IAAI,IAAI,SAAS,KAAK,IAAI,MAAM,cAAc,CAACD,OAAiB,WAAW,MAAM,CAAC,GAChF,OAAO;GAAE,QAAQ;GAAgB,UAAU;EAAI;CACnD;CACA,MAAM,eAAeC,UAAQ,QAAQ,cAAc,CAACD,OAAiB,WAAW,MAAM,CAAC;CACvF,IAAI,OAAO,WAAW,eAAe,aAAa,SAAS,GACzD,OAAO;EAAE,QAAQ;EAAkB,UAAU;CAAa;AAE9D;;AAGA,MAAa,sBACX,QACA,iBAC0B;CAC1B,KAAK,MAAM,SAAS,cAAc;EAChC,IAAIA,OAAiB,QAAQ,KAAK,GAAG;EACrC,MAAM,UAAU,UAAU,QAAQ,CAAC,KAAK,CAAC;EACzC,IAAI,YAAY,KAAA,GAAW,OAAO;EAClC,MAAM,UAAU,UAAU,OAAO,CAAC,MAAM,CAAC;EACzC,IAAI,YAAY,KAAA,GAAW,OAAO;GAAE,QAAQ,QAAQ;GAAQ,UAAU,CAAC,KAAK;EAAE;CAChF;AAEF"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/internal/spf.ts
|
|
2
|
+
/** RFC 7208 version selection only; full SPF policy validation belongs to the host. */
|
|
3
|
+
const isSpf = (value) => value.slice(0, 6).toLowerCase() === "v=spf1" && (value.length === 6 || value[6] === " ");
|
|
4
|
+
//#endregion
|
|
5
|
+
export { isSpf };
|
|
6
|
+
|
|
7
|
+
//# sourceMappingURL=spf.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spf.mjs","names":[],"sources":["../../src/internal/spf.ts"],"sourcesContent":["/** RFC 7208 version selection only; full SPF policy validation belongs to the host. */\nexport const isSpf = (value: string): boolean =>\n value.slice(0, 6).toLowerCase() === \"v=spf1\" && (value.length === 6 || value[6] === \" \");\n"],"mappings":";;AACA,MAAa,SAAS,UACpB,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,YAAY,MAAM,aAAa,MAAM,WAAW,KAAK,MAAM,OAAO"}
|
package/dist/package.mjs
CHANGED
|
@@ -38,12 +38,14 @@ declare const CNAME_base: Schema.Class<CNAME, Schema.TaggedStruct<"CNAME", {
|
|
|
38
38
|
}>, {}>;
|
|
39
39
|
export declare class CNAME extends CNAME_base {
|
|
40
40
|
}
|
|
41
|
-
declare const TXT_base: Schema.Class<TXT, Schema.
|
|
41
|
+
declare const TXT_base: Schema.Class<TXT, Schema.Struct<{
|
|
42
|
+
readonly _tag: Schema.tag<"TXT">;
|
|
42
43
|
readonly name: Schema.suspend<Schema.decodeTo<Schema.brand<Schema.String, "@domainkit/DomainName">, Schema.String, never, never>>;
|
|
43
44
|
readonly ttl: Schema.NullOr<Schema.Int>;
|
|
44
45
|
readonly policy: Schema.Literals<readonly ["exclusive", "append"]>;
|
|
45
46
|
readonly purpose: Schema.optionalKey<Schema.String>;
|
|
46
47
|
readonly value: Schema.String;
|
|
48
|
+
readonly constraint: Schema.optionalKey<Schema.Literal<"spf">>;
|
|
47
49
|
}>, {}>;
|
|
48
50
|
export declare class TXT extends TXT_base {
|
|
49
51
|
}
|
|
@@ -123,6 +125,11 @@ export declare const txt: (input: {
|
|
|
123
125
|
readonly name: string;
|
|
124
126
|
readonly value: string;
|
|
125
127
|
} & Options) => TXT;
|
|
128
|
+
/** A TXT requirement that permits unrelated TXT but requires a single matching SPF record. */
|
|
129
|
+
export declare const spf: (input: {
|
|
130
|
+
readonly name: string;
|
|
131
|
+
readonly value: string;
|
|
132
|
+
} & Omit<Options, "policy">) => TXT;
|
|
126
133
|
export declare const mx: (input: {
|
|
127
134
|
readonly name: string;
|
|
128
135
|
readonly exchange: string;
|
|
@@ -149,8 +156,8 @@ export declare const isDnsRecord: (input: unknown) => input is Model;
|
|
|
149
156
|
/** The record's data portion as a canonical string, for display and zone-file rendering. */
|
|
150
157
|
export declare const data: (record: Model) => string;
|
|
151
158
|
/**
|
|
152
|
-
* Structural equality over type, name, and data. `ttl`, `policy`,
|
|
153
|
-
*
|
|
159
|
+
* Structural equality over type, name, and data. Requirement metadata (`ttl`, `policy`,
|
|
160
|
+
* `constraint`, and `purpose`) is ignored; TTL drift is reported on plan operations.
|
|
154
161
|
*/
|
|
155
162
|
export declare const equals: (left: Observed, right: Observed) => boolean;
|
|
156
163
|
/** `true` when both records occupy the same name and type, regardless of data. */
|
package/dist/types/Plan.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ declare const Conflict_base: Schema.Class<Conflict, Schema.TaggedStruct<"Conflic
|
|
|
33
33
|
readonly id: Schema.brand<Schema.String, "@domainkit/OperationId">;
|
|
34
34
|
readonly record: Schema.suspend<Schema.Union<readonly [typeof DnsRecord.A, typeof DnsRecord.AAAA, typeof DnsRecord.CNAME, typeof DnsRecord.TXT, typeof DnsRecord.MX, typeof DnsRecord.CAA, typeof DnsRecord.NS, typeof DnsRecord.SRV]>>;
|
|
35
35
|
readonly existing: Schema.$Array<Schema.suspend<Schema.Union<readonly [Schema.Union<readonly [typeof DnsRecord.A, typeof DnsRecord.AAAA, typeof DnsRecord.CNAME, typeof DnsRecord.TXT, typeof DnsRecord.MX, typeof DnsRecord.CAA, typeof DnsRecord.NS, typeof DnsRecord.SRV]>, typeof DnsRecord.Opaque]>>>;
|
|
36
|
-
readonly reason: Schema.Literals<readonly ["exclusive-name", "cname-collision", "value-mismatch", "opaque", "missing"]>;
|
|
36
|
+
readonly reason: Schema.Literals<readonly ["exclusive-name", "spf-conflict", "cname-collision", "value-mismatch", "opaque", "missing"]>;
|
|
37
37
|
}>, {}>;
|
|
38
38
|
export declare class Conflict extends Conflict_base {
|
|
39
39
|
}
|