domainkit 0.15.0 → 0.16.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 CHANGED
@@ -65,6 +65,13 @@ Plans are additive and fail closed: exact records are `Noop`, missing records ar
65
65
  incompatible state is `Conflict`. DomainKit never updates or deletes a record it did not create,
66
66
  and cleanup is its own plan, approval, and receipt built from the apply receipt.
67
67
 
68
+ Several domains at once are one batch: `Provision.batch.create` plans them with
69
+ `Policy.batchConcurrency` in flight, `approve` binds one digest over every plan, and `apply` walks
70
+ the approved domains under their own attempt leases, so a second apply skips what the first holds
71
+ and a domain that fails does not stop the ones beside it. A batch is resumable at every step —
72
+ `resumePlanning` re-plans what failed, `apply` re-claims what failed — and
73
+ `Provision.batch.list({ unfinished: true })` is the index behind a "you still owe this" banner.
74
+
68
75
  Every step is a stored attempt, so a host can render the plan in one request, collect consent in
69
76
  another, and apply in a third; retrying any step replays its result. A customer who declines calls
70
77
  `Provision.reject`, which closes the attempt for good and leaves the domain free for a new plan. Every failure is one
@@ -153,9 +160,9 @@ export const ApiLive = HttpApiBuilder.layer(Api).pipe(
153
160
  );
