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 +9 -2
- package/dist/Provision.mjs +209 -19
- package/dist/Provision.mjs.map +1 -1
- package/dist/Reason.mjs +22 -2
- package/dist/Reason.mjs.map +1 -1
- package/dist/Server.mjs +86 -4
- package/dist/Server.mjs.map +1 -1
- package/dist/Storage.mjs +100 -2
- package/dist/Storage.mjs.map +1 -1
- package/dist/internal/attempts.mjs +14 -4
- package/dist/internal/attempts.mjs.map +1 -1
- package/dist/internal/conformance/storage.mjs +183 -0
- package/dist/internal/conformance/storage.mjs.map +1 -1
- package/dist/internal/error.mjs +2 -0
- package/dist/internal/error.mjs.map +1 -1
- package/dist/internal/storage-memory.mjs +185 -2
- package/dist/internal/storage-memory.mjs.map +1 -1
- package/dist/package.mjs +1 -1
- package/dist/types/Provision.d.ts +124 -3
- package/dist/types/Reason.d.ts +18 -2
- package/dist/types/Server.d.ts +3619 -202
- package/dist/types/Storage.d.ts +176 -0
- package/dist/types/internal/attempts.d.ts +1 -0
- package/dist/types/internal/error.d.ts +1 -1
- package/package.json +1 -1
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
|
|
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,
|
|
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.
|
package/dist/Provision.mjs
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
|
|
2
2
|
import { decode } from "./DomainName.mjs";
|
|
3
|
-
import {
|
|
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 "./
|
|
6
|
-
import { Service as Service$2 } from "./
|
|
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$
|
|
37
|
-
const connect = yield* Service$
|
|
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*
|
|
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
|
package/dist/Provision.mjs.map
CHANGED
|
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkKA,IAAa,UAAb,cAA6B,QAAQ,QAA4B,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC;AAU5F,MAAa,WAAwB;CACnC,WAAW;CACX,cAAc;CACd,kBAAkB;AACpB;AACA,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;;CAGA,MAAM,WACJ,YACA,cACA,UAEA,OAAO,IAAI,aAAa;EACtB,OAAOC,aAAsB,YAAY,YAAY;EACrD,MAAM,EAAE,SAAS,WAAW,OAAO,QAAQ,QAAQ,WAAW,EAAE;EAChE,MAAM,WAAW,OAAO,QAAQ,IAAI,MAAM,CAAC,CAAC,KAAK,OAAO,IAAI;EAC5D,MAAM,aAAa,OAAOC,UACxB,cACA,SAAS,KAAK,EAAE,aAAa,MAAM,CACrC;EACA,OAAO,OAAO,SAAS,OAAO;GAC5B;GACA;GACA;GACA;GACA,iBAAiB;EACnB,CAAC;CACH,CAAC;CAEH,MAAM,cAAc,OAAsB,WACxCH,KAAY,IAAII,WAAkB;EAAE,SAAS,MAAM;EAAI,QAAQ,MAAM;EAAQ;CAAO,CAAC,CAAC;;CAGxF,MAAM,qBAAqB,OACzB,OAAO,QAAQ,QAAQ,QAAQ,IAAI,EAAE,IAAI,cACvC,UAAU,MAAM,SAAS,iBACrB,OAAO,QAAQ,SAAS,IACxBJ,KAAY,IAAIC,SAAgB;EAAE,QAAQ;EAAS;CAAG,CAAC,CAAC,CAC9D;;;;;CAMF,MAAM,YAAY,UAChB,MAAM,MAAM,SAAS,KAAK,SAAS,IAAI,IACnC,OAAO,QAAQ,IAAI,IACnB,OAAO,IACL,UACE,MACG,KAAK,SAAS,GAAG,KAAK,aAAa,GAAG,KAAK,MAAM,UAAU,IAAI,CAAC,CAChE,KAAK,CAAC,CACN,KAAK,IAAI,CACd,GAAA,OACY,IACd;CAEN,MAAM,aAAa,cACjB,OAAO,IAAI,aAAa;EACtB,MAAM,SAAS,OAAO;EACtB,MAAM,QAAQ,OAAO,OAAO,QAC1B,UAAU,QACT,SACC,KAAK,cAAc,OACf,OAAO,QAAQ;GACb,cAAc,KAAK;GACnB,UAAU,KAAK;GACf,MAAM;GACN,QAAQ;GACR,UAAU;GACV,SAAS;GACT,WAAW;GACX,SAAS;GACT,aAAa,KAAK;EACpB,CAAC,IACD,OAAO,IAAI,QAAQ,SAAS,IAAI,KAAK,SAAS,IAAI,aAAa;GAC7D,cAAc,KAAK;GACnB,UAAU,KAAK;GACf,MAAM,QAAQ;GACd,QAAQ,QAAQ;GAChB,UAAU,QAAQ;GAClB,SAAS,QAAQ;GACjB,WAAW,QAAQ;GACnB,SAAS,QAAQ;GACjB,aAAa,KAAK;EACpB,EAAE,GACR,EAAE,aAAa,OAAO,iBAAiB,CACzC;EACA,OAAO;GACL,IAAI,UAAU,MAAM;GACpB,QAAQ,UAAU,MAAM;GACxB,QAAQ,UAAU,MAAM,WAAW,OAAO,SAAS,KAAK;GACxD,UAAU,UAAU,MAAM;GAC1B,WAAW,UAAU,MAAM;GAC3B,WAAW,UAAU,MAAM;GAC3B,WAAW,UAAU,MAAM;GAC3B,aAAa,UAAU,MAAM;GAC7B;EACF;CACF,CAAC;;CAGH,MAAM,eACJ,WAEA,OAAO,KACL,OAAO,QACP,OAAO,OAAO,UACZ,MAAM,OAAO,SAAS,eAAe,OAAO,OAAO,OAAO,KAAK,KAAK,CACtE,CACF;;;;;CAMF,MAAM,eACJ,WACA,aAKA,OAAO,IAAI,aAAa;EACtB,MAAM,SAAS,OAAO;EACtB,MAAM,KAAK,UAAU,MAAM;EAC3B,MAAM,UAAU,UAAU,MAAM,SAAS,SAAS;GAChD,IAAI,KAAK,cAAc,MAAM,OAAO,CAAC;GACrC,MAAM,QAAQ,SAAS,MAAM,EAAE,iBAAiB,WAAW,OAAO,KAAK,YAAY;GACnF,OAAO,UAAU,KAAA,IAAY,CAAC,IAAI,CAAC,KAAK;EAC1C,CAAC;EACD,OAAO,OAAO,QACZ,UACC,EAAE,YAAY,mBACb,QAAQ,YAAY,cAAc,OAAO,SAAS,CAAC,CAAC,KAClD,OAAO,YAAY;GAGjB,YAAY,UACV,YACE,QAAQ,QAAQ,sBAAsB,IAAI,WAAW,IAAI,MAAM,OAAO,CACxE;GACF,YAAY,SACV,QAAQ,QAAQ,eAAe,IAAI,WAAW,IAAI,KAAK,EAAE,CAAC,CAAC,KACzD,OAAO,QACP,OAAO,OAAO,UACZ,MAAM,OAAO,SAAS,eAIlB,OAAO,OACL,SAAS,OAAO,KAAK,IAAI,EAAE,QAAQ,yBAAyB,CAAC,CAC/D,IACA,OAAO,KAAK,KAAK,CACvB,CACF;EACJ,CAAC,CACH,GACF;GAAE,aAAa,OAAO;GAAkB,SAAS;EAAK,CACxD;EACA,OAAO,OAAO,UAAU,OAAO,QAAQ,QAAQ,IAAI,EAAE,CAAC;CACxD,CAAC;CAEH,MAAM,kBAAkB,UACtB,OAAO,QAAQ,QAAQ,SACrB,OAAO,IAAI,cAAc,KAAK,MAAM,IAAI,gBAAgB;EACtD;EACA,cAAc,KAAK;CACrB,EAAE,CACJ;;;;;;;;CASF,MAAM,iBAAiB,UACrB,OAAO,IACL,OAAO,QAAQ,QAAQ,SACrB,cAAc,KAAK,MAAM,CAAC,CAAC,KACzB,OAAO,KAAK,eAAe,CAAC;EAAE;EAAY,cAAc,KAAK;CAAa,CAAC,CAAC,GAC5E,OAAO,OAAO,UACZ,MAAM,OAAO,SAAS,cAAc,MAAM,OAAO,SAAS,iBACtD,OAAO,QACL,CAAC,CAIH,IACA,OAAO,KAAK,KAAK,CACvB,CACF,CACF,IACC,WAAW,OAAO,KAAK,CAC1B;CAkHF,OAAO;EACL,OAAO,UACL,OAAO,IAAI,aAAa;GACtB,MAAM,SAAS,OAAO;GACtB,MAAM,aAAa,OAAO,cAAc,MAAM,MAAM;GACpD,OAAO,OAAO,QAAQ,YAAY,MAAM,cAAc,OAAO,SAAS;EACxE,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;EACH,OAAA;GAhIA,SAAS,UACP,OAAO,IAAI,aAAa;IAItB,MAAM,SAAS,OAAO,QAAQ,QAAQ,iBAAiB,MAAM,cAAc;IAC3E,IAAI,OAAO,OAAO,MAAM,GAAG;KACzB,MAAM,YAAY,OAAO;KACzB,IAAI,UAAU,MAAM,SAAS,gBAC3B,OAAO,OAAOD,KACZ,IAAIC,SAAgB;MAAE,QAAQ;MAAS,IAAI,UAAU,MAAM;KAAG,CAAC,CACjE;KAEF,OAAO,OAAO,YAAY,WAAW,OAAO,cAAc,MAAM,KAAK,CAAC;IACxE;IACA,MAAM,WAAW,OAAO,eAAe,MAAM,KAAK;IAClD,MAAM,YAAY,OAAO,QAAQ,QAAQ,OAAO;KAC9C,MAAM;KACN,gBAAgB,MAAM;KACtB,eAAe,SAAS,KAAK,EAAE,iBAAiB,WAAW,EAAE;IAC/D,CAAC;IACD,OAAO,OAAO,YAAY,WAAW,QAAQ;GAC/C,CAAC;GACH,iBAAiB,IAAI,UACnB,OAAO,IAAI,aAAa;IACtB,MAAM,YAAY,OAAO,kBAAkB,EAAE;IAC7C,MAAM,WAAW,OAAO,eAAe,MAAM,KAAK;IAClD,OAAO,OAAO,YAAY,WAAW,QAAQ;GAC/C,CAAC;GACH,UAAU,IAAI,YACZ,OAAO,IAAI,aAAa;IACtB,MAAM,YAAY,OAAOI;IACzB,MAAM,YAAY,OAAO,kBAAkB,EAAE;IAC7C,MAAM,UAAU,OAAO,UAAU,SAAS;IAG1C,IAAI,QAAQ,WAAW,QAAQ,QAAQ,WAAW,QAAQ,QACxD,OAAO,OAAO,WAAW,UAAU,OAAO,QAAQ,MAAM;IAE1D,IAAI,UAAU,MAAM,aAAa,MAAM,OAAO;IAC9C,MAAM,YAAY,OAAO,OAAO,QAAQ,UAAU,QAAQ,SACxD,OAAO,IAAI,aAAa;KACtB,IAAI,KAAK,cAAc,MAAM,OAAO,OAAO,WAAW,UAAU,OAAO,IAAI;KAC3E,MAAM,UAAU,OAAO,QAAQ,SAAS,IAAI,KAAK,SAAS;KAC1D,OAAO;MACL,cAAc,KAAK;MACnB,UAAU,QAAQ,aAAa,OAAO,SAAS,cAAc,OAAO;KACtE;IACF,CAAC,CACH;IAGA,MAAM,WAAW,OAAO,QAAQ,QAAQ,QAAQ,IAAI;KAClD,QAAQ,QAAQ;KAChB,SAAS,UAAU;KACnB;IACF,CAAC;IACD,OAAO,OAAO,UAAU,QAAQ;GAClC,CAAC;GACH,QAAQ,OACN,OAAO,IAAI,aAAa;IACtB,MAAM,SAAS,OAAO;IACtB,MAAM,YAAY,OAAO,kBAAkB,EAAE;IAC7C,IAAI,UAAU,MAAM,WAAW,YAAY,OAAO,OAAO,UAAU,SAAS;IAC5E,IAAI,UAAU,MAAM,aAAa,MAC/B,OAAO,OAAO,WAAW,UAAU,OAAO,UAAU,MAAM,MAAM;IAElE,OAAO,OAAO,QACZ,UAAU,QACT,SACC,OAAO,IAAI,aAAa;KACtB,IAAI,KAAK,cAAc,MAAM;KAC7B,MAAM,UAAU,OAAO,QAAQ,SAAS,IAAI,KAAK,SAAS;KAC1D,IAAI,QAAQ,YAAY,QAAQ,QAAQ,aAAa,MAAM;KAG3D,OAAO,SAAS,MAAM,QAAQ,SAAS,IAAI,MAAM,CAAC,CAAC,KACjD,OAAO,QACP,OAAO,YAAY,OAAO,IAAI,CAChC;IACF,CAAC,GACH;KAAE,aAAa,OAAO;KAAkB,SAAS;IAAK,CACxD;IACA,OAAO,OAAO,UAAU,OAAO,QAAQ,QAAQ,QAAQ,EAAE,CAAC;GAC5D,CAAC;GACH,SAAS,IAAI,YACX,OAAO,IAAI,aAAa;IACtB,MAAM,YAAY,OAAOA;IACzB,OAAO,kBAAkB,EAAE;IAC3B,MAAM,WAAW,OAAO,QAAQ,QAAQ,OAAO,IAAI;KACjD,SAAS,UAAU;KACnB,QAAQ,SAAS,UAAU;IAC7B,CAAC;IACD,OAAO,OAAO,UAAU,QAAQ;GAClC,CAAC;GACH,MAAM,OAAO,OAAO,QAAQ,kBAAkB,EAAE,GAAG,SAAS;GAC5D,YACE,OAAO,IAAI,QAAQ,QAAQ,eAAe,IAAI,eAC5C,WACG,QAAQ,EAAE,OAAO,UAAU,IAAI,SAAS,cAAc,CAAC,CACvD,KAAK,EAAE,OAAO,KAAK,aAAa;IAC/B,IAAI,IAAI;IACR,QAAQ,IAAI;IACZ,WAAW,MAAM;IACjB,WAAW,IAAI;IACf,WAAW,IAAI;IACf,aAAa,IAAI;GACnB,EAAE,CACN;EAoBE;CACN;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;;;;;AAM1D,MAAa,QAAQ;CACnB,QAAQ,UAAU,YAAY,QAAQ,MAAM,MAAM;CAClD,gBAAgB,UAAU,YAAY,QAAQ,MAAM,cAAc;CAClE,SAAS,UAAU,YAAY,QAAQ,MAAM,OAAO;CACpD,OAAO,UAAU,YAAY,QAAQ,MAAM,KAAK;CAChD,QAAQ,UAAU,YAAY,QAAQ,MAAM,MAAM;CAClD,KAAK,UAAU,YAAY,QAAQ,MAAM,GAAG;CAC5C,MAAM,UAAU,YAAY,QAAQ,MAAM,IAAI;AAChD"}
|
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
|
package/dist/Reason.mjs.map
CHANGED
|
@@ -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":"
|
|
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,
|
|
@@ -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
|