domainkit 0.1.0 → 0.2.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.
Files changed (68) hide show
  1. package/README.md +91 -71
  2. package/dist/{auth-Vi-hhXbV.mjs → auth-CZ1pZHMZ.mjs} +301 -38
  3. package/dist/auth-CZ1pZHMZ.mjs.map +1 -0
  4. package/dist/{client-BnD7QLRF.mjs → auth-CzhZ3Cuc.mjs} +235 -131
  5. package/dist/auth-CzhZ3Cuc.mjs.map +1 -0
  6. package/dist/authorization-DOg2Re1D.mjs +247 -0
  7. package/dist/authorization-DOg2Re1D.mjs.map +1 -0
  8. package/dist/canonical-json-CpvqGMcr.mjs +57 -0
  9. package/dist/canonical-json-CpvqGMcr.mjs.map +1 -0
  10. package/dist/{cloudflare-C6ryPlCZ.mjs → cloudflare-BedFw4cX.mjs} +28 -7
  11. package/dist/cloudflare-BedFw4cX.mjs.map +1 -0
  12. package/dist/cloudflare-FDl5q7Dm.d.mts +32 -0
  13. package/dist/cloudflare.d.mts +3 -3
  14. package/dist/cloudflare.mjs +2 -2
  15. package/dist/connection-BWrBwqjk.mjs +58 -0
  16. package/dist/connection-BWrBwqjk.mjs.map +1 -0
  17. package/dist/connection-CEqadXSm.d.mts +182 -0
  18. package/dist/connection-CcQokgrd.mjs +186 -0
  19. package/dist/connection-CcQokgrd.mjs.map +1 -0
  20. package/dist/connection-api-DftK7eHo.d.mts +7 -0
  21. package/dist/{dns-record-C_KwGPLf.mjs → dns-record-BLeqo56C.mjs} +23 -3
  22. package/dist/dns-record-BLeqo56C.mjs.map +1 -0
  23. package/dist/effect-cloudflare.d.mts +2 -2
  24. package/dist/effect-cloudflare.mjs +2 -2
  25. package/dist/effect-vercel.d.mts +2 -2
  26. package/dist/effect-vercel.mjs +2 -2
  27. package/dist/effect.d.mts +9 -46
  28. package/dist/effect.mjs +29 -7
  29. package/dist/effect.mjs.map +1 -1
  30. package/dist/index-B1opsgls.d.mts +159 -0
  31. package/dist/{index-CEzk2gbZ.d.mts → index-Ewc__bQu.d.mts} +58 -7
  32. package/dist/index.d.mts +169 -113
  33. package/dist/index.mjs +201 -138
  34. package/dist/index.mjs.map +1 -1
  35. package/dist/{resolver-BHPsADzE.d.mts → resolver-CJOR9mQ8.d.mts} +620 -263
  36. package/dist/resolver-CxE-VDw4.mjs +598 -0
  37. package/dist/resolver-CxE-VDw4.mjs.map +1 -0
  38. package/dist/testing.d.mts +53 -44
  39. package/dist/testing.mjs +379 -107
  40. package/dist/testing.mjs.map +1 -1
  41. package/dist/{vercel-DD89DCqH.mjs → vercel-CWZ33NUS.mjs} +23 -3
  42. package/dist/vercel-CWZ33NUS.mjs.map +1 -0
  43. package/dist/vercel-DgJaUX9D.d.mts +31 -0
  44. package/dist/vercel.d.mts +3 -3
  45. package/dist/vercel.mjs +2 -2
  46. package/dist/verify-BzpiA0Es.mjs +642 -0
  47. package/dist/verify-BzpiA0Es.mjs.map +1 -0
  48. package/dist/verify-DZWLM91T.d.mts +805 -0
  49. package/dist/zone-discovery-BA5I24vu.d.mts +1390 -0
  50. package/dist/zones-DKHTpX9C.mjs +19 -0
  51. package/dist/zones-DKHTpX9C.mjs.map +1 -0
  52. package/package.json +1 -1
  53. package/dist/auth-Vi-hhXbV.mjs.map +0 -1
  54. package/dist/client-BnD7QLRF.mjs.map +0 -1
  55. package/dist/cloudflare-C6ryPlCZ.mjs.map +0 -1
  56. package/dist/cloudflare-tdjNS8vE.d.mts +0 -23
  57. package/dist/dns-record-C_KwGPLf.mjs.map +0 -1
  58. package/dist/index-CS9Rl5LQ.d.mts +0 -71
  59. package/dist/manifest-CP85ePFn.d.mts +0 -583
  60. package/dist/receipt-CGSnfZQ6.mjs +0 -28
  61. package/dist/receipt-CGSnfZQ6.mjs.map +0 -1
  62. package/dist/resolver-tEXpKxmH.mjs +0 -200
  63. package/dist/resolver-tEXpKxmH.mjs.map +0 -1
  64. package/dist/vercel-D-E2aqb1.d.mts +0 -22
  65. package/dist/vercel-DD89DCqH.mjs.map +0 -1
  66. package/dist/verify-B80xCBh2.d.mts +0 -1407
  67. package/dist/verify-BjrR_4NF.mjs +0 -1401
  68. package/dist/verify-BjrR_4NF.mjs.map +0 -1