154
161
  ```
155
162
 
156
- `Server.group` is one `HttpApiGroup` with fifteen typed endpoints covering the whole lifecycle:
163
+ `Server.group` is one `HttpApiGroup` with twenty-five typed endpoints covering the whole lifecycle:
157
164
  inspect, discover, connect, callback, attach, detach, disconnect, plan, approve, reject, apply, read
158
- a plan or a receipt, observe, and build a cleanup plan. `Identity` is the only service you write, and
165
+ a plan or a receipt, observe, build a cleanup plan, and the seven batch routes under `/batches`. `Identity` is the only service you write, and
159
166
  every handler derives the `Principal` for the request it is serving. `Server.group.prefix("/internal/dns")` moves
160
167
  every route, and the OAuth callback URL follows the mount. `OpenApi.fromApi(Server.api)` documents
161
168
  the group.
@@ -1,9 +1,12 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
2
2
  import { decode } from "./DomainName.mjs";
3
- import { NotFound } from "./Reason.mjs";
3
+ import { Digest } from "./Plan.mjs";
4
+ import { BatchStale, NotFound } from "./Reason.mjs";
4
5
  import { fail } from "./internal/error.mjs";
5
- import { Service as Service$1 } from "./Storage.mjs";
6
- import { Service as Service$2 } from "./Connect.mjs";
6
+ import { Service as Service$1 } from "./Principal.mjs";
7
+ import { Service as Service$2 } from "./Storage.mjs";
8
+ import { Service as Service$3 } from "./Connect.mjs";
9
+ import { sha256Hex } from "./internal/digest.mjs";
7
10
  import { reconcile } from "./internal/planner.mjs";
8
11
  import { assertWithin, make as make$1 } from "./internal/attempts.mjs";
9
12
  import { Context, Effect, Layer, Option } from "effect";
@@ -18,6 +21,7 @@ var Provision_exports = /* @__PURE__ */ __exportAll({
18
21
  Service: () => Service,
19
22
  apply: () => apply,
20
23
  approve: () => approve,
24
+ batch: () => batch,
21
25
  defaults: () => defaults,
22
26
  get: () => get,
23
27
  latest: () => latest,
@@ -29,12 +33,13 @@ var Provision_exports = /* @__PURE__ */ __exportAll({
29
33
  var Service = class extends Context.Service()("@domainkit/Provision") {};
30
34
  const defaults = {
31
35
  planTtlMs: 36e5,
32
- applyLeaseMs: 12e4
36
+ applyLeaseMs: 12e4,
37
+ batchConcurrency: 4
33
38
  };
34
39
  var Policy = class extends Context.Reference("@domainkit/Provision/Policy", { defaultValue: () => defaults }) {};
35
40
  const make = Effect.gen(function* () {
36
- const storage = yield* Service$1;
37
- const connect = yield* Service$2;
41
+ const storage = yield* Service$2;
42
+ const connect = yield* Service$3;
38
43
  const attempts = make$1(storage, connect, "provisioning");
39
44
  const attachmentFor = (input) => Effect.gen(function* () {
40
45
  const domain = yield* decode(input);
@@ -52,21 +57,113 @@ const make = Effect.gen(function* () {
52
57
  receipt: attempt.receipt,
53
58
  rejection: attempt.rejection
54
59
  });
60
+ /** Read the attached zone and record one `planned` attempt for it. */
61
+ const planFor = (attachment, requirements, ttlMs) => Effect.gen(function* () {
62
+ yield* assertWithin(attachment, requirements);
63
+ const { session, target } = yield* connect.session(attachment.id);
64
+ const observed = yield* session.dns(target).list(target.zone);
65
+ const operations = yield* reconcile(requirements, observed.map(({ record }) => record));
66
+ return yield* attempts.record({
67
+ attachment,
68
+ target,
69
+ operations,
70
+ ttlMs,
71
+ sourceReceiptId: null
72
+ });
73
+ });
74
+ const batchStale = (batch, digest) => fail(new BatchStale({
75
+ batchId: batch.id,
76
+ status: batch.status,
77
+ digest
78
+ }));
79
+ /** Provision reads provisioning batches; a cleanup batch is not its aggregate. */
80
+ const provisioningBatch = (id) => Effect.flatMap(storage.batches.get(id), (aggregate) => aggregate.batch.kind === "provisioning" ? Effect.succeed(aggregate) : fail(new NotFound({
81
+ entity: "batch",
82
+ id
83
+ })));
84
+ /**
85
+ * The digest the principal reviews: SHA-256 over the items' sorted `attachmentId:planDigest`
86
+ * pairs, and null while any item is still unplanned.
87
+ */
88
+ const digestOf = (items) => items.some((item) => item.plan === null) ? Effect.succeed(null) : Effect.map(sha256Hex(items.map((item) => `${item.attachmentId}:${item.plan?.digest ?? ""}`).sort().join("\n")), Digest.make);
89
+ const batchView = (aggregate) => Effect.gen(function* () {
90
+ const policy = yield* Policy;
91
+ const items = yield* Effect.forEach(aggregate.items, (item) => item.attemptId === null ? Effect.succeed({
92
+ attachmentId: item.attachmentId,
93
+ position: item.position,
94
+ plan: null,
95
+ status: null,
96
+ approval: null,
97
+ receipt: null,
98
+ rejection: null,
99
+ failure: null,
100
+ planFailure: item.planFailure
101
+ }) : Effect.map(storage.attempts.get(item.attemptId), (attempt) => ({
102
+ attachmentId: item.attachmentId,
103
+ position: item.position,
104
+ plan: attempt.plan,
105
+ status: attempt.status,
106
+ approval: attempt.approval,
107
+ receipt: attempt.receipt,
108
+ rejection: attempt.rejection,
109
+ failure: attempt.failure,
110
+ planFailure: item.planFailure
111
+ })), { concurrency: policy.batchConcurrency });
112
+ return {
113
+ id: aggregate.batch.id,
114
+ status: aggregate.batch.status,
115
+ digest: aggregate.batch.digest ?? (yield* digestOf(items)),
116
+ approval: aggregate.batch.approval,
117
+ rejection: aggregate.batch.rejection,
118
+ createdAt: aggregate.batch.createdAt,
119
+ updatedAt: aggregate.batch.updatedAt,
120
+ completedAt: aggregate.batch.completedAt,
121
+ items
122
+ };
123
+ });
124
+ /** The batch was declined under a planning pass still in flight; the fence did its work. */
125
+ const ignoreFence = (effect) => effect.pipe(Effect.asVoid, Effect.catch((error) => error.reason._tag === "BatchStale" ? Effect.void : Effect.fail(error)));
126
+ /**
127
+ * Plan every item that has no plan yet and for which this call supplied requirements. An item
128
+ * the caller did not name keeps whatever it holds, so a replayed create never re-plans.
129
+ */
130
+ const planMissing = (aggregate, supplied) => Effect.gen(function* () {
131
+ const policy = yield* Policy;
132
+ const id = aggregate.batch.id;
133
+ const pending = aggregate.items.flatMap((item) => {
134
+ if (item.attemptId !== null) return [];
135
+ const match = supplied.find(({ attachment }) => attachment.id === item.attachmentId);
136
+ return match === void 0 ? [] : [match];
137
+ });
138
+ yield* Effect.forEach(pending, ({ attachment, requirements }) => planFor(attachment, requirements, policy.planTtlMs).pipe(Effect.matchEffect({
139
+ onFailure: (error) => ignoreFence(storage.batches.recordItemPlanFailure(id, attachment.id, error.message)),
140
+ onSuccess: (plan) => storage.batches.recordItemPlan(id, attachment.id, plan.id).pipe(Effect.asVoid, Effect.catch((error) => error.reason._tag === "BatchStale" ? Effect.ignore(attempts.reject(plan.id, { reason: "the batch was declined" })) : Effect.fail(error)))
141
+ })), {
142
+ concurrency: policy.batchConcurrency,
143
+ discard: true
144
+ });
145
+ return yield* batchView(yield* storage.batches.get(id));
146
+ });
147
+ const attachmentsFor = (items) => Effect.forEach(items, (item) => Effect.map(attachmentFor(item.domain), (attachment) => ({
148
+ attachment,
149
+ requirements: item.requirements
150
+ })));
151
+ /**
152
+ * The same resolution, tolerating a domain this owner cannot reach.
153
+ *
154
+ * What a replay uses: the batch it answers with is already stored, so a payload naming a
155
+ * domain that was never attached, or one detached since, plans nothing rather than failing a
156
+ * request the first call already succeeded at.
157
+ */
158
+ const resolvableFor = (items) => Effect.map(Effect.forEach(items, (item) => attachmentFor(item.domain).pipe(Effect.map((attachment) => [{
159
+ attachment,
160
+ requirements: item.requirements
161
+ }]), Effect.catch((error) => error.reason._tag === "NotFound" || error.reason._tag === "InvalidInput" ? Effect.succeed([]) : Effect.fail(error)))), (groups) => groups.flat());
55
162
  return {
56
163
  plan: (input) => Effect.gen(function* () {
57
164
  const policy = yield* Policy;
58
165
  const attachment = yield* attachmentFor(input.domain);
59
- yield* assertWithin(attachment, input.requirements);
60
- const { session, target } = yield* connect.session(attachment.id);
61
- const observed = yield* session.dns(target).list(target.zone);
62
- const operations = yield* reconcile(input.requirements, observed.map(({ record }) => record));
63
- return yield* attempts.record({
64
- attachment,
65
- target,
66
- operations,
67
- ttlMs: policy.planTtlMs,
68
- sourceReceiptId: null
69
- });
166
+ return yield* planFor(attachment, input.requirements, policy.planTtlMs);
70
167
  }),
71
168
  approve: (plan, options) => attempts.approve(plan, options),
72
169
  reject: (plan, options) => Effect.map(attempts.reject(plan, options), view),
@@ -76,7 +173,87 @@ const make = Effect.gen(function* () {
76
173
  const attachment = yield* attachmentFor(domain);
77
174
  const latest = yield* storage.attempts.latest(attachment.id, "provisioning");
78
175
  return Option.isSome(latest) ? view(latest.value) : null;
79
- })
176
+ }),
177
+ batch: {
178
+ create: (input) => Effect.gen(function* () {
179
+ const replay = yield* storage.batches.byIdempotencyKey(input.idempotencyKey);
180
+ if (Option.isSome(replay)) {
181
+ const aggregate = replay.value;
182
+ if (aggregate.batch.kind !== "provisioning") return yield* fail(new NotFound({
183
+ entity: "batch",
184
+ id: aggregate.batch.id
185
+ }));
186
+ return yield* planMissing(aggregate, yield* resolvableFor(input.items));
187
+ }
188
+ const supplied = yield* attachmentsFor(input.items);
189
+ const aggregate = yield* storage.batches.create({
190
+ kind: "provisioning",
191
+ idempotencyKey: input.idempotencyKey,
192
+ attachmentIds: supplied.map(({ attachment }) => attachment.id)
193
+ });
194
+ return yield* planMissing(aggregate, supplied);
195
+ }),
196
+ resumePlanning: (id, input) => Effect.gen(function* () {
197
+ const aggregate = yield* provisioningBatch(id);
198
+ const supplied = yield* attachmentsFor(input.items);
199
+ return yield* planMissing(aggregate, supplied);
200
+ }),
201
+ approve: (id, options) => Effect.gen(function* () {
202
+ const principal = yield* Service$1;
203
+ const aggregate = yield* provisioningBatch(id);
204
+ const current = yield* batchView(aggregate);
205
+ if (current.digest === null || current.digest !== options.digest) return yield* batchStale(aggregate.batch, current.digest);
206
+ if (aggregate.batch.approval !== null) return current;
207
+ const approvals = yield* Effect.forEach(aggregate.items, (item) => Effect.gen(function* () {
208
+ if (item.attemptId === null) return yield* batchStale(aggregate.batch, null);
209
+ const attempt = yield* storage.attempts.get(item.attemptId);
210
+ return {
211
+ attachmentId: item.attachmentId,
212
+ approval: attempt.approval ?? (yield* attempts.draftApproval(attempt))
213
+ };
214
+ }));
215
+ const approved = yield* storage.batches.approve(id, {
216
+ digest: current.digest,
217
+ actorId: principal.actorId,
218
+ approvals
219
+ });
220
+ return yield* batchView(approved);
221
+ }),
222
+ apply: (id) => Effect.gen(function* () {
223
+ const policy = yield* Policy;
224
+ const aggregate = yield* provisioningBatch(id);
225
+ if (aggregate.batch.status === "complete") return yield* batchView(aggregate);
226
+ if (aggregate.batch.approval === null) return yield* batchStale(aggregate.batch, aggregate.batch.digest);
227
+ yield* Effect.forEach(aggregate.items, (item) => Effect.gen(function* () {
228
+ if (item.attemptId === null) return;
229
+ const attempt = yield* storage.attempts.get(item.attemptId);
230
+ if (attempt.receipt !== null || attempt.approval === null) return;
231
+ yield* attempts.apply(attempt.approval.id, policy).pipe(Effect.asVoid, Effect.catch(() => Effect.void));
232
+ }), {
233
+ concurrency: policy.batchConcurrency,
234
+ discard: true
235
+ });
236
+ return yield* batchView(yield* storage.batches.refresh(id));
237
+ }),
238
+ reject: (id, options) => Effect.gen(function* () {
239
+ const principal = yield* Service$1;
240
+ yield* provisioningBatch(id);
241
+ const rejected = yield* storage.batches.reject(id, {
242
+ actorId: principal.actorId,
243
+ reason: options?.reason ?? null
244
+ });
245
+ return yield* batchView(rejected);
246
+ }),
247
+ get: (id) => Effect.flatMap(provisioningBatch(id), batchView),
248
+ list: () => Effect.map(storage.batches.listUnfinished(), (aggregates) => aggregates.filter(({ batch: row }) => row.kind === "provisioning").map(({ batch: row, items }) => ({
249
+ id: row.id,
250
+ status: row.status,
251
+ itemCount: items.length,
252
+ createdAt: row.createdAt,
253
+ updatedAt: row.updatedAt,
254
+ completedAt: row.completedAt
255
+ })))
256
+ }
80
257
  };
81
258
  });
82
259
  const layer = Layer.effect(Service)(make);
@@ -87,7 +264,20 @@ const reject = accessor((service) => service.reject);
87
264
  const apply = accessor((service) => service.apply);
88
265
  const get = accessor((service) => service.get);
89
266
  const latest = accessor((service) => service.latest);
267
+ /**
268
+ * Many domains as one reviewable unit. Every operation needs `Provision.Service` and `Principal`,
269
+ * exactly like the single-domain accessors above.
270
+ */
271
+ const batch = {
272
+ create: accessor((service) => service.batch.create),
273
+ resumePlanning: accessor((service) => service.batch.resumePlanning),
274
+ approve: accessor((service) => service.batch.approve),
275
+ apply: accessor((service) => service.batch.apply),
276
+ reject: accessor((service) => service.batch.reject),
277
+ get: accessor((service) => service.batch.get),
278
+ list: accessor((service) => service.batch.list)
279
+ };
90
280
  //#endregion
91
- export { Policy, Provision_exports, Service, apply, approve, defaults, get, latest, layer, make, plan, reject };
281
+ export { Policy, Provision_exports, Service, apply, approve, batch, defaults, get, latest, layer, make, plan, reject };
92
282
 
93
283
  //# sourceMappingURL=Provision.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"Provision.mjs","names":["Storage.Service","Connect.Service","Attempts.make","DomainName.decode","Errors.fail","Reason.NotFound","Attempts.assertWithin","Planner.reconcile"],"sources":["../src/Provision.ts"],"sourcesContent":["/**\n * plan -> approve -> apply, durable. Every step is a stored attempt, so a host can render the\n * plan in one request, collect consent in another, and apply in a third, and a retry of any step\n * is idempotent.\n */\nimport { Context, Effect, Layer, Option } from \"effect\";\n\nimport type * as Approval from \"./Approval.ts\";\nimport * as Connect from \"./Connect.ts\";\nimport type * 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 * as Attempts from \"./internal/attempts.ts\";\nimport * as Planner from \"./internal/planner.ts\";\nimport type * as Plan from \"./Plan.ts\";\nimport type * as Principal from \"./Principal.ts\";\nimport type * as Receipt from \"./Receipt.ts\";\nimport * as Storage from \"./Storage.ts\";\n\ntype Fx<A> = Effect.Effect<A, Errors.DomainKitError, Principal.Service>;\n\nexport interface Attempt {\n readonly plan: Plan.Model;\n readonly status: Storage.AttemptStatus;\n readonly approval: Approval.Model | null;\n readonly receipt: Receipt.Model | null;\n readonly rejection: Storage.Rejection | null;\n}\n\nexport interface RejectOptions {\n readonly reason?: string;\n}\n\nexport interface Interface {\n /** Read provider state and build an additive plan. Fails `NotFound` when the domain is not attached. */\n readonly plan: (input: {\n readonly domain: string;\n readonly requirements: ReadonlyArray<DnsRecord.Model>;\n }) => Fx<Plan.Model>;\n /**\n * Record the principal's consent. Fails `Conflict` unless `allowPartial` and the selected\n * operations are conflict-free. Approving an already-approved plan returns the same approval.\n */\n readonly approve: (\n plan: Plan.Model | Plan.PlanId,\n options?: Attempts.ApproveOptions,\n ) => Fx<Approval.Model>;\n /**\n * Decline the plan for the acting principal; terminal. Rejecting again returns the same\n * attempt; a plan that was approved or applied fails `Stale`.\n */\n readonly reject: (plan: Plan.Model | Plan.PlanId, options?: RejectOptions) => Fx<Attempt>;\n /**\n * Re-plan the zone, fail `Stale` if the digest moved, then create records. Partial success is a\n * `partial` receipt. Applying an attempt that already completed returns its receipt.\n */\n readonly apply: (approval: Approval.Model | Approval.ApprovalId) => Fx<Receipt.Model>;\n readonly get: (planId: Plan.PlanId) => Fx<Attempt>;\n readonly latest: (domain: string) => Fx<Attempt | null>;\n}\n\nexport class Service extends Context.Service<Service, Interface>()(\"@domainkit/Provision\") {}\n\nexport interface PolicyShape {\n /** Plan lifetime before it must be rebuilt. Default 1 hour. */\n readonly planTtlMs: number;\n /** Apply lease; a crashed apply can be retried after this. Default 2 minutes. */\n readonly applyLeaseMs: number;\n}\nexport const defaults: PolicyShape = { planTtlMs: 60 * 60_000, applyLeaseMs: 2 * 60_000 };\nexport class Policy extends Context.Reference<PolicyShape>(\"@domainkit/Provision/Policy\", {\n defaultValue: () => defaults,\n}) {}\n\nexport const make: Effect.Effect<Interface, never, Storage.Service | Connect.Service> = Effect.gen(\n function* () {\n const storage = yield* Storage.Service;\n const connect = yield* Connect.Service;\n const attempts = Attempts.make(storage, connect, \"provisioning\");\n\n const attachmentFor = (input: string): Fx<Storage.Attachment> =>\n Effect.gen(function* () {\n const domain = yield* DomainName.decode(input);\n const attachment = yield* storage.attachments.byDomain(domain);\n if (Option.isNone(attachment)) {\n return yield* Errors.fail(new Reason.NotFound({ entity: \"attachment\", id: domain }));\n }\n return attachment.value;\n });\n\n const view = (attempt: Storage.Attempt): Attempt => ({\n plan: attempt.plan,\n status: attempt.status,\n approval: attempt.approval,\n receipt: attempt.receipt,\n rejection: attempt.rejection,\n });\n\n return {\n plan: (input) =>\n Effect.gen(function* () {\n const policy = yield* Policy;\n const attachment = yield* attachmentFor(input.domain);\n yield* Attempts.assertWithin(attachment, input.requirements);\n const { session, target } = yield* connect.session(attachment.id);\n const observed = yield* session.dns(target).list(target.zone);\n const operations = yield* Planner.reconcile(\n input.requirements,\n observed.map(({ record }) => record),\n );\n return yield* attempts.record({\n attachment,\n target,\n operations,\n ttlMs: policy.planTtlMs,\n sourceReceiptId: null,\n });\n }),\n approve: (plan, options) => attempts.approve(plan, options),\n reject: (plan, options) => Effect.map(attempts.reject(plan, options), view),\n apply: (approval) => Effect.flatMap(Policy, (policy) => attempts.apply(approval, policy)),\n get: (planId) => Effect.map(attempts.attemptOf(planId), view),\n latest: (domain) =>\n Effect.gen(function* () {\n const attachment = yield* attachmentFor(domain);\n const latest = yield* storage.attempts.latest(attachment.id, \"provisioning\");\n return Option.isSome(latest) ? view(latest.value) : null;\n }),\n };\n },\n);\n\nexport const layer: Layer.Layer<Service, never, Storage.Service | Connect.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 plan = accessor((service) => service.plan);\nexport const approve = accessor((service) => service.approve);\nexport const reject = accessor((service) => service.reject);\nexport const apply = accessor((service) => service.apply);\nexport const get = accessor((service) => service.get);\nexport const latest = accessor((service) => service.latest);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8DA,IAAa,UAAb,cAA6B,QAAQ,QAA4B,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC;AAQ5F,MAAa,WAAwB;CAAE,WAAW;CAAa,cAAc;AAAW;AACxF,IAAa,SAAb,cAA4B,QAAQ,UAAuB,+BAA+B,EACxF,oBAAoB,SACtB,CAAC,CAAC,CAAC,CAAC;AAEJ,MAAa,OAA2E,OAAO,IAC7F,aAAa;CACX,MAAM,UAAU,OAAOA;CACvB,MAAM,UAAU,OAAOC;CACvB,MAAM,WAAWC,OAAc,SAAS,SAAS,cAAc;CAE/D,MAAM,iBAAiB,UACrB,OAAO,IAAI,aAAa;EACtB,MAAM,SAAS,OAAOC,OAAkB,KAAK;EAC7C,MAAM,aAAa,OAAO,QAAQ,YAAY,SAAS,MAAM;EAC7D,IAAI,OAAO,OAAO,UAAU,GAC1B,OAAO,OAAOC,KAAY,IAAIC,SAAgB;GAAE,QAAQ;GAAc,IAAI;EAAO,CAAC,CAAC;EAErF,OAAO,WAAW;CACpB,CAAC;CAEH,MAAM,QAAQ,aAAuC;EACnD,MAAM,QAAQ;EACd,QAAQ,QAAQ;EAChB,UAAU,QAAQ;EAClB,SAAS,QAAQ;EACjB,WAAW,QAAQ;CACrB;CAEA,OAAO;EACL,OAAO,UACL,OAAO,IAAI,aAAa;GACtB,MAAM,SAAS,OAAO;GACtB,MAAM,aAAa,OAAO,cAAc,MAAM,MAAM;GACpD,OAAOC,aAAsB,YAAY,MAAM,YAAY;GAC3D,MAAM,EAAE,SAAS,WAAW,OAAO,QAAQ,QAAQ,WAAW,EAAE;GAChE,MAAM,WAAW,OAAO,QAAQ,IAAI,MAAM,CAAC,CAAC,KAAK,OAAO,IAAI;GAC5D,MAAM,aAAa,OAAOC,UACxB,MAAM,cACN,SAAS,KAAK,EAAE,aAAa,MAAM,CACrC;GACA,OAAO,OAAO,SAAS,OAAO;IAC5B;IACA;IACA;IACA,OAAO,OAAO;IACd,iBAAiB;GACnB,CAAC;EACH,CAAC;EACH,UAAU,MAAM,YAAY,SAAS,QAAQ,MAAM,OAAO;EAC1D,SAAS,MAAM,YAAY,OAAO,IAAI,SAAS,OAAO,MAAM,OAAO,GAAG,IAAI;EAC1E,QAAQ,aAAa,OAAO,QAAQ,SAAS,WAAW,SAAS,MAAM,UAAU,MAAM,CAAC;EACxF,MAAM,WAAW,OAAO,IAAI,SAAS,UAAU,MAAM,GAAG,IAAI;EAC5D,SAAS,WACP,OAAO,IAAI,aAAa;GACtB,MAAM,aAAa,OAAO,cAAc,MAAM;GAC9C,MAAM,SAAS,OAAO,QAAQ,SAAS,OAAO,WAAW,IAAI,cAAc;GAC3E,OAAO,OAAO,OAAO,MAAM,IAAI,KAAK,OAAO,KAAK,IAAI;EACtD,CAAC;CACL;AACF,CACF;AAEA,MAAa,QACX,MAAM,OAAO,OAAO,CAAC,CAAC,IAAI;AAE5B,MAAM,YAEF,UAED,GAAG,SACF,OAAO,QAAQ,UAAU,YAAY,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;AAE/D,MAAa,OAAO,UAAU,YAAY,QAAQ,IAAI;AACtD,MAAa,UAAU,UAAU,YAAY,QAAQ,OAAO;AAC5D,MAAa,SAAS,UAAU,YAAY,QAAQ,MAAM;AAC1D,MAAa,QAAQ,UAAU,YAAY,QAAQ,KAAK;AACxD,MAAa,MAAM,UAAU,YAAY,QAAQ,GAAG;AACpD,MAAa,SAAS,UAAU,YAAY,QAAQ,MAAM"}
1
+ {"version":3,"file":"Provision.mjs","names":["Storage.Service","Connect.Service","Attempts.make","DomainName.decode","Errors.fail","Reason.NotFound","Attempts.assertWithin","Planner.reconcile","Reason.BatchStale","Principal.Service"],"sources":["../src/Provision.ts"],"sourcesContent":["/**\n * plan -> approve -> apply, durable. Every step is a stored attempt, so a host can render the\n * plan in one request, collect consent in another, and apply in a third, and a retry of any step\n * is idempotent.\n */\nimport { Context, type DateTime, Effect, Layer, Option } from \"effect\";\n\nimport type * as Approval from \"./Approval.ts\";\nimport * as Connect from \"./Connect.ts\";\nimport type * 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 * as Attempts from \"./internal/attempts.ts\";\nimport { sha256Hex } from \"./internal/digest.ts\";\nimport * as Planner from \"./internal/planner.ts\";\nimport * as Plan from \"./Plan.ts\";\nimport * as Principal from \"./Principal.ts\";\nimport type * as Receipt from \"./Receipt.ts\";\nimport * as Storage from \"./Storage.ts\";\n\ntype Fx<A> = Effect.Effect<A, Errors.DomainKitError, Principal.Service>;\n\nexport interface Attempt {\n readonly plan: Plan.Model;\n readonly status: Storage.AttemptStatus;\n readonly approval: Approval.Model | null;\n readonly receipt: Receipt.Model | null;\n readonly rejection: Storage.Rejection | null;\n}\n\nexport interface RejectOptions {\n readonly reason?: string;\n}\n\n/** One domain to plan inside a batch. The domain must already be attached. */\nexport interface BatchInput {\n readonly domain: string;\n readonly requirements: ReadonlyArray<DnsRecord.Model>;\n}\n\n/**\n * One domain's place in a batch, with whatever its attempt holds.\n *\n * Everything but `planFailure` is read from the attempt the item points at, so a batch never\n * shows a customer a plan, receipt, or failure that disagrees with the attempt it came from.\n */\nexport interface BatchItem {\n readonly attachmentId: string;\n readonly position: number;\n /** Null while the item has no plan; `planFailure` then says why. */\n readonly plan: Plan.Model | null;\n readonly status: Storage.AttemptStatus | null;\n readonly approval: Approval.Model | null;\n readonly receipt: Receipt.Model | null;\n readonly rejection: Storage.Rejection | null;\n /** Why the last apply stopped before any write. */\n readonly failure: string | null;\n /** Why the last planning pass could not build this item's plan. */\n readonly planFailure: string | null;\n}\n\n/** Many domains planned together, approved once, and applied with bounded concurrency. */\nexport interface Batch {\n readonly id: Storage.BatchId;\n readonly status: Storage.BatchStatus;\n /**\n * What `approve` binds: SHA-256 over the items' sorted `attachmentId:planDigest` pairs. The\n * digest the batch was approved at once it is approved, the digest its current plans produce\n * before that, and null while any item is still unplanned.\n */\n readonly digest: Plan.Digest | null;\n readonly approval: Storage.BatchApproval | null;\n readonly rejection: Storage.Rejection | null;\n readonly createdAt: DateTime.Utc;\n readonly updatedAt: DateTime.Utc;\n readonly completedAt: DateTime.Utc | null;\n readonly items: ReadonlyArray<BatchItem>;\n}\n\n/** A batch without its plans, for the index a host renders as a banner. */\nexport interface BatchSummary {\n readonly id: Storage.BatchId;\n readonly status: Storage.BatchStatus;\n readonly itemCount: number;\n readonly createdAt: DateTime.Utc;\n readonly updatedAt: DateTime.Utc;\n readonly completedAt: DateTime.Utc | null;\n}\n\nexport interface BatchInterface {\n /**\n * Plan every domain in `items` as one batch, with `Policy.batchConcurrency` in flight.\n *\n * `idempotencyKey` is unique per owner: a retried create returns the batch the first call made,\n * plans whatever is still unplanned, and ignores items that batch does not hold. An item whose\n * plan fails records the reason and leaves the batch `planning` for `resumePlanning`.\n */\n readonly create: (input: {\n readonly items: ReadonlyArray<BatchInput>;\n readonly idempotencyKey: string;\n }) => Fx<Batch>;\n /**\n * Plan the items that still have none, with the requirements supplied again. A batch stores\n * pointers, not requirements, so the host owns them across the retry.\n */\n readonly resumePlanning: (\n id: Storage.BatchId,\n input: { readonly items: ReadonlyArray<BatchInput> },\n ) => Fx<Batch>;\n /**\n * Record the principal's consent to the whole batch. Fails `BatchStale` unless `digest` is the\n * one the batch's current plans produce. Approving an approved batch at its digest returns it.\n */\n readonly approve: (id: Storage.BatchId, options: { readonly digest: Plan.Digest }) => Fx<Batch>;\n /**\n * Apply every approved item that has no receipt yet, with `Policy.batchConcurrency` in flight\n * and each item under its own attempt lease. An item another apply holds is skipped this round,\n * and an item that fails records its failure on its attempt rather than stopping the others, so\n * calling `apply` again resumes the batch. Applying a complete batch returns it unchanged.\n */\n readonly apply: (id: Storage.BatchId) => Fx<Batch>;\n /**\n * Decline the batch and every plan under it; terminal. Rejecting again returns the same batch;\n * a batch that was approved fails `BatchStale`.\n */\n readonly reject: (id: Storage.BatchId, options?: RejectOptions) => Fx<Batch>;\n readonly get: (id: Storage.BatchId) => Fx<Batch>;\n /** Every batch this owner still owes a move on, most recently touched first. */\n readonly list: (filter: { readonly unfinished: true }) => Fx<ReadonlyArray<BatchSummary>>;\n}\n\nexport interface Interface {\n /** Read provider state and build an additive plan. Fails `NotFound` when the domain is not attached. */\n readonly plan: (input: {\n readonly domain: string;\n readonly requirements: ReadonlyArray<DnsRecord.Model>;\n }) => Fx<Plan.Model>;\n /**\n * Record the principal's consent. Fails `Conflict` unless `allowPartial` and the selected\n * operations are conflict-free. Approving an already-approved plan returns the same approval.\n */\n readonly approve: (\n plan: Plan.Model | Plan.PlanId,\n options?: Attempts.ApproveOptions,\n ) => Fx<Approval.Model>;\n /**\n * Decline the plan for the acting principal; terminal. Rejecting again returns the same\n * attempt; a plan that was approved or applied fails `Stale`.\n */\n readonly reject: (plan: Plan.Model | Plan.PlanId, options?: RejectOptions) => Fx<Attempt>;\n /**\n * Re-plan the zone, fail `Stale` if the digest moved, then create records. Partial success is a\n * `partial` receipt. Applying an attempt that already completed returns its receipt.\n */\n readonly apply: (approval: Approval.Model | Approval.ApprovalId) => Fx<Receipt.Model>;\n readonly get: (planId: Plan.PlanId) => Fx<Attempt>;\n readonly latest: (domain: string) => Fx<Attempt | null>;\n /** Many domains as one reviewable unit: plan together, approve once, apply bounded. */\n readonly batch: BatchInterface;\n}\n\nexport class Service extends Context.Service<Service, Interface>()(\"@domainkit/Provision\") {}\n\nexport interface PolicyShape {\n /** Plan lifetime before it must be rebuilt. Default 1 hour. */\n readonly planTtlMs: number;\n /** Apply lease; a crashed apply can be retried after this. Default 2 minutes. */\n readonly applyLeaseMs: number;\n /** How many of a batch's domains are planned or applied at once. Default 4. */\n readonly batchConcurrency: number;\n}\nexport const defaults: PolicyShape = {\n planTtlMs: 60 * 60_000,\n applyLeaseMs: 2 * 60_000,\n batchConcurrency: 4,\n};\nexport class Policy extends Context.Reference<PolicyShape>(\"@domainkit/Provision/Policy\", {\n defaultValue: () => defaults,\n}) {}\n\nexport const make: Effect.Effect<Interface, never, Storage.Service | Connect.Service> = Effect.gen(\n function* () {\n const storage = yield* Storage.Service;\n const connect = yield* Connect.Service;\n const attempts = Attempts.make(storage, connect, \"provisioning\");\n\n const attachmentFor = (input: string): Fx<Storage.Attachment> =>\n Effect.gen(function* () {\n const domain = yield* DomainName.decode(input);\n const attachment = yield* storage.attachments.byDomain(domain);\n if (Option.isNone(attachment)) {\n return yield* Errors.fail(new Reason.NotFound({ entity: \"attachment\", id: domain }));\n }\n return attachment.value;\n });\n\n const view = (attempt: Storage.Attempt): Attempt => ({\n plan: attempt.plan,\n status: attempt.status,\n approval: attempt.approval,\n receipt: attempt.receipt,\n rejection: attempt.rejection,\n });\n\n /** Read the attached zone and record one `planned` attempt for it. */\n const planFor = (\n attachment: Storage.Attachment,\n requirements: ReadonlyArray<DnsRecord.Model>,\n ttlMs: number,\n ): Fx<Plan.Model> =>\n Effect.gen(function* () {\n yield* Attempts.assertWithin(attachment, requirements);\n const { session, target } = yield* connect.session(attachment.id);\n const observed = yield* session.dns(target).list(target.zone);\n const operations = yield* Planner.reconcile(\n requirements,\n observed.map(({ record }) => record),\n );\n return yield* attempts.record({\n attachment,\n target,\n operations,\n ttlMs,\n sourceReceiptId: null,\n });\n });\n\n const batchStale = (batch: Storage.Batch, digest: Plan.Digest | null) =>\n Errors.fail(new Reason.BatchStale({ batchId: batch.id, status: batch.status, digest }));\n\n /** Provision reads provisioning batches; a cleanup batch is not its aggregate. */\n const provisioningBatch = (id: Storage.BatchId): Fx<Storage.BatchAggregate> =>\n Effect.flatMap(storage.batches.get(id), (aggregate) =>\n aggregate.batch.kind === \"provisioning\"\n ? Effect.succeed(aggregate)\n : Errors.fail(new Reason.NotFound({ entity: \"batch\", id })),\n );\n\n /**\n * The digest the principal reviews: SHA-256 over the items' sorted `attachmentId:planDigest`\n * pairs, and null while any item is still unplanned.\n */\n const digestOf = (items: ReadonlyArray<BatchItem>): Fx<Plan.Digest | null> =>\n items.some((item) => item.plan === null)\n ? Effect.succeed(null)\n : Effect.map(\n sha256Hex(\n items\n .map((item) => `${item.attachmentId}:${item.plan?.digest ?? \"\"}`)\n .sort()\n .join(\"\\n\"),\n ),\n Plan.Digest.make,\n );\n\n const batchView = (aggregate: Storage.BatchAggregate): Fx<Batch> =>\n Effect.gen(function* () {\n const policy = yield* Policy;\n const items = yield* Effect.forEach(\n aggregate.items,\n (item): Fx<BatchItem> =>\n item.attemptId === null\n ? Effect.succeed({\n attachmentId: item.attachmentId,\n position: item.position,\n plan: null,\n status: null,\n approval: null,\n receipt: null,\n rejection: null,\n failure: null,\n planFailure: item.planFailure,\n })\n : Effect.map(storage.attempts.get(item.attemptId), (attempt) => ({\n attachmentId: item.attachmentId,\n position: item.position,\n plan: attempt.plan,\n status: attempt.status,\n approval: attempt.approval,\n receipt: attempt.receipt,\n rejection: attempt.rejection,\n failure: attempt.failure,\n planFailure: item.planFailure,\n })),\n { concurrency: policy.batchConcurrency },\n );\n return {\n id: aggregate.batch.id,\n status: aggregate.batch.status,\n digest: aggregate.batch.digest ?? (yield* digestOf(items)),\n approval: aggregate.batch.approval,\n rejection: aggregate.batch.rejection,\n createdAt: aggregate.batch.createdAt,\n updatedAt: aggregate.batch.updatedAt,\n completedAt: aggregate.batch.completedAt,\n items,\n };\n });\n\n /** The batch was declined under a planning pass still in flight; the fence did its work. */\n const ignoreFence = <A>(\n effect: Fx<A>,\n ): Effect.Effect<void, Errors.DomainKitError, Principal.Service> =>\n effect.pipe(\n Effect.asVoid,\n Effect.catch((error) =>\n error.reason._tag === \"BatchStale\" ? Effect.void : Effect.fail(error),\n ),\n );\n\n /**\n * Plan every item that has no plan yet and for which this call supplied requirements. An item\n * the caller did not name keeps whatever it holds, so a replayed create never re-plans.\n */\n const planMissing = (\n aggregate: Storage.BatchAggregate,\n supplied: ReadonlyArray<{\n readonly attachment: Storage.Attachment;\n readonly requirements: ReadonlyArray<DnsRecord.Model>;\n }>,\n ): Fx<Batch> =>\n Effect.gen(function* () {\n const policy = yield* Policy;\n const id = aggregate.batch.id;\n const pending = aggregate.items.flatMap((item) => {\n if (item.attemptId !== null) return [];\n const match = supplied.find(({ attachment }) => attachment.id === item.attachmentId);\n return match === undefined ? [] : [match];\n });\n yield* Effect.forEach(\n pending,\n ({ attachment, requirements }) =>\n planFor(attachment, requirements, policy.planTtlMs).pipe(\n Effect.matchEffect({\n // A domain that cannot be planned is the batch's news to carry, not a reason to\n // abandon the domains beside it.\n onFailure: (error) =>\n ignoreFence(\n storage.batches.recordItemPlanFailure(id, attachment.id, error.message),\n ),\n onSuccess: (plan) =>\n storage.batches.recordItemPlan(id, attachment.id, plan.id).pipe(\n Effect.asVoid,\n Effect.catch((error) =>\n error.reason._tag === \"BatchStale\"\n ? // The batch was declined while this plan was being built. The fence kept\n // the plan off the aggregate; closing the attempt keeps it out of the\n // single-domain API too, which would otherwise approve and apply it.\n Effect.ignore(\n attempts.reject(plan.id, { reason: \"the batch was declined\" }),\n )\n : Effect.fail(error),\n ),\n ),\n }),\n ),\n { concurrency: policy.batchConcurrency, discard: true },\n );\n return yield* batchView(yield* storage.batches.get(id));\n });\n\n const attachmentsFor = (items: ReadonlyArray<BatchInput>) =>\n Effect.forEach(items, (item) =>\n Effect.map(attachmentFor(item.domain), (attachment) => ({\n attachment,\n requirements: item.requirements,\n })),\n );\n\n /**\n * The same resolution, tolerating a domain this owner cannot reach.\n *\n * What a replay uses: the batch it answers with is already stored, so a payload naming a\n * domain that was never attached, or one detached since, plans nothing rather than failing a\n * request the first call already succeeded at.\n */\n const resolvableFor = (items: ReadonlyArray<BatchInput>) =>\n Effect.map(\n Effect.forEach(items, (item) =>\n attachmentFor(item.domain).pipe(\n Effect.map((attachment) => [{ attachment, requirements: item.requirements }]),\n Effect.catch((error) =>\n error.reason._tag === \"NotFound\" || error.reason._tag === \"InvalidInput\"\n ? Effect.succeed(\n [] as ReadonlyArray<{\n readonly attachment: Storage.Attachment;\n readonly requirements: ReadonlyArray<DnsRecord.Model>;\n }>,\n )\n : Effect.fail(error),\n ),\n ),\n ),\n (groups) => groups.flat(),\n );\n\n const batch: BatchInterface = {\n create: (input) =>\n Effect.gen(function* () {\n // The key decides first: a retried request answers with the batch the first one made,\n // whatever this payload now says, so a domain detached in between cannot fail a create\n // that already succeeded.\n const replay = yield* storage.batches.byIdempotencyKey(input.idempotencyKey);\n if (Option.isSome(replay)) {\n const aggregate = replay.value;\n if (aggregate.batch.kind !== \"provisioning\") {\n return yield* Errors.fail(\n new Reason.NotFound({ entity: \"batch\", id: aggregate.batch.id }),\n );\n }\n return yield* planMissing(aggregate, yield* resolvableFor(input.items));\n }\n const supplied = yield* attachmentsFor(input.items);\n const aggregate = yield* storage.batches.create({\n kind: \"provisioning\",\n idempotencyKey: input.idempotencyKey,\n attachmentIds: supplied.map(({ attachment }) => attachment.id),\n });\n return yield* planMissing(aggregate, supplied);\n }),\n resumePlanning: (id, input) =>\n Effect.gen(function* () {\n const aggregate = yield* provisioningBatch(id);\n const supplied = yield* attachmentsFor(input.items);\n return yield* planMissing(aggregate, supplied);\n }),\n approve: (id, options) =>\n Effect.gen(function* () {\n const principal = yield* Principal.Service;\n const aggregate = yield* provisioningBatch(id);\n const current = yield* batchView(aggregate);\n // Consent is bound to the digest the principal read, so nothing is written until the\n // batch's current plans still produce it.\n if (current.digest === null || current.digest !== options.digest) {\n return yield* batchStale(aggregate.batch, current.digest);\n }\n if (aggregate.batch.approval !== null) return current;\n const approvals = yield* Effect.forEach(aggregate.items, (item) =>\n Effect.gen(function* () {\n if (item.attemptId === null) return yield* batchStale(aggregate.batch, null);\n const attempt = yield* storage.attempts.get(item.attemptId);\n return {\n attachmentId: item.attachmentId,\n approval: attempt.approval ?? (yield* attempts.draftApproval(attempt)),\n };\n }),\n );\n // One transaction: the batch is never approved without the per-attempt approvals\n // `apply` takes.\n const approved = yield* storage.batches.approve(id, {\n digest: current.digest,\n actorId: principal.actorId,\n approvals,\n });\n return yield* batchView(approved);\n }),\n apply: (id) =>\n Effect.gen(function* () {\n const policy = yield* Policy;\n const aggregate = yield* provisioningBatch(id);\n if (aggregate.batch.status === \"complete\") return yield* batchView(aggregate);\n if (aggregate.batch.approval === null) {\n return yield* batchStale(aggregate.batch, aggregate.batch.digest);\n }\n yield* Effect.forEach(\n aggregate.items,\n (item) =>\n Effect.gen(function* () {\n if (item.attemptId === null) return;\n const attempt = yield* storage.attempts.get(item.attemptId);\n if (attempt.receipt !== null || attempt.approval === null) return;\n // The attempt lease is the item lock: an item another apply holds is `Busy` and\n // waits for the next round, and one that fails records that on its own attempt.\n yield* attempts.apply(attempt.approval.id, policy).pipe(\n Effect.asVoid,\n Effect.catch(() => Effect.void),\n );\n }),\n { concurrency: policy.batchConcurrency, discard: true },\n );\n return yield* batchView(yield* storage.batches.refresh(id));\n }),\n reject: (id, options) =>\n Effect.gen(function* () {\n const principal = yield* Principal.Service;\n yield* provisioningBatch(id);\n const rejected = yield* storage.batches.reject(id, {\n actorId: principal.actorId,\n reason: options?.reason ?? null,\n });\n return yield* batchView(rejected);\n }),\n get: (id) => Effect.flatMap(provisioningBatch(id), batchView),\n list: () =>\n Effect.map(storage.batches.listUnfinished(), (aggregates) =>\n aggregates\n .filter(({ batch: row }) => row.kind === \"provisioning\")\n .map(({ batch: row, items }) => ({\n id: row.id,\n status: row.status,\n itemCount: items.length,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n completedAt: row.completedAt,\n })),\n ),\n };\n\n return {\n plan: (input) =>\n Effect.gen(function* () {\n const policy = yield* Policy;\n const attachment = yield* attachmentFor(input.domain);\n return yield* planFor(attachment, input.requirements, policy.planTtlMs);\n }),\n approve: (plan, options) => attempts.approve(plan, options),\n reject: (plan, options) => Effect.map(attempts.reject(plan, options), view),\n apply: (approval) => Effect.flatMap(Policy, (policy) => attempts.apply(approval, policy)),\n get: (planId) => Effect.map(attempts.attemptOf(planId), view),\n latest: (domain) =>\n Effect.gen(function* () {\n const attachment = yield* attachmentFor(domain);\n const latest = yield* storage.attempts.latest(attachment.id, \"provisioning\");\n return Option.isSome(latest) ? view(latest.value) : null;\n }),\n batch,\n };\n },\n);\n\nexport const layer: Layer.Layer<Service, never, Storage.Service | Connect.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 plan = accessor((service) => service.plan);\nexport const approve = accessor((service) => service.approve);\nexport const reject = accessor((service) => service.reject);\nexport const apply = accessor((service) => service.apply);\nexport const get = accessor((service) => service.get);\nexport const latest = accessor((service) => service.latest);\n\n/**\n * Many domains as one reviewable unit. Every operation needs `Provision.Service` and `Principal`,\n * exactly like the single-domain accessors above.\n */\nexport const batch = {\n create: accessor((service) => service.batch.create),\n resumePlanning: accessor((service) => service.batch.resumePlanning),\n approve: accessor((service) => service.batch.approve),\n apply: accessor((service) => service.batch.apply),\n reject: accessor((service) => service.batch.reject),\n get: accessor((service) => service.batch.get),\n list: accessor((service) => service.batch.list),\n};\n"],"mappings":"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"}
package/dist/Reason.mjs CHANGED
@@ -7,6 +7,7 @@ import { Schema } from "effect";
7
7
  * match on `reason._tag` and add a reason here rather than a new error class.
