domainkit 0.14.1 → 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.
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAUA,MAAM,SAAS,OAAO,cAAcA,QAAW;AAC/C,MAAM,SAAS,OAAO,cAAcC,QAAW;AAC/C,MAAM,YAAY,OAAO,cAAc,OAAO,MAAMC,UAAa,CAAC;AAElE,IAAa,eAAb,cAAkC,OAAO,YACvC,gCACF,CAAC,CAAC,gBAAgB;CAAE,SAAS,OAAO;CAAQ,OAAO,OAAO,YAAY,OAAO,MAAM;AAAE,CAAC,CAAC,CAAC,CAAC;AACzF,IAAa,kBAAb,cAAqC,OAAO,YAC1C,mCACF,CAAC,CAAC,mBAAmB,EAAE,SAAS,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC;AAClD,IAAa,YAAb,cAA+B,OAAO,YAAuB,6BAA6B,CAAC,CACzF,aACA,EAAE,SAAS,OAAO,OAAO,CAC3B,CAAC,CAAC,CAAC;AACH,IAAa,WAAb,cAA8B,OAAO,YAAsB,4BAA4B,CAAC,CACtF,YACA;CACE,QAAQ,OAAO,SAAS;EACtB;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CAAC;CACD,IAAI,OAAO;AACb,CACF,CAAC,CAAC;CACA,IAAa,UAAkB;EAC7B,OAAO,GAAG,KAAK,OAAO,GAAG,KAAK,GAAG;CACnC;AACF;;AAEA,IAAa,WAAb,cAA8B,OAAO,YAAsB,4BAA4B,CAAC,CACtF,YACA;CACE,QAAQ;CACR,YAAY;AACd,CACF,CAAC,CAAC;CACA,IAAa,UAAkB;EAC7B,OAAO,QAAQ,KAAK,OAAO,OAAO,KAAK,WAAW,OAAO;CAC3D;AACF;;AAEA,IAAa,QAAb,cAA2B,OAAO,YAAmB,yBAAyB,CAAC,CAAC,SAAS;CACvF,QAAQ;CACR,QAAQ;AACV,CAAC,CAAC,CAAC;CACD,IAAa,UAAkB;EAC7B,OAAO,QAAQ,KAAK,OAAO,qDAAqD,KAAK;CACvF;AACF;;;;;;;;;AASA,IAAa,aAAb,cAAgC,OAAO,YAAwB,8BAA8B,CAAC,CAC5F,cACA;CAAE,SAAS,OAAO;CAAQ,QAAQ,OAAO;CAAQ,QAAQ,OAAO,OAAO,OAAO,MAAM;AAAE,CACxF,CAAC,CAAC;CACA,IAAa,UAAkB;EAC7B,OAAO,SAAS,KAAK,QAAQ,MAAM,KAAK,OAAO,0BAA0B,KAAK,UAAU;CAC1F;AACF;AACA,IAAa,UAAb,cAA6B,OAAO,YAAqB,2BAA2B,CAAC,CAAC,WAAW;CAC/F,QAAQ,OAAO,SAAS;EAAC;EAAQ;EAAY;EAAgB;CAAY,CAAC;CAC1E,IAAI,OAAO;AACb,CAAC,CAAC,CAAC;CACD,IAAa,UAAkB;EAC7B,OAAO,GAAG,KAAK,OAAO,GAAG,KAAK,GAAG;CACnC;AACF;;AAEA,IAAa,OAAb,cAA0B,OAAO,YAAkB,wBAAwB,CAAC,CAAC,QAAQ,EACnF,KAAK,OAAO,OACd,CAAC,CAAC,CAAC;CACD,IAAa,UAAkB;EAC7B,OAAO,GAAG,KAAK,IAAI;CACrB;AACF;AACA,IAAa,mBAAb,cAAsC,OAAO,YAC3C,oCACF,CAAC,CAAC,oBAAoB;CACpB,UAAU,OAAO;CACjB,MAAM,OAAO,YAAY,OAAO,MAAM;CACtC,SAAS,OAAO;AAClB,CAAC,CAAC,CAAC,CAAC;AACJ,IAAa,sBAAb,cAAyC,OAAO,YAC9C,uCACF,CAAC,CAAC,uBAAuB;CACvB,UAAU,OAAO;CACjB,cAAc,OAAO,YAAY,OAAO,MAAM;CAC9C,SAAS,OAAO;AAClB,CAAC,CAAC,CAAC,CAAC;;AAEJ,IAAa,YAAb,cAA+B,OAAO,YAAuB,6BAA6B,CAAC,CACzF,aACA;CAAE,UAAU,OAAO;CAAQ,cAAc,OAAO;AAAO,CACzD,CAAC,CAAC;CACA,IAAa,UAAkB;EAC7B,OAAO,GAAG,KAAK,SAAS,cAAc,KAAK,aAAa;CAC1D;AACF;AACA,IAAa,gBAAb,cAAmC,OAAO,YACxC,iCACF,CAAC,CAAC,iBAAiB;CAAE,WAAW,OAAO;CAAQ,SAAS,OAAO;AAAO,CAAC,CAAC,CAAC,CAAC;AAC1E,IAAa,eAAb,cAAkC,OAAO,YACvC,gCACF,CAAC,CAAC,gBAAgB,EAAE,WAAW,OAAO,SAAS;CAAC;CAAU;CAAQ;AAAM,CAAC,EAAE,CAAC,CAAC,CAAC;CAC5E,IAAa,UAAkB;EAC7B,OAAO,UAAU,KAAK,UAAU;CAClC;AACF;AACA,IAAa,iBAAb,cAAoC,OAAO,YACzC,kCACF,CAAC,CAAC,kBAAkB;CAAE,UAAU,OAAO;CAAQ,SAAS,OAAO;AAAO,CAAC,CAAC,CAAC,CAAC;;AAG1E,IAAa,mBAAb,cAAsC,OAAO,YAC3C,oCACF,CAAC,CAAC,oBAAoB;CACpB,UAAU,OAAO;CACjB,MAAM,OAAO,YAAY,OAAO,MAAM;CACtC,SAAS,OAAO;AAClB,CAAC,CAAC,CAAC,CAAC;;AAEJ,IAAa,cAAb,cAAiC,OAAO,YAAyB,+BAA+B,CAAC,CAC/F,eACA;CAAE,UAAU,OAAO;CAAQ,WAAW,OAAO;CAAQ,SAAS,OAAO;AAAO,CAC9E,CAAC,CAAC,CAAC;AAEH,MAAa,QAAQ,OAAO,MAAM;CAChC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC"}
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,
@@ -206,6 +211,8 @@ const Zones = Schema.Struct({
206
211
  connections: Schema.Array(Schema.Struct({
207
212
  connectionId: Schema.String,
208
213
  provider: Schema.String,
214
+ /** What the provider called the account at connect time; `null` when it named none. */
215
+ label: Schema.NullOr(Schema.String),
209
216
  status: ConnectionStatus
210
217
  })),
211
218
  /** Every provider a customer could connect, so a picker offers one without a second call. */
@@ -238,6 +245,48 @@ const Attempt = Schema.Struct({
238
245
  receipt: Schema.NullOr(Model$3),
239
246
  rejection: Schema.NullOr(Rejection)
240
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
+ });
241
290
  /** `Verify.Readiness` on the wire; timestamps encode as ISO strings. */
242
291
  const Readiness = Schema.Struct({
243
292
  domain: Schema.String,
@@ -351,6 +400,38 @@ const group = HttpApiGroup.make("domainkit").add(HttpApiEndpoint.get("inspect",
351
400
  params: { receiptId: ReceiptId },
352
401
  success: Model$1,
353
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
354
435
  }));
355
436
  const START_ROUTE = "/connections";
356
437
  const reconnectRoute = (connectionId) => `/connections/${encodeURIComponent(connectionId)}/reconnections`;
@@ -495,7 +576,7 @@ const layer = (api, options = {}) => HttpApiBuilder.group(api, "domainkit", (han
495
576
  return new Connected({
496
577
  connectionId: input.connection.id,
497
578
  provider: authorization.provider,
498
- label: input.attachment?.label ?? definition.name,
579
+ label: input.attachment?.label ?? authorization.label ?? definition.name,
499
580
  snapshot: input.domain === void 0 ? null : yield* snapshot(input.domain)
500
581
  });
501
582
  });
@@ -649,7 +730,10 @@ const layer = (api, options = {}) => HttpApiBuilder.group(api, "domainkit", (han
649
730
  })) : Effect.succeed(attempt.receipt)))).handle("observe", ({ params, payload, request }) => as("observe", request, verify.observe({
650
731
  domain: params.domain,
651
732
  ...payload.requirements === void 0 ? {} : { requirements: payload.requirements }
652
- }))).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)));
653
737
  }));
654
738
  /** A standalone API for hosts not on HttpApi. Mount at any prefix. */
655
739
  const api = HttpApi.make("domainkit").add(group);
@@ -660,6 +744,6 @@ const toWebHandler = (services, options = {}) => {
660
744
  return HttpRouter.toWebHandler(app, { disableLogger: true });
661
745
  };
662
746
  //#endregion
663
- 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 };
664
748
 
665
749
  //# sourceMappingURL=Server.mjs.map