@@ -0,0 +1,598 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { E as decode$1, T as Schema$2, _ as equals, b as Service$2, h as decode$2, k as Error$2, m as data, p as Type, s as Observed, u as Schema$3 } from "./dns-record-BLeqo56C.mjs";
3
+ import { t as candidates } from "./zones-DKHTpX9C.mjs";
4
+ import { n as sha256Encoded, r as stringify } from "./canonical-json-CpvqGMcr.mjs";
5
+ import { Cause, Clock, Context, Data, Effect, Layer, Schema } from "effect";
6
+ //#region src/discovery/zone-discovery.ts
7
+ var zone_discovery_exports = /* @__PURE__ */ __exportAll({
8
+ Candidate: () => Candidate,
9
+ Outcome: () => Outcome,
10
+ Service: () => Service$1,
11
+ make: () => make
12
+ });
13
+ /** Display-safe identity and nameserver evidence for one provider-owned zone. */
14
+ const Candidate = Schema.Struct({
15
+ accountId: Schema.String,
16
+ id: Schema.String,
17
+ name: Schema$2,
18
+ nameservers: Schema.Array(Schema$2),
19
+ providerId: Schema.String,
20
+ status: Schema.Literals([
21
+ "active",
22
+ "pending",
23
+ "unknown"
24
+ ])
25
+ });
26
+ const Outcome = Data.taggedEnum();
27
+ var Service$1 = class extends Context.Service()("@domainkit/ZoneDiscovery") {};
28
+ /** Compose provider-account discovery sources without expanding the record-provider contract. */
29
+ function make(sources) {
30
+ return Service$1.of({ discover: Effect.fn("ZoneDiscovery.discover")(function* (domain) {
31
+ for (const name of candidates(domain)) {
32
+ const matches = deduplicate((yield* Effect.forEach(sources, (source) => source.listZones(name).pipe(Effect.map((zones) => zones.filter((zone) => zone.name === name).map((zone) => ({
33
+ candidate: {
34
+ ...zone,
35
+ providerId: source.provider.id
36
+ },
37
+ provider: source.provider
38
+ })))), { concurrency: "unbounded" })).flat());
39
+ const match = matches[0];
40
+ if (matches.length === 1 && match !== void 0) return Outcome.Resolved(match);
41
+ if (matches.length > 1) return Outcome.SelectionRequired({ candidates: matches.map(({ candidate }) => candidate) });
42
+ }
43
+ return Outcome.NotFound({ domain });
44
+ }) });
45
+ }
46
+ function deduplicate(matches) {
47
+ const unique = /* @__PURE__ */ new Map();
48
+ for (const match of matches) {
49
+ const key = [
50
+ match.candidate.providerId,
51
+ match.candidate.accountId,
52
+ match.candidate.id,
53
+ match.candidate.name
54
+ ].join(":");
55
+ if (unique.get(key) === void 0) unique.set(key, match);
56
+ }
57
+ return [...unique.values()].sort((left, right) => candidateKey(left.candidate).localeCompare(candidateKey(right.candidate)));
58
+ }
59
+ function candidateKey(candidate) {
60
+ return [
61
+ candidate.name,
62
+ candidate.providerId,
63
+ candidate.accountId,
64
+ candidate.id
65
+ ].join(":");
66
+ }
67
+ //#endregion
68
+ //#region src/plan/types.ts
69
+ var types_exports = /* @__PURE__ */ __exportAll({
70
+ Authorization: () => Authorization$1,
71
+ Operation: () => Operation$1,
72
+ Receipt: () => Receipt$1,
73
+ Schema: () => Schema$1,
74
+ decode: () => decode,
75
+ decodeReceipt: () => decodeReceipt,
76
+ encode: () => encode,
77
+ encodeReceipt: () => encodeReceipt
78
+ });
79
+ const Operation$1 = Schema.TaggedUnion({
80
+ create: {
81
+ id: Schema.String,
82
+ requirement: Schema$3
83
+ },
84
+ noop: {
85
+ id: Schema.String,
86
+ requirement: Schema$3,
87
+ ttlDrift: Schema.Boolean
88
+ },
89
+ conflict: {
90
+ existing: Schema.Array(Observed),
91
+ id: Schema.String,
92
+ reason: Schema.String,
93
+ requirement: Schema$3
94
+ }
95
+ });
96
+ const Schema$1 = Schema.Struct({
97
+ digest: Schema.String,
98
+ operations: Schema.Array(Operation$1),
99
+ providerId: Schema.String,
100
+ version: Schema.Literal("domainkit.dns-plan.v1"),
101
+ zone: Schema$2
102
+ });
103
+ const Authorization$1 = Schema.Struct({
104
+ allowPartial: Schema.Boolean,
105
+ operationIds: Schema.Array(Schema.String),
106
+ planDigest: Schema.String,
107
+ version: Schema.Literal("domainkit.plan-authorization.v1")
108
+ });
109
+ const Receipt$1 = Schema.Struct({
110
+ appliedAt: Schema.DateFromString,
111
+ operations: Schema.Array(Schema.Struct({
112
+ operationId: Schema.String,
113
+ providerRecordId: Schema.NullOr(Schema.String)
114
+ })),
115
+ planDigest: Schema.String,
116
+ providerId: Schema.String,
117
+ status: Schema.Literals(["complete", "partial"]),
118
+ version: Schema.Literal("domainkit.apply-receipt.v1"),
119
+ zone: Schema$2
120
+ });
121
+ const decode = Effect.fn("DnsPlan.decode")((input) => Schema.decodeUnknownEffect(Schema$1)(input).pipe(Effect.mapError((cause) => new Error$2({ message: cause.message }))));
122
+ const encode = Schema.encodeSync(Schema$1);
123
+ const decodeReceipt = Effect.fn("DnsPlan.decodeReceipt")((input) => Schema.decodeUnknownEffect(Receipt$1)(input).pipe(Effect.mapError((cause) => new Error$2({ message: cause.message }))));
124
+ const encodeReceipt = Schema.encodeSync(Receipt$1);
125
+ //#endregion
126
+ //#region src/plan/plan.ts
127
+ var plan_exports = /* @__PURE__ */ __exportAll({
128
+ AuthorizationError: () => AuthorizationError,
129
+ ConflictError: () => ConflictError,
130
+ CreateResult: () => CreateResult,
131
+ PartialApplyError: () => PartialApplyError,
132
+ StaleError: () => StaleError,
133
+ Target: () => Target,
134
+ apply: () => apply$1,
135
+ authorize: () => authorize$1,
136
+ create: () => create$1,
137
+ renderManualInstructions: () => renderManualInstructions,
138
+ validate: () => validate$1
139
+ });
140
+ const UnsignedPlan$1 = Schema.Struct({
141
+ operations: Schema.Array(Operation$1),
142
+ providerId: Schema.String,
143
+ version: Schema.Literal("domainkit.dns-plan.v1"),
144
+ zone: Schema$2
145
+ });
146
+ const RequirementDigest = Schema.Struct({ requirement: Schema$3 });
147
+ var ConflictError = class extends Schema.TaggedError()("PlanConflictError", { operationIds: Schema.Array(Schema.String) }) {};
148
+ var AuthorizationError = class extends Schema.TaggedError()("AuthorizationError", { message: Schema.String }) {};
149
+ var StaleError = class extends Schema.TaggedError()("StalePlanError", {
150
+ approvedPlanDigest: Schema.String,
151
+ currentPlanDigest: Schema.String
152
+ }) {};
153
+ var PartialApplyError = class extends Schema.TaggedError()("PartialApplyError", {
154
+ causeTag: Schema.Literals([
155
+ "CryptoError",
156
+ "InvalidInputError",
157
+ "Interrupted",
158
+ "ProviderError",
159
+ "StalePlanError"
160
+ ]),
161
+ failedOperationId: Schema.String,
162
+ message: Schema.String,
163
+ receipt: Receipt$1
164
+ }) {};
165
+ const Target = Data.taggedEnum();
166
+ const CreateResult = Data.taggedEnum();
167
+ function create$1(input) {
168
+ const target = input.target;
169
+ if (target._tag === "ExactZone") return Effect.gen(function* () {
170
+ const provider = yield* Service$2;
171
+ const zone = yield* decode$1(target.zone);
172
+ return CreateResult.Resolved({
173
+ candidate: null,
174
+ plan: yield* planAgainst(provider, zone, input.requirements)
175
+ });
176
+ }).pipe(Effect.withSpan("Provisioning.create"));
177
+ return Effect.gen(function* () {
178
+ const discovery = yield* Service$1;
179
+ const domain = yield* decode$1(target.domain);
180
+ const outcome = yield* discovery.discover(domain);
181
+ switch (outcome._tag) {
182
+ case "NotFound": return CreateResult.NotFound({ domain: outcome.domain });
183
+ case "SelectionRequired": return CreateResult.SelectionRequired({ candidates: outcome.candidates });
184
+ case "Resolved": return CreateResult.Resolved({
185
+ candidate: outcome.candidate,
186
+ plan: yield* planAgainst(outcome.provider, outcome.candidate.name, input.requirements)
187
+ });
188
+ }
189
+ }).pipe(Effect.withSpan("Provisioning.create"));
190
+ }
191
+ const planAgainst = Effect.fn("Provisioning.planAgainst")(function* (provider, zone, inputs) {
192
+ const requirements = (yield* Effect.forEach(inputs, decode$2, { concurrency: "unbounded" })).sort(compareRecords);
193
+ const existing = [...yield* provider.listRecords(zone)].sort(compareRecords);
194
+ const { operations } = yield* Effect.reduce(requirements, () => ({
195
+ operations: [],
196
+ projected: existing
197
+ }), (state, requirement) => reconcileRequirement(requirement, state.projected).pipe(Effect.map((operation) => ({
198
+ operations: [...state.operations, operation],
199
+ projected: operation._tag === "create" ? [...state.projected, operation.requirement].sort(compareRecords) : state.projected
200
+ }))));
201
+ const unsigned = {
202
+ operations,
203
+ providerId: provider.id,
204
+ version: "domainkit.dns-plan.v1",
205
+ zone
206
+ };
207
+ return {
208
+ ...unsigned,
209
+ digest: yield* sha256Encoded(UnsignedPlan$1, unsigned)
210
+ };
211
+ });
212
+ /** Authorizes all or a selected subset of create operations in a digest-bound plan. */
213
+ const authorize$1 = Effect.fn("Provisioning.authorize")(function* (plan, operationIds, options = {}) {
214
+ yield* validate$1(plan);
215
+ const createIds = plan.operations.filter((operation) => operation._tag === "create").map(({ id }) => id);
216
+ const selected = [...new Set(operationIds ?? createIds)].sort();
217
+ const unknown = selected.filter((id) => !createIds.includes(id));
218
+ if (unknown.length > 0) return yield* new AuthorizationError({ message: `Unknown operation IDs: ${unknown.join(", ")}` });
219
+ if (!options.allowPartial && selected.length !== createIds.length) return yield* new AuthorizationError({ message: "Partial authorization requires allowPartial" });
220
+ return {
221
+ allowPartial: options.allowPartial ?? false,
222
+ operationIds: selected,
223
+ planDigest: plan.digest,
224
+ version: "domainkit.plan-authorization.v1"
225
+ };
226
+ });
227
+ /** Applies approved creates sequentially after full and per-operation revalidation. */
228
+ const apply$1 = Effect.fn("Provisioning.apply")(function* (input) {
229
+ const { authorization, plan } = input;
230
+ const provider = yield* Service$2;
231
+ yield* validate$1(plan);
232
+ if (authorization.planDigest !== plan.digest) return yield* new AuthorizationError({ message: "Authorization belongs to a different plan" });
233
+ if (provider.id !== plan.providerId) return yield* new AuthorizationError({ message: "Provider does not match the approved plan" });
234
+ const conflicts = plan.operations.filter((operation) => operation._tag === "conflict");
235
+ if (conflicts.length > 0) return yield* new ConflictError({ operationIds: conflicts.map(({ id }) => id) });
236
+ const creates = plan.operations.filter(Operation$1.guards.create);
237
+ const approved = new Set(authorization.operationIds);
238
+ const unknown = authorization.operationIds.filter((id) => !creates.some((operation) => operation.id === id));
239
+ if (unknown.length > 0) return yield* new AuthorizationError({ message: `Authorization contains unknown operations: ${unknown.join(", ")}` });
240
+ if (!authorization.allowPartial && creates.some(({ id }) => !approved.has(id))) return yield* new AuthorizationError({ message: "Authorization does not cover every create operation" });
241
+ const currentPlan = yield* planAgainst(provider, plan.zone, plan.operations.map(({ requirement }) => requirement));
242
+ if (currentPlan.digest !== plan.digest) return yield* new StaleError({
243
+ approvedPlanDigest: plan.digest,
244
+ currentPlanDigest: currentPlan.digest
245
+ });
246
+ const operations = [];
247
+ for (const operation of creates) {
248
+ if (!approved.has(operation.id)) continue;
249
+ yield* Effect.gen(function* () {
250
+ yield* assertStillCreatable(plan, operation);
251
+ const result = yield* provider.createRecord(plan.zone, operation.requirement);
252
+ operations.push({
253
+ operationId: operation.id,
254
+ providerRecordId: result.providerRecordId
255
+ });
256
+ }).pipe(Effect.catch((failure) => operations.length === 0 ? Effect.fail(failure) : makeReceipt$1(input, operations, "partial").pipe(Effect.flatMap((receipt) => Effect.fail(new PartialApplyError({
257
+ causeTag: failure._tag,
258
+ failedOperationId: operation.id,
259
+ message: failureMessage(failure),
260
+ receipt
261
+ }))))), Effect.catchCause((cause) => {
262
+ if (!Cause.hasInterruptsOnly(cause) || operations.length === 0) return Effect.failCause(cause);
263
+ return makeReceipt$1(input, operations, "partial").pipe(Effect.flatMap((receipt) => Effect.fail(new PartialApplyError({
264
+ causeTag: "Interrupted",
265
+ failedOperationId: operation.id,
266
+ message: "DNS apply was interrupted after confirmed writes",
267
+ receipt
268
+ }))));
269
+ }));
270
+ }
271
+ return yield* makeReceipt$1(input, operations, "complete");
272
+ });
273
+ function renderManualInstructions(plan) {
274
+ return plan.operations.map((operation) => {
275
+ const record = operation.requirement;
276
+ const data$1 = Object.entries(data(record)).map(([key, value]) => `${key}=${value}`).join(" ");
277
+ return `${operation._tag.toUpperCase()} ${record._tag} ${record.name} ${data$1}`.trim();
278
+ });
279
+ }
280
+ function makeReceipt$1(input, operations, status) {
281
+ return Clock.currentTimeMillis.pipe(Effect.map((now) => ({
282
+ appliedAt: new Date(now),
283
+ operations,
284
+ planDigest: input.plan.digest,
285
+ providerId: input.plan.providerId,
286
+ status,
287
+ version: "domainkit.apply-receipt.v1",
288
+ zone: input.plan.zone
289
+ })));
290
+ }
291
+ function assertStillCreatable(plan, operation) {
292
+ return Service$2.pipe(Effect.flatMap((provider) => planAgainst(provider, plan.zone, [operation.requirement])), Effect.flatMap((currentPlan) => {
293
+ const currentOperation = currentPlan.operations[0];
294
+ return currentOperation?._tag === "create" && currentOperation.id === operation.id ? Effect.void : Effect.fail(new StaleError({
295
+ approvedPlanDigest: plan.digest,
296
+ currentPlanDigest: currentPlan.digest
297
+ }));
298
+ }));
299
+ }
300
+ function reconcileRequirement(requirement, allExisting) {
301
+ return Effect.gen(function* () {
302
+ const sameName = allExisting.filter((record) => record.name === requirement.name);
303
+ const sameSet = sameName.filter((record) => record._tag !== "Opaque" && record._tag === requirement._tag);
304
+ const exact = sameSet.find((record) => equals(record, requirement));
305
+ const id = yield* sha256Encoded(RequirementDigest, { requirement });
306
+ if (exact !== void 0) return {
307
+ _tag: "noop",
308
+ id,
309
+ requirement,
310
+ ttlDrift: exact.ttl !== requirement.ttl
311
+ };
312
+ if (sameName.length > 0 && (requirement._tag === "CNAME" || sameName.some((record) => record._tag === "CNAME"))) return {
313
+ _tag: "conflict",
314
+ existing: sameName,
315
+ id,
316
+ reason: "CNAME records cannot coexist with other records at the same name",
317
+ requirement
318
+ };
319
+ if (sameSet.length > 0 && (requirement.policy === "exclusive" || sameSet.some(({ policy }) => policy === "exclusive"))) return {
320
+ _tag: "conflict",
321
+ existing: sameSet,
322
+ id,
323
+ reason: "The exclusive record set already contains incompatible data",
324
+ requirement
325
+ };
326
+ return {
327
+ _tag: "create",
328
+ id,
329
+ requirement
330
+ };
331
+ });
332
+ }
333
+ function validate$1(plan) {
334
+ const unsigned = {
335
+ operations: plan.operations,
336
+ providerId: plan.providerId,
337
+ version: plan.version,
338
+ zone: plan.zone
339
+ };
340
+ return sha256Encoded(UnsignedPlan$1, unsigned).pipe(Effect.flatMap((digest) => digest === plan.digest ? Effect.void : Effect.fail(new AuthorizationError({ message: "Plan digest does not match its contents" }))));
341
+ }
342
+ function failureMessage(failure) {
343
+ return failure._tag === "StalePlanError" ? "DNS state changed while applying the approved plan" : failure.message;
344
+ }
345
+ function compareRecords(left, right) {
346
+ const encode = Schema.encodeSync(Observed);
347
+ const toJson = Schema.decodeUnknownSync(Schema.Json);
348
+ const leftJson = stringify(toJson(encode(left)));
349
+ const rightJson = stringify(toJson(encode(right)));
350
+ return leftJson < rightJson ? -1 : leftJson > rightJson ? 1 : 0;
351
+ }
352
+ //#endregion
353
+ //#region src/plan/deletion.ts
354
+ var deletion_exports = /* @__PURE__ */ __exportAll({
355
+ Authorization: () => Authorization,
356
+ Error: () => Error$1,
357
+ Operation: () => Operation,
358
+ PartialError: () => PartialError,
359
+ Plan: () => Plan,
360
+ Receipt: () => Receipt,
361
+ apply: () => apply,
362
+ authorize: () => authorize,
363
+ create: () => create,
364
+ validate: () => validate
365
+ });
366
+ const Operation = Schema.Struct({
367
+ id: Schema.String,
368
+ providerRecordId: Schema.String,
369
+ record: Schema$3
370
+ });
371
+ const UnsignedPlan = Schema.Struct({
372
+ createdAt: Schema.DateFromString,
373
+ expiresAt: Schema.DateFromString,
374
+ operations: Schema.Array(Operation),
375
+ providerId: Schema.String,
376
+ sourcePlanDigest: Schema.String,
377
+ version: Schema.Literal("domainkit.deletion-plan.v1"),
378
+ zone: Schema$2
379
+ });
380
+ const Plan = Schema.Struct({
381
+ ...UnsignedPlan.fields,
382
+ digest: Schema.String
383
+ });
384
+ const Authorization = Schema.Struct({
385
+ operationIds: Schema.Array(Schema.String),
386
+ planDigest: Schema.String,
387
+ version: Schema.Literal("domainkit.deletion-authorization.v1")
388
+ });
389
+ const Receipt = Schema.Struct({
390
+ deletedAt: Schema.DateFromString,
391
+ operations: Schema.Array(Schema.Struct({
392
+ operationId: Schema.String,
393
+ providerRecordId: Schema.String
394
+ })),
395
+ planDigest: Schema.String,
396
+ providerId: Schema.String,
397
+ status: Schema.Literals(["complete", "partial"]),
398
+ version: Schema.Literal("domainkit.deletion-receipt.v1"),
399
+ zone: Schema$2
400
+ });
401
+ var Error$1 = class extends Schema.TaggedError()("UnsafeDeletionError", {
402
+ message: Schema.String,
403
+ operationId: Schema.optionalKey(Schema.String)
404
+ }) {};
405
+ var PartialError = class extends Schema.TaggedError()("PartialDeletionError", {
406
+ failedOperationId: Schema.String,
407
+ message: Schema.String,
408
+ receipt: Receipt
409
+ }) {};
410
+ /** Build a fresh deletion plan only from records proven by a prior create receipt. */
411
+ const create = Effect.fn("Deletion.create")(function* (input) {
412
+ const provider = yield* Service$2;
413
+ yield* validate$1(input.plan);
414
+ if (input.receipt.planDigest !== input.plan.digest || input.receipt.providerId !== input.plan.providerId || input.receipt.zone !== input.plan.zone) return yield* new Error$1({ message: "Create receipt does not belong to the source plan" });
415
+ if (provider.id !== input.plan.providerId) return yield* new Error$1({ message: "Provider does not match the source receipt" });
416
+ const operations = yield* Effect.forEach(input.receipt.operations, (applied) => Effect.gen(function* () {
417
+ const source = input.plan.operations.find((operation) => operation._tag === "create" && operation.id === applied.operationId);
418
+ if (source === void 0 || source._tag !== "create") return yield* new Error$1({
419
+ message: "Receipt operation is not a created source record",
420
+ operationId: applied.operationId
421
+ });
422
+ if (applied.providerRecordId === null) return yield* new Error$1({
423
+ message: "Provider record ID is required for safe deletion",
424
+ operationId: applied.operationId
425
+ });
426
+ yield* assertExact(provider, input.plan.zone, applied.providerRecordId, source.requirement, source.id);
427
+ return {
428
+ id: source.id,
429
+ providerRecordId: applied.providerRecordId,
430
+ record: source.requirement
431
+ };
432
+ }));
433
+ const now = yield* Clock.currentTimeMillis;
434
+ const unsigned = {
435
+ createdAt: new Date(now),
436
+ expiresAt: new Date(now + (input.ttlMs ?? 9e5)),
437
+ operations,
438
+ providerId: input.plan.providerId,
439
+ sourcePlanDigest: input.plan.digest,
440
+ version: "domainkit.deletion-plan.v1",
441
+ zone: input.plan.zone
442
+ };
443
+ return {
444
+ ...unsigned,
445
+ digest: yield* sha256Encoded(UnsignedPlan, unsigned)
446
+ };
447
+ });
448
+ /** Produce a separate digest-bound consent artifact for destructive operations. */
449
+ const authorize = Effect.fn("Deletion.authorize")(function* (plan, operationIds) {
450
+ yield* validate(plan);
451
+ const selected = [...new Set(operationIds ?? plan.operations.map(({ id }) => id))].sort();
452
+ const unknown = selected.filter((id) => !plan.operations.some((operation) => operation.id === id));
453
+ if (unknown.length > 0) return yield* new Error$1({ message: `Unknown deletion operation IDs: ${unknown.join(", ")}` });
454
+ return {
455
+ operationIds: selected,
456
+ planDigest: plan.digest,
457
+ version: "domainkit.deletion-authorization.v1"
458
+ };
459
+ });
460
+ /** Re-read every approved record before the first delete, then delete by provider record ID. */
461
+ const apply = Effect.fn("Deletion.apply")(function* (input) {
462
+ const provider = yield* Service$2;
463
+ yield* validate(input.plan);
464
+ if (input.authorization.planDigest !== input.plan.digest) return yield* new Error$1({ message: "Deletion authorization belongs to a different plan" });
465
+ if (provider.id !== input.plan.providerId) return yield* new Error$1({ message: "Provider does not match the deletion plan" });
466
+ const now = yield* Clock.currentTimeMillis;
467
+ if (input.plan.expiresAt <= new Date(now)) return yield* new Error$1({ message: "Deletion plan has expired" });
468
+ const selected = input.plan.operations.filter(({ id }) => input.authorization.operationIds.includes(id));
469
+ if (selected.length !== input.authorization.operationIds.length) return yield* new Error$1({ message: "Deletion authorization contains unknown operations" });
470
+ const previouslyDeleted = yield* validatePriorReceipt(input.plan, input.priorReceipt);
471
+ const completedIds = new Set(previouslyDeleted.map(({ operationId }) => operationId));
472
+ if (previouslyDeleted.some(({ operationId }) => !input.authorization.operationIds.includes(operationId))) return yield* new Error$1({ message: "Prior receipt contains an unauthorized operation" });
473
+ const remaining = selected.filter(({ id }) => !completedIds.has(id));
474
+ yield* Effect.forEach(remaining, (operation) => assertExact(provider, input.plan.zone, operation.providerRecordId, operation.record, operation.id));
475
+ const deleted = [...previouslyDeleted];
476
+ for (const operation of remaining) {
477
+ const failure = yield* Effect.gen(function* () {
478
+ yield* assertExact(provider, input.plan.zone, operation.providerRecordId, operation.record, operation.id);
479
+ yield* provider.deleteRecord(input.plan.zone, operation.providerRecordId);
480
+ deleted.push({
481
+ operationId: operation.id,
482
+ providerRecordId: operation.providerRecordId
483
+ });
484
+ }).pipe(Effect.match({
485
+ onFailure: (cause) => cause,
486
+ onSuccess: () => null
487
+ }));
488
+ if (failure !== null) {
489
+ if (deleted.length === 0) return yield* failure;
490
+ return yield* new PartialError({
491
+ failedOperationId: operation.id,
492
+ message: failure.message,
493
+ receipt: yield* makeReceipt(input.plan, deleted, "partial")
494
+ });
495
+ }
496
+ }
497
+ return yield* makeReceipt(input.plan, deleted, "complete");
498
+ });
499
+ function validate(plan) {
500
+ const unsigned = {
501
+ createdAt: plan.createdAt,
502
+ expiresAt: plan.expiresAt,
503
+ operations: plan.operations,
504
+ providerId: plan.providerId,
505
+ sourcePlanDigest: plan.sourcePlanDigest,
506
+ version: plan.version,
507
+ zone: plan.zone
508
+ };
509
+ return sha256Encoded(UnsignedPlan, unsigned).pipe(Effect.flatMap((digest) => digest === plan.digest ? Effect.void : Effect.fail(new Error$1({ message: "Deletion plan digest does not match its contents" }))));
510
+ }
511
+ function validatePriorReceipt(plan, receipt) {
512
+ if (receipt === void 0) return Effect.succeed([]);
513
+ if (receipt.planDigest !== plan.digest || receipt.providerId !== plan.providerId || receipt.zone !== plan.zone || receipt.status !== "partial") return Effect.fail(new Error$1({ message: "Prior receipt does not belong to this deletion plan" }));
514
+ for (const completed of receipt.operations) {
515
+ const operation = plan.operations.find(({ id }) => id === completed.operationId);
516
+ if (operation === void 0 || operation.providerRecordId !== completed.providerRecordId) return Effect.fail(new Error$1({ message: "Prior receipt operation does not match the deletion plan" }));
517
+ }
518
+ return Effect.succeed([...receipt.operations]);
519
+ }
520
+ function assertExact(provider, zone, providerRecordId, expected, operationId) {
521
+ return provider.getRecord(zone, providerRecordId).pipe(Effect.flatMap((observed) => observed !== null && observed._tag !== "Opaque" && equals(observed, expected) ? Effect.void : Effect.fail(new Error$1({
522
+ message: observed === null ? "Created record no longer exists" : "Created record content no longer matches its receipt",
523
+ operationId
524
+ }))));
525
+ }
526
+ function makeReceipt(plan, operations, status) {
527
+ return Clock.currentTimeMillis.pipe(Effect.map((now) => ({
528
+ deletedAt: new Date(now),
529
+ operations,
530
+ planDigest: plan.digest,
531
+ providerId: plan.providerId,
532
+ status,
533
+ version: "domainkit.deletion-receipt.v1",
534
+ zone: plan.zone
535
+ })));
536
+ }
537
+ //#endregion
538
+ //#region src/verification/resolver.ts
539
+ var resolver_exports = /* @__PURE__ */ __exportAll({
540
+ Answer: () => Answer,
541
+ Error: () => Error,
542
+ Resolution: () => Resolution,
543
+ Service: () => Service,
544
+ layerFromAsync: () => layerFromAsync,
545
+ toAsync: () => toAsync
546
+ });
547
+ const Answer = Schema.Struct({
548
+ data: Schema.String,
549
+ name: Schema$2,
550
+ ttl: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
551
+ type: Type
552
+ });
553
+ const Resolution = Schema.TaggedUnion({
554
+ answer: { answers: Schema.Array(Answer) },
555
+ nodata: {}
556
+ });
557
+ var Error = class extends Schema.TaggedError()("ResolverError", {
558
+ cause: Schema.optionalKey(Schema.Unknown),
559
+ message: Schema.String,
560
+ reason: Schema.Literals([
561
+ "response",
562
+ "timeout",
563
+ "transport"
564
+ ])
565
+ }) {};
566
+ var Service = class extends Context.Service()("@domainkit/DnsResolver") {};
567
+ const layerFromAsync = (resolver) => Layer.succeed(Service, { resolve: Effect.fn("DnsResolver.resolve")((query) => Effect.tryPromise({
568
+ try: () => resolver.resolve(query),
569
+ catch: (cause) => new Error({
570
+ cause,
571
+ message: cause instanceof globalThis.Error ? cause.message : String(cause),
572
+ reason: "transport"
573
+ })
574
+ }).pipe(Effect.flatMap((resolution) => {
575
+ switch (resolution._tag) {
576
+ case "answer":
577
+ case "nodata": return Effect.succeed(resolution);
578
+ case "timeout": return Effect.fail(new Error({
579
+ message: "DNS query timed out",
580
+ reason: "timeout"
581
+ }));
582
+ case "failure": return Effect.fail(new Error({
583
+ message: resolution.message,
584
+ reason: "transport"
585
+ }));
586
+ }
587
+ }))) });
588
+ const toAsync = (resolver) => ({ resolve: (query) => Effect.runPromise(resolver.resolve(query).pipe(Effect.match({
589
+ onFailure: (failure) => failure.reason === "timeout" ? { _tag: "timeout" } : {
590
+ _tag: "failure",
591
+ message: failure.message
592
+ },
593
+ onSuccess: (resolution) => resolution
594
+ }))) });
595
+ //#endregion
596
+ export { decodeReceipt as A, plan_exports as C, Receipt$1 as D, Operation$1 as E, Service$1 as F, make as I, zone_discovery_exports as L, encodeReceipt as M, types_exports as N, Schema$1 as O, Candidate as P, create$1 as S, Authorization$1 as T, PartialApplyError as _, Authorization as a, apply$1 as b, PartialError as c, apply as d, authorize as f, CreateResult as g, ConflictError as h, toAsync as i, encode as j, decode as k, Plan as l, deletion_exports as m, Service as n, Error$1 as o, create as p, resolver_exports as r, Operation as s, Error as t, Receipt as u, StaleError as v, renderManualInstructions as w, authorize$1 as x, Target as y };
597
+
598
+ //# sourceMappingURL=resolver-CxE-VDw4.mjs.map