8
8
  */
9
9
  var Reason_exports = /* @__PURE__ */ __exportAll({
10
+ BatchStale: () => BatchStale,
10
11
  Busy: () => Busy,
11
12
  Conflict: () => Conflict,
12
13
  CryptoFailed: () => CryptoFailed,
@@ -45,7 +46,8 @@ var NotFound = class extends Schema.TaggedError("@domainkit/Reason/NotFound")("N
45
46
  "provider",
46
47
  "zone",
47
48
  "authorization",
48
- "record"
49
+ "record",
50
+ "batch"
49
51
  ]),
50
52
  id: Schema.String
51
53
  }) {
@@ -71,6 +73,23 @@ var Stale = class extends Schema.TaggedError("@domainkit/Reason/Stale")("Stale",
71
73
  return `plan ${this.planId} is stale; provider state no longer matches digest ${this.digest}`;
72
74
  }
73
75
  };
76
+ /**
77
+ * The batch moved since it was read: it was declined, it is already approved, or the digest the
78
+ * principal reviewed is no longer the one its plans produce. Read the batch again.
79
+ *
80
+ * The aggregate's own refusal, beside `Stale`: a batch has no plan and no plan digest of its own,
81
+ * so nothing here can name one. `digest` is the batch's current digest, null while its items are
82
+ * still being planned.
83
+ */
84
+ var BatchStale = class extends Schema.TaggedError("@domainkit/Reason/BatchStale")("BatchStale", {
85
+ batchId: Schema.String,
86
+ status: Schema.String,
87
+ digest: Schema.NullOr(Schema.String)
88
+ }) {
89
+ get message() {
90
+ return `batch ${this.batchId} is ${this.status}; its current digest is ${this.digest ?? "not yet known"}`;
91
+ }
92
+ };
74
93
  var Expired = class extends Schema.TaggedError("@domainkit/Reason/Expired")("Expired", {
75
94
  entity: Schema.Literals([
76
95
  "plan",
@@ -145,6 +164,7 @@ const Model = Schema.Union([
145
164
  NotFound,
146
165
  Conflict,
147
166
  Stale,
167
+ BatchStale,
148
168
  Expired,
149
169
  Busy,
150
170
  ProviderRejected,
@@ -157,6 +177,6 @@ const Model = Schema.Union([
157
177
  ResolverFailed
158
178
  ]);
159
179
  //#endregion
160
- export { Busy, Conflict, CryptoFailed, Expired, Forbidden, InvalidInput, Model, NotFound, ProviderConflict, ProviderRejected, ProviderUnavailable, Reason_exports, Reconnect, ResolverFailed, Stale, StorageFailed, Unauthenticated, Unsupported };
180
+ export { BatchStale, Busy, Conflict, CryptoFailed, Expired, Forbidden, InvalidInput, Model, NotFound, ProviderConflict, ProviderRejected, ProviderUnavailable, Reason_exports, Reconnect, ResolverFailed, Stale, StorageFailed, Unauthenticated, Unsupported };
161
181
 
162
182
  //# sourceMappingURL=Reason.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"Reason.mjs","names":["Plan.PlanId","Plan.Digest","Plan.Conflict"],"sources":["../src/Reason.ts"],"sourcesContent":["/**\n * Why an operation failed. Every `DomainKit.Error` carries exactly one of these as `reason`; hosts\n * match on `reason._tag` and add a reason here rather than a new error class.\n */\nimport { Schema } from \"effect\";\n\nimport * as Plan from \"./Plan.ts\";\n\n// Plan imports `internal/error.ts`, which imports this module; read Plan lazily so evaluation\n// order inside the cycle does not matter.\nconst PlanId = Schema.suspend(() => Plan.PlanId);\nconst Digest = Schema.suspend(() => Plan.Digest);\nconst Conflicts = Schema.suspend(() => Schema.Array(Plan.Conflict));\n\nexport class InvalidInput extends Schema.TaggedError<InvalidInput>(\n \"@domainkit/Reason/InvalidInput\",\n)(\"InvalidInput\", { message: Schema.String, field: Schema.optionalKey(Schema.String) }) {}\nexport class Unauthenticated extends Schema.TaggedError<Unauthenticated>(\n \"@domainkit/Reason/Unauthenticated\",\n)(\"Unauthenticated\", { message: Schema.String }) {}\nexport class Forbidden extends Schema.TaggedError<Forbidden>(\"@domainkit/Reason/Forbidden\")(\n \"Forbidden\",\n { message: Schema.String },\n) {}\nexport class NotFound extends Schema.TaggedError<NotFound>(\"@domainkit/Reason/NotFound\")(\n \"NotFound\",\n {\n entity: Schema.Literals([\n \"connection\",\n \"attachment\",\n \"plan\",\n \"approval\",\n \"receipt\",\n \"continuation\",\n \"provider\",\n \"zone\",\n \"authorization\",\n \"record\",\n ]),\n id: Schema.String,\n },\n) {\n override get message(): string {\n return `${this.entity} ${this.id} was not found`;\n }\n}\n/** The plan contains conflicts and cannot be applied. */\nexport class Conflict extends Schema.TaggedError<Conflict>(\"@domainkit/Reason/Conflict\")(\n \"Conflict\",\n {\n planId: PlanId,\n operations: Conflicts,\n },\n) {\n override get message(): string {\n return `plan ${this.planId} has ${this.operations.length} conflicting operation(s)`;\n }\n}\n/** Provider state moved since the plan was built; build a new plan. */\nexport class Stale extends Schema.TaggedError<Stale>(\"@domainkit/Reason/Stale\")(\"Stale\", {\n planId: PlanId,\n digest: Digest,\n}) {\n override get message(): string {\n return `plan ${this.planId} is stale; provider state no longer matches digest ${this.digest}`;\n }\n}\nexport class Expired extends Schema.TaggedError<Expired>(\"@domainkit/Reason/Expired\")(\"Expired\", {\n entity: Schema.Literals([\"plan\", \"approval\", \"continuation\", \"credential\"]),\n id: Schema.String,\n}) {\n override get message(): string {\n return `${this.entity} ${this.id} has expired`;\n }\n}\n/** Another operation holds the lock (e.g. concurrent apply or refresh). Retrying is safe. */\nexport class Busy extends Schema.TaggedError<Busy>(\"@domainkit/Reason/Busy\")(\"Busy\", {\n key: Schema.String,\n}) {\n override get message(): string {\n return `${this.key} is busy`;\n }\n}\nexport class ProviderRejected extends Schema.TaggedError<ProviderRejected>(\n \"@domainkit/Reason/ProviderRejected\",\n)(\"ProviderRejected\", {\n provider: Schema.String,\n code: Schema.optionalKey(Schema.String),\n message: Schema.String,\n}) {}\nexport class ProviderUnavailable extends Schema.TaggedError<ProviderUnavailable>(\n \"@domainkit/Reason/ProviderUnavailable\",\n)(\"ProviderUnavailable\", {\n provider: Schema.String,\n retryAfterMs: Schema.optionalKey(Schema.Number),\n message: Schema.String,\n}) {}\n/** The provider credential no longer works; the customer must reconnect. */\nexport class Reconnect extends Schema.TaggedError<Reconnect>(\"@domainkit/Reason/Reconnect\")(\n \"Reconnect\",\n { provider: Schema.String, connectionId: Schema.String },\n) {\n override get message(): string {\n return `${this.provider} connection ${this.connectionId} must be reconnected`;\n }\n}\nexport class StorageFailed extends Schema.TaggedError<StorageFailed>(\n \"@domainkit/Reason/StorageFailed\",\n)(\"StorageFailed\", { operation: Schema.String, message: Schema.String }) {}\nexport class CryptoFailed extends Schema.TaggedError<CryptoFailed>(\n \"@domainkit/Reason/CryptoFailed\",\n)(\"CryptoFailed\", { operation: Schema.Literals([\"digest\", \"seal\", \"open\"]) }) {\n override get message(): string {\n return `crypto ${this.operation} failed`;\n }\n}\nexport class ResolverFailed extends Schema.TaggedError<ResolverFailed>(\n \"@domainkit/Reason/ResolverFailed\",\n)(\"ResolverFailed\", { resolver: Schema.String, message: Schema.String }) {}\n\n/** The provider refused the write because a conflicting record already exists there. */\nexport class ProviderConflict extends Schema.TaggedError<ProviderConflict>(\n \"@domainkit/Reason/ProviderConflict\",\n)(\"ProviderConflict\", {\n provider: Schema.String,\n code: Schema.optionalKey(Schema.String),\n message: Schema.String,\n}) {}\n/** The provider, or this target within it, cannot do the requested operation. */\nexport class Unsupported extends Schema.TaggedError<Unsupported>(\"@domainkit/Reason/Unsupported\")(\n \"Unsupported\",\n { provider: Schema.String, operation: Schema.String, message: Schema.String },\n) {}\n\nexport const Model = Schema.Union([\n InvalidInput,\n Unauthenticated,\n Forbidden,\n NotFound,\n Conflict,\n Stale,\n Expired,\n Busy,\n ProviderRejected,\n ProviderUnavailable,\n ProviderConflict,\n Unsupported,\n Reconnect,\n StorageFailed,\n CryptoFailed,\n ResolverFailed,\n]);\nexport type Model = typeof Model.Type;\n"],"mappings":"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"}
1
+ {"version":3,"file":"Reason.mjs","names":["Plan.PlanId","Plan.Digest","Plan.Conflict"],"sources":["../src/Reason.ts"],"sourcesContent":["/**\n * Why an operation failed. Every `DomainKit.Error` carries exactly one of these as `reason`; hosts\n * match on `reason._tag` and add a reason here rather than a new error class.\n */\nimport { Schema } from \"effect\";\n\nimport * as Plan from \"./Plan.ts\";\n\n// Plan imports `internal/error.ts`, which imports this module; read Plan lazily so evaluation\n// order inside the cycle does not matter.\nconst PlanId = Schema.suspend(() => Plan.PlanId);\nconst Digest = Schema.suspend(() => Plan.Digest);\nconst Conflicts = Schema.suspend(() => Schema.Array(Plan.Conflict));\n\nexport class InvalidInput extends Schema.TaggedError<InvalidInput>(\n \"@domainkit/Reason/InvalidInput\",\n)(\"InvalidInput\", { message: Schema.String, field: Schema.optionalKey(Schema.String) }) {}\nexport class Unauthenticated extends Schema.TaggedError<Unauthenticated>(\n \"@domainkit/Reason/Unauthenticated\",\n)(\"Unauthenticated\", { message: Schema.String }) {}\nexport class Forbidden extends Schema.TaggedError<Forbidden>(\"@domainkit/Reason/Forbidden\")(\n \"Forbidden\",\n { message: Schema.String },\n) {}\nexport class NotFound extends Schema.TaggedError<NotFound>(\"@domainkit/Reason/NotFound\")(\n \"NotFound\",\n {\n entity: Schema.Literals([\n \"connection\",\n \"attachment\",\n \"plan\",\n \"approval\",\n \"receipt\",\n \"continuation\",\n \"provider\",\n \"zone\",\n \"authorization\",\n \"record\",\n \"batch\",\n ]),\n id: Schema.String,\n },\n) {\n override get message(): string {\n return `${this.entity} ${this.id} was not found`;\n }\n}\n/** The plan contains conflicts and cannot be applied. */\nexport class Conflict extends Schema.TaggedError<Conflict>(\"@domainkit/Reason/Conflict\")(\n \"Conflict\",\n {\n planId: PlanId,\n operations: Conflicts,\n },\n) {\n override get message(): string {\n return `plan ${this.planId} has ${this.operations.length} conflicting operation(s)`;\n }\n}\n/** Provider state moved since the plan was built; build a new plan. */\nexport class Stale extends Schema.TaggedError<Stale>(\"@domainkit/Reason/Stale\")(\"Stale\", {\n planId: PlanId,\n digest: Digest,\n}) {\n override get message(): string {\n return `plan ${this.planId} is stale; provider state no longer matches digest ${this.digest}`;\n }\n}\n/**\n * The batch moved since it was read: it was declined, it is already approved, or the digest the\n * principal reviewed is no longer the one its plans produce. Read the batch again.\n *\n * The aggregate's own refusal, beside `Stale`: a batch has no plan and no plan digest of its own,\n * so nothing here can name one. `digest` is the batch's current digest, null while its items are\n * still being planned.\n */\nexport class BatchStale extends Schema.TaggedError<BatchStale>(\"@domainkit/Reason/BatchStale\")(\n \"BatchStale\",\n { batchId: Schema.String, status: Schema.String, digest: Schema.NullOr(Schema.String) },\n) {\n override get message(): string {\n return `batch ${this.batchId} is ${this.status}; its current digest is ${this.digest ?? \"not yet known\"}`;\n }\n}\nexport class Expired extends Schema.TaggedError<Expired>(\"@domainkit/Reason/Expired\")(\"Expired\", {\n entity: Schema.Literals([\"plan\", \"approval\", \"continuation\", \"credential\"]),\n id: Schema.String,\n}) {\n override get message(): string {\n return `${this.entity} ${this.id} has expired`;\n }\n}\n/** Another operation holds the lock (e.g. concurrent apply or refresh). Retrying is safe. */\nexport class Busy extends Schema.TaggedError<Busy>(\"@domainkit/Reason/Busy\")(\"Busy\", {\n key: Schema.String,\n}) {\n override get message(): string {\n return `${this.key} is busy`;\n }\n}\nexport class ProviderRejected extends Schema.TaggedError<ProviderRejected>(\n \"@domainkit/Reason/ProviderRejected\",\n)(\"ProviderRejected\", {\n provider: Schema.String,\n code: Schema.optionalKey(Schema.String),\n message: Schema.String,\n}) {}\nexport class ProviderUnavailable extends Schema.TaggedError<ProviderUnavailable>(\n \"@domainkit/Reason/ProviderUnavailable\",\n)(\"ProviderUnavailable\", {\n provider: Schema.String,\n retryAfterMs: Schema.optionalKey(Schema.Number),\n message: Schema.String,\n}) {}\n/** The provider credential no longer works; the customer must reconnect. */\nexport class Reconnect extends Schema.TaggedError<Reconnect>(\"@domainkit/Reason/Reconnect\")(\n \"Reconnect\",\n { provider: Schema.String, connectionId: Schema.String },\n) {\n override get message(): string {\n return `${this.provider} connection ${this.connectionId} must be reconnected`;\n }\n}\nexport class StorageFailed extends Schema.TaggedError<StorageFailed>(\n \"@domainkit/Reason/StorageFailed\",\n)(\"StorageFailed\", { operation: Schema.String, message: Schema.String }) {}\nexport class CryptoFailed extends Schema.TaggedError<CryptoFailed>(\n \"@domainkit/Reason/CryptoFailed\",\n)(\"CryptoFailed\", { operation: Schema.Literals([\"digest\", \"seal\", \"open\"]) }) {\n override get message(): string {\n return `crypto ${this.operation} failed`;\n }\n}\nexport class ResolverFailed extends Schema.TaggedError<ResolverFailed>(\n \"@domainkit/Reason/ResolverFailed\",\n)(\"ResolverFailed\", { resolver: Schema.String, message: Schema.String }) {}\n\n/** The provider refused the write because a conflicting record already exists there. */\nexport class ProviderConflict extends Schema.TaggedError<ProviderConflict>(\n \"@domainkit/Reason/ProviderConflict\",\n)(\"ProviderConflict\", {\n provider: Schema.String,\n code: Schema.optionalKey(Schema.String),\n message: Schema.String,\n}) {}\n/** The provider, or this target within it, cannot do the requested operation. */\nexport class Unsupported extends Schema.TaggedError<Unsupported>(\"@domainkit/Reason/Unsupported\")(\n \"Unsupported\",\n { provider: Schema.String, operation: Schema.String, message: Schema.String },\n) {}\n\nexport const Model = Schema.Union([\n InvalidInput,\n Unauthenticated,\n Forbidden,\n NotFound,\n Conflict,\n Stale,\n BatchStale,\n Expired,\n Busy,\n ProviderRejected,\n ProviderUnavailable,\n ProviderConflict,\n Unsupported,\n Reconnect,\n StorageFailed,\n CryptoFailed,\n ResolverFailed,\n]);\nexport type Model = typeof Model.Type;\n"],"mappings":"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"}
package/dist/Server.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
2
2
  import { Model } from "./DnsRecord.mjs";
3
- import { Model as Model$1, OperationId, PlanId } from "./Plan.mjs";
3
+ import { Digest, Model as Model$1, OperationId, PlanId } from "./Plan.mjs";
4
4
  import { InvalidInput, NotFound } from "./Reason.mjs";
5
5
  import { DomainKitError, fail } from "./internal/error.mjs";
6
6
  import { ApprovalId, Model as Model$2 } from "./Approval.mjs";
7
7
  import { Service } from "./Principal.mjs";
8
8
  import { Service as Service$1 } from "./Providers.mjs";
9
9
  import { Model as Model$3, ReceiptId } from "./Receipt.mjs";
10
- import { AttemptStatus, AuthMethod, Overall, Rejection, RequirementStatus, Service as Service$2 } from "./Storage.mjs";
10
+ import { AttemptStatus, AuthMethod, BatchApproval, BatchId, BatchStatus, Overall, Rejection, RequirementStatus, Service as Service$2 } from "./Storage.mjs";
11
11
  import { Method as Method$1, Service as Service$3 } from "./Connect.mjs";
12
12
  import { Service as Service$4 } from "./Provision.mjs";
13
13
  import { Service as Service$5 } from "./Cleanup.mjs";
@@ -40,6 +40,11 @@ var Server_exports = /* @__PURE__ */ __exportAll({
40
40
  AttachPayload: () => AttachPayload,
41
41
  Attachment: () => Attachment,
42
42
  Attempt: () => Attempt,
43
+ Batch: () => Batch,
44
+ BatchApprovePayload: () => BatchApprovePayload,
45
+ BatchItem: () => BatchItem,
46
+ BatchPayload: () => BatchPayload,
47
+ BatchSummary: () => BatchSummary,
43
48
  Candidate: () => Candidate,
44
49
  Connected: () => Connected,
45
50
  ConnectionStatus: () => ConnectionStatus,
@@ -240,6 +245,48 @@ const Attempt = Schema.Struct({
240
245
  receipt: Schema.NullOr(Model$3),
241
246
  rejection: Schema.NullOr(Rejection)
242
247
  });
248
+ /** What a batch's create and resume routes take: one domain and its requirements per item. */
249
+ const BatchPayload = Schema.Struct({ items: Schema.Array(Schema.Struct({
250
+ domain: Schema.String,
251
+ requirements: Schema.Array(Model)
252
+ })) });
253
+ /** The digest the principal read, which the batch's current plans must still produce. */
254
+ const BatchApprovePayload = Schema.Struct({ digest: Digest });
255
+ /** One domain's place in a batch, with whatever its attempt holds. */
256
+ const BatchItem = Schema.Struct({
257
+ attachmentId: Schema.String,
258
+ position: Schema.Number,
259
+ /** Null while the item has no plan; `planFailure` then says why. */
260
+ plan: Schema.NullOr(Model$1),
261
+ status: Schema.NullOr(AttemptStatus),
262
+ approval: Schema.NullOr(Model$2),
263
+ receipt: Schema.NullOr(Model$3),
264
+ rejection: Schema.NullOr(Rejection),
265
+ failure: Schema.NullOr(Schema.String),
266
+ planFailure: Schema.NullOr(Schema.String)
267
+ });
268
+ /** Many domains planned together, as every batch route returns them. */
269
+ const Batch = Schema.Struct({
270
+ id: BatchId,
271
+ status: BatchStatus,
272
+ /** What `POST /batches/:batchId/approvals` takes; null while any item is unplanned. */
273
+ digest: Schema.NullOr(Digest),
274
+ approval: Schema.NullOr(BatchApproval),
275
+ rejection: Schema.NullOr(Rejection),
276
+ createdAt: Schema.DateTimeUtcFromString,
277
+ updatedAt: Schema.DateTimeUtcFromString,
278
+ completedAt: Schema.NullOr(Schema.DateTimeUtcFromString),
279
+ items: Schema.Array(BatchItem)
280
+ });
281
+ /** A batch without its plans, as the unfinished index returns them. */
282
+ const BatchSummary = Schema.Struct({
283
+ id: BatchId,
284
+ status: BatchStatus,
285
+ itemCount: Schema.Number,
286
+ createdAt: Schema.DateTimeUtcFromString,
287
+ updatedAt: Schema.DateTimeUtcFromString,
288
+ completedAt: Schema.NullOr(Schema.DateTimeUtcFromString)
289
+ });
243
290
  /** `Verify.Readiness` on the wire; timestamps encode as ISO strings. */
244
291
  const Readiness = Schema.Struct({
245
292
  domain: Schema.String,
@@ -353,6 +400,38 @@ const group = HttpApiGroup.make("domainkit").add(HttpApiEndpoint.get("inspect",
353
400
  params: { receiptId: ReceiptId },
354
401
  success: Model$1,
355
402
  error: errors
403
+ })).add(HttpApiEndpoint.post("createBatch", "/batches", {
404
+ headers: { "idempotency-key": Schema.String },
405
+ payload: BatchPayload,
406
+ success: Batch,
407
+ error: errors
408
+ })).add(HttpApiEndpoint.get("batches", "/batches", {
409
+ query: { unfinished: Schema.Literal("true") },
410
+ success: Schema.Array(BatchSummary),
411
+ error: errors
412
+ })).add(HttpApiEndpoint.get("batch", "/batches/:batchId", {
413
+ params: { batchId: BatchId },
414
+ success: Batch,
415
+ error: errors
416
+ })).add(HttpApiEndpoint.post("planBatch", "/batches/:batchId/plans", {
417
+ params: { batchId: BatchId },
418
+ payload: BatchPayload,
419
+ success: Batch,
420
+ error: errors
421
+ })).add(HttpApiEndpoint.post("approveBatch", "/batches/:batchId/approvals", {
422
+ params: { batchId: BatchId },
423
+ payload: BatchApprovePayload,
424
+ success: Batch,
425
+ error: errors
426
+ })).add(HttpApiEndpoint.post("rejectBatch", "/batches/:batchId/rejections", {
427
+ params: { batchId: BatchId },
428
+ payload: RejectPayload,
429
+ success: Batch,
430
+ error: errors
431
+ })).add(HttpApiEndpoint.post("applyBatch", "/batches/:batchId/apply", {
432
+ params: { batchId: BatchId },
433
+ success: Batch,
434
+ error: errors
356
435
  }));
357
436
  const START_ROUTE = "/connections";
358
437
  const reconnectRoute = (connectionId) => `/connections/${encodeURIComponent(connectionId)}/reconnections`;
@@ -651,7 +730,10 @@ const layer = (api, options = {}) => HttpApiBuilder.group(api, "domainkit", (han
651
730
  })) : Effect.succeed(attempt.receipt)))).handle("observe", ({ params, payload, request }) => as("observe", request, verify.observe({
652
731
  domain: params.domain,
653
732
  ...payload.requirements === void 0 ? {} : { requirements: payload.requirements }
654
- }))).handle("readiness", ({ params, request }) => as("readiness", request, verify.latest(params.domain))).handle("cleanupPlan", ({ params, request }) => as("cleanupPlan", request, cleanup.plan({ receiptId: params.receiptId })));
733
+ }))).handle("readiness", ({ params, request }) => as("readiness", request, verify.latest(params.domain))).handle("cleanupPlan", ({ params, request }) => as("cleanupPlan", request, cleanup.plan({ receiptId: params.receiptId }))).handle("createBatch", ({ headers, payload, request }) => as("createBatch", request, provision.batch.create({
734
+ items: payload.items,
735
+ idempotencyKey: headers["idempotency-key"]
736
+ }))).handle("batches", ({ request }) => as("batches", request, provision.batch.list({ unfinished: true }))).handle("batch", ({ params, request }) => as("batch", request, provision.batch.get(params.batchId))).handle("planBatch", ({ params, payload, request }) => as("planBatch", request, provision.batch.resumePlanning(params.batchId, { items: payload.items }))).handle("approveBatch", ({ params, payload, request }) => as("approveBatch", request, provision.batch.approve(params.batchId, { digest: payload.digest }))).handle("rejectBatch", ({ params, payload, request }) => as("rejectBatch", request, provision.batch.reject(params.batchId, { ...payload.reason === void 0 ? {} : { reason: payload.reason } }))).handle("applyBatch", ({ params, request }) => as("applyBatch", request, provision.batch.apply(params.batchId)));
655
737
  }));
