domainkit 0.15.0 → 0.17.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.
@@ -130,6 +130,94 @@ declare const Attempt_base: Schema.Class<Attempt, Schema.Struct<{
130
130
  /** One durable plan -> approval -> receipt lifecycle. */
131
131
  export declare class Attempt extends Attempt_base {
132
132
  }
133
+ export declare const BatchId: Schema.brand<Schema.String, "@domainkit/BatchId">;
134
+ export type BatchId = typeof BatchId.Type;
135
+ /**
136
+ * Where a batch stands, recomputed from its items' attempts after every transition.
137
+ *
138
+ * `planning` while any item is still without a plan, `planned` once every item carries one,
139
+ * `approved` once the principal bound the batch digest, then the apply states. `partial` and
140
+ * `failed` are resumable; `complete` and `rejected` are terminal and leave the unfinished index.
141
+ */
142
+ export declare const BatchStatus: Schema.Literals<readonly ["planning", "planned", "approved", "applying", "complete", "partial", "failed", "rejected"]>;
143
+ export type BatchStatus = typeof BatchStatus.Type;
144
+ /** Who bound the batch to its digest, and when. The digest itself is `Batch.digest`. */
145
+ export declare const BatchApproval: Schema.Struct<{
146
+ readonly actorId: Schema.String;
147
+ readonly at: Schema.DateTimeUtcFromString;
148
+ }>;
149
+ export type BatchApproval = typeof BatchApproval.Type;
150
+ declare const Batch_base: Schema.Class<Batch, Schema.Struct<{
151
+ readonly id: Schema.brand<Schema.String, "@domainkit/BatchId">;
152
+ readonly ownerId: Schema.String;
153
+ readonly kind: Schema.Literals<readonly ["provisioning", "cleanup"]>;
154
+ readonly status: Schema.Literals<readonly ["planning", "planned", "approved", "applying", "complete", "partial", "failed", "rejected"]>;
155
+ /** SHA-256 over the items' `attachmentId:planDigest` pairs, bound at approval. */
156
+ readonly digest: Schema.NullOr<Schema.brand<Schema.String, "@domainkit/Digest">>;
157
+ readonly approval: Schema.NullOr<Schema.Struct<{
158
+ readonly actorId: Schema.String;
159
+ readonly at: Schema.DateTimeUtcFromString;
160
+ }>>;
161
+ readonly rejection: Schema.NullOr<Schema.Struct<{
162
+ readonly actorId: Schema.String;
163
+ readonly reason: Schema.NullOr<Schema.String>;
164
+ readonly at: Schema.DateTimeUtcFromString;
165
+ }>>;
166
+ /** Unique per owner: a replayed create returns the batch this key already made. */
167
+ readonly idempotencyKey: Schema.String;
168
+ readonly createdBy: Schema.String;
169
+ readonly createdAt: Schema.DateTimeUtcFromString;
170
+ readonly updatedAt: Schema.DateTimeUtcFromString;
171
+ /** When the batch reached `complete` or `rejected`; null while it still owes a move. */
172
+ readonly completedAt: Schema.NullOr<Schema.DateTimeUtcFromString>;
173
+ }>, {}>;
174
+ /**
175
+ * Many domains planned together, approved once, and applied with bounded concurrency.
176
+ *
177
+ * The batch owns the aggregate's own state only: its digest, who approved or declined it, and the
178
+ * status recomputed from its items. Every plan, approval, receipt, lease, and failure stays on the
179
+ * attempt an item points at, so nothing is stored twice.
180
+ */
181
+ export declare class Batch extends Batch_base {
182
+ }
183
+ declare const BatchItem_base: Schema.Class<BatchItem, Schema.Struct<{
184
+ readonly batchId: Schema.brand<Schema.String, "@domainkit/BatchId">;
185
+ readonly attachmentId: Schema.String;
186
+ /** The item's place in the batch, as `create` received it. */
187
+ readonly position: Schema.Number;
188
+ readonly attemptId: Schema.NullOr<Schema.brand<Schema.String, "@domainkit/PlanId">>;
189
+ readonly planFailure: Schema.NullOr<Schema.String>;
190
+ }>, {}>;
191
+ /**
192
+ * One domain's place in a batch: a pointer to the attachment and to the attempt carrying its plan.
193
+ *
194
+ * `attemptId` is null until the item is planned, and `planFailure` carries why the last planning
195
+ * pass stopped, cleared the moment a plan lands.
196
+ */
197
+ export declare class BatchItem extends BatchItem_base {
198
+ }
199
+ /**
200
+ * The status a batch holds, given whether it was approved or declined and where each item's
201
+ * attempt stands. `null` names an item that has no plan yet.
202
+ *
203
+ * Both `Storage` implementations recompute the stored status through this after every transition,
204
+ * so the owner-scoped unfinished index never disagrees with the attempts it summarizes.
205
+ */
206
+ export declare const batchStatusOf: (input: {
207
+ readonly approved: boolean;
208
+ readonly rejected: boolean;
209
+ readonly items: ReadonlyArray<AttemptStatus | null>;
210
+ }) => BatchStatus;
211
+ /** A batch with its items, in `position` order. Every batch read returns one. */
212
+ export interface BatchAggregate {
213
+ readonly batch: Batch;
214
+ readonly items: ReadonlyArray<BatchItem>;
215
+ }
216
+ /** One item's attempt approval, as `batches.approve` writes it beside the batch's own. */
217
+ export interface BatchItemApproval {
218
+ readonly attachmentId: string;
219
+ readonly approval: Approval.Model;
220
+ }
133
221
  export declare const RequirementStatus: Schema.Literals<readonly ["satisfied", "missing", "mismatch", "unknown"]>;
134
222
  export type RequirementStatus = typeof RequirementStatus.Type;
135
223
  export declare const Overall: Schema.Literals<readonly ["ready", "pending", "failed"]>;
@@ -247,6 +335,72 @@ export interface Interface {
247
335
  readonly complete: (id: Plan.PlanId, receipt: Receipt.Model) => Fx<Attempt>;
248
336
  readonly fail: (id: Plan.PlanId, message: string) => Fx<Attempt>;
249
337
  };
338
+ /**
339
+ * The batch aggregate: many attempts planned together and approved once.
340
+ *
341
+ * Every transition runs in one transaction over the locked batch row and leaves `status`
342
+ * recomputed from the items' attempts, so the owner-scoped unfinished index never disagrees
343
+ * with the attempts it summarizes.
344
+ */
345
+ readonly batches: {
346
+ /**
347
+ * Insert the batch and one item per attachment, or return the batch this owner already
348
+ * created under `idempotencyKey`. A replay ignores `attachmentIds` and returns what is
349
+ * stored, which is what makes a retried create safe.
350
+ */
351
+ readonly create: (input: {
352
+ readonly kind: Plan.Kind;
353
+ readonly idempotencyKey: string;
354
+ /** One item per attachment, in order; empty or duplicated fails `InvalidInput`. */
355
+ readonly attachmentIds: ReadonlyArray<string>;
356
+ }) => Fx<BatchAggregate>;
357
+ readonly get: (id: BatchId) => Fx<BatchAggregate>;
358
+ /**
359
+ * The batch this owner created under `idempotencyKey`, if any.
360
+ *
361
+ * What a caller reads before it validates a create's payload, so a retried request answers
362
+ * with the batch the first one made whatever the retry now says.
363
+ */
364
+ readonly byIdempotencyKey: (key: string) => Fx<Option.Option<BatchAggregate>>;
365
+ /** Every batch that has not reached `complete` or `rejected`, most recently touched first. */
366
+ readonly listUnfinished: () => Fx<ReadonlyArray<BatchAggregate>>;
367
+ /**
368
+ * Point an item at the attempt that now carries its plan and clear its plan failure.
369
+ *
370
+ * Fails `Stale` unless the batch is still `planning` or `planned`. That check is the fence a
371
+ * planner still in flight hits when the principal declines the batch underneath it, so plan
372
+ * state never lands on a rejected aggregate.
373
+ */
374
+ readonly recordItemPlan: (id: BatchId, attachmentId: string, attemptId: Plan.PlanId) => Fx<BatchAggregate>;
375
+ /** Record why an item could not be planned; the batch stays `planning`. Same fence. */
376
+ readonly recordItemPlanFailure: (id: BatchId, attachmentId: string, message: string) => Fx<BatchAggregate>;
377
+ /**
378
+ * Bind the batch to `digest` and approve every item's attempt in the same transaction, so a
379
+ * batch is never approved without the per-attempt approvals `attempts.apply` needs.
380
+ *
381
+ * Fails `Stale` unless the batch is `planned` and every item still points at the attempt its
382
+ * approval names. Approving an approved batch under the same digest returns it unchanged; a
383
+ * different digest fails `Stale`.
384
+ */
385
+ readonly approve: (id: BatchId, input: {
386
+ readonly digest: Plan.Digest;
387
+ readonly actorId: string;
388
+ /** One per item, covering every item in the batch. */
389
+ readonly approvals: ReadonlyArray<BatchItemApproval>;
390
+ }) => Fx<BatchAggregate>;
391
+ /**
392
+ * Decline the batch and every planned attempt under it, in one transaction. Terminal.
393
+ *
394
+ * Rejecting again returns the batch unchanged; a batch that reached `approved` or later
395
+ * fails `Stale`.
396
+ */
397
+ readonly reject: (id: BatchId, input: {
398
+ readonly actorId: string;
399
+ readonly reason: string | null;
400
+ }) => Fx<BatchAggregate>;
401
+ /** Recompute the stored status from the items' attempts, after applying moved them. */
402
+ readonly refresh: (id: BatchId) => Fx<BatchAggregate>;
403
+ };
250
404
  readonly readiness: {
251
405
  /** One row per (owner, domain); `attachmentId`, when set, must exist for the owner. */
252
406
  readonly put: (readiness: Readiness) => Fx<void>;
@@ -334,6 +488,28 @@ export interface AsyncInterface {
334
488
  readonly complete: (principal: Principal.Interface, id: Plan.PlanId, receipt: Receipt.Model) => Promise<Attempt>;
335
489
  readonly fail: (principal: Principal.Interface, id: Plan.PlanId, message: string) => Promise<Attempt>;
336
490
  };
491
+ readonly batches: {
492
+ readonly create: (principal: Principal.Interface, input: {
493
+ readonly kind: Plan.Kind;
494
+ readonly idempotencyKey: string;
495
+ readonly attachmentIds: ReadonlyArray<string>;
496
+ }) => Promise<BatchAggregate>;
497
+ readonly get: (principal: Principal.Interface, id: BatchId) => Promise<BatchAggregate>;
498
+ readonly byIdempotencyKey: (principal: Principal.Interface, key: string) => Promise<BatchAggregate | null>;
499
+ readonly listUnfinished: (principal: Principal.Interface) => Promise<ReadonlyArray<BatchAggregate>>;
500
+ readonly recordItemPlan: (principal: Principal.Interface, id: BatchId, attachmentId: string, attemptId: Plan.PlanId) => Promise<BatchAggregate>;
501
+ readonly recordItemPlanFailure: (principal: Principal.Interface, id: BatchId, attachmentId: string, message: string) => Promise<BatchAggregate>;
502
+ readonly approve: (principal: Principal.Interface, id: BatchId, input: {
503
+ readonly digest: Plan.Digest;
504
+ readonly actorId: string;
505
+ readonly approvals: ReadonlyArray<BatchItemApproval>;
506
+ }) => Promise<BatchAggregate>;
507
+ readonly reject: (principal: Principal.Interface, id: BatchId, input: {
508
+ readonly actorId: string;
509
+ readonly reason: string | null;
510
+ }) => Promise<BatchAggregate>;
511
+ readonly refresh: (principal: Principal.Interface, id: BatchId) => Promise<BatchAggregate>;
512
+ };
337
513
  readonly readiness: {
338
514
  readonly put: (principal: Principal.Interface, readiness: Readiness) => Promise<void>;
339
515
  readonly get: (principal: Principal.Interface, domain: string) => Promise<Readiness | null>;
@@ -29,6 +29,7 @@ export declare const make: (storage: Storage.Interface, connect: Connect.Interfa
29
29
  readonly sourceReceiptId: Receipt.ReceiptId | null;
30
30
  }) => Fx<Plan.Model>;
31
31
  approve: (plan: Plan.Model | Plan.PlanId, options?: ApproveOptions) => Fx<Approval.Model>;
32
+ draftApproval: (attempt: Storage.Attempt, options?: ApproveOptions) => Fx<Approval.Model>;
32
33
  reject: (plan: Plan.Model | Plan.PlanId, options?: {
33
34
  readonly reason?: string;
34
35
  }) => Fx<Storage.Attempt>;
@@ -7,7 +7,7 @@ import { Effect, Schema } from "effect";
7
7
  import * as Reason from "../Reason.ts";
8
8
  export type Category = "request" | "auth" | "plan" | "provider" | "storage" | "internal";
9
9
  declare const DomainKitError_base: Schema.Class<DomainKitError, Schema.TaggedStruct<"DomainKitError", {
10
- readonly reason: Schema.suspend<Schema.Union<readonly [typeof Reason.InvalidInput, typeof Reason.Unauthenticated, typeof Reason.Forbidden, typeof Reason.NotFound, typeof Reason.Conflict, typeof Reason.Stale, typeof Reason.Expired, typeof Reason.Busy, typeof Reason.ProviderRejected, typeof Reason.ProviderUnavailable, typeof Reason.ProviderConflict, typeof Reason.Unsupported, typeof Reason.Reconnect, typeof Reason.StorageFailed, typeof Reason.CryptoFailed, typeof Reason.ResolverFailed]>>;
10
+ readonly reason: Schema.suspend<Schema.Union<readonly [typeof Reason.InvalidInput, typeof Reason.Unauthenticated, typeof Reason.Forbidden, typeof Reason.NotFound, typeof Reason.Conflict, typeof Reason.Stale, typeof Reason.BatchStale, typeof Reason.Expired, typeof Reason.Busy, typeof Reason.ProviderRejected, typeof Reason.ProviderUnavailable, typeof Reason.ProviderConflict, typeof Reason.Unsupported, typeof Reason.Reconnect, typeof Reason.StorageFailed, typeof Reason.CryptoFailed, typeof Reason.ResolverFailed]>>;
11
11
  }>, import("effect/Cause").YieldableError>;
12
12
  export declare class DomainKitError extends DomainKitError_base {
13
13
  get category(): Category;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domainkit",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Build reviewable DNS plans for custom domains in TypeScript",
5
5
  "keywords": [
6
6
  "custom-domains",
@@ -143,16 +143,17 @@
143
143
  "tldts": "^7.4.11"
144
144
  },
145
145
  "devDependencies": {
146
- "@effect/vitest": "4.0.0-rc.112",
146
+ "@effect/vitest": "4.0.0-rc.117",
147
147
  "@types/bun": "1.4.0",
148
- "effect": "4.0.0-rc.112",
148
+ "effect": "4.0.0-rc.117",
149
149
  "miniflare": "3.20250718.3",
150
150
  "tsdown": "0.22.14",
151
151
  "typescript": "7.0.2",
152
- "vitest": "4.1.11"
152
+ "vite": "7.1.12",
153
+ "vitest": "5.0.1"
153
154
  },
154
155
  "peerDependencies": {
155
- "effect": ">=4.0.0-rc.112 <5.0.0"
156
+ "effect": ">=4.0.0-rc.117 <5.0.0"
156
157
  },
157
158
  "engines": {
158
159
  "node": ">=24.10.0"