656
738
  /** A standalone API for hosts not on HttpApi. Mount at any prefix. */
657
739
  const api = HttpApi.make("domainkit").add(group);
@@ -662,6 +744,6 @@ const toWebHandler = (services, options = {}) => {
662
744
  return HttpRouter.toWebHandler(app, { disableLogger: true });
663
745
  };
664
746
  //#endregion
665
- export { ApprovePayload, AttachPayload, Attachment, Attempt, Candidate, Connected, ConnectionStatus, Discovery, DiscoveryNotFound, DiscoveryResolved, DiscoverySelectionRequired, Field, Identity, Integration, Method, MethodDescriptor, OAuth, ObservePayload, PlanPayload, Readiness, ReconnectPayload, Redirect, RejectPayload, SelectionRequired, Server_exports, Snapshot, StartPayload, Started, Token, Zone, Zones, api, group, layer, toWebHandler };
747
+ export { ApprovePayload, AttachPayload, Attachment, Attempt, Batch, BatchApprovePayload, BatchItem, BatchPayload, BatchSummary, Candidate, Connected, ConnectionStatus, Discovery, DiscoveryNotFound, DiscoveryResolved, DiscoverySelectionRequired, Field, Identity, Integration, Method, MethodDescriptor, OAuth, ObservePayload, PlanPayload, Readiness, ReconnectPayload, Redirect, RejectPayload, SelectionRequired, Server_exports, Snapshot, StartPayload, Started, Token, Zone, Zones, api, group, layer, toWebHandler };
666
748
 
667
749
  //# sourceMappingURL=Server.mjs.map