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.
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
@@ -81,8 +88,8 @@ export const DomainKitLive = DomainKit.layer({
81
88
  providers: [
82
89
  Cloudflare.provider({
83
90
  oauth: {
84
- clientId: Config.string("CF_CLIENT_ID"),
85
- clientSecret: Config.redacted("CF_CLIENT_SECRET"),
91
+ clientId: Config.String("CF_CLIENT_ID"),
92
+ clientSecret: Config.Redacted("CF_CLIENT_SECRET"),
86
93
  },
87
94
  }),
88
95
  Vercel.provider(), // tokens only
@@ -153,9 +160,9 @@ export const ApiLive = HttpApiBuilder.layer(Api).pipe(
153
160
  );
154
161
  ```
155
162
 
156
- `Server.group` is one `HttpApiGroup` with fifteen typed endpoints covering the whole lifecycle:
163
+ `Server.group` is one `HttpApiGroup` with twenty-five typed endpoints covering the whole lifecycle:
157
164
  inspect, discover, connect, callback, attach, detach, disconnect, plan, approve, reject, apply, read
158
- a plan or a receipt, observe, and build a cleanup plan. `Identity` is the only service you write, and
165
+ a plan or a receipt, observe, build a cleanup plan, and the seven batch routes under `/batches`. `Identity` is the only service you write, and
159
166
  every handler derives the `Principal` for the request it is serving. `Server.group.prefix("/internal/dns")` moves
160
167
  every route, and the OAuth callback URL follows the mount. `OpenApi.fromApi(Server.api)` documents
161
168
  the group.
package/dist/Custody.mjs CHANGED
@@ -37,7 +37,7 @@ const make = (options) => Effect.gen(function* () {
37
37
  const layer = (options) => Layer.effect(Service)(make(options));
38
38
  /** Same options, each field a `Config`. Defaults to `DOMAINKIT_CUSTODY_KEY`. */
39
39
  const layerConfig = (options = {}) => Layer.effect(Service)(Effect.gen(function* () {
40
- const key = yield* (options.key ?? Config.redacted("DOMAINKIT_CUSTODY_KEY")).pipe(Effect.mapError((cause) => new DomainKitError({ reason: new InvalidInput({
40
+ const key = yield* (options.key ?? Config.Redacted("DOMAINKIT_CUSTODY_KEY")).pipe(Effect.mapError((cause) => new DomainKitError({ reason: new InvalidInput({
41
41
  message: cause.message,
42
42
  field: "key"
43
43
  }) })));
@@ -1 +1 @@
1
- {"version":3,"file":"Custody.mjs","names":["Aes.decodeKey","Errors.fail","Reason.InvalidInput","Aes.importKey","Aes.seal","Aes.open","Errors.DomainKitError","Aes.generateKey","Errors.isDomainKitError","Reason.CryptoFailed"],"sources":["../src/Custody.ts"],"sourcesContent":["/**\n * Seals provider credentials before Storage writes them. The default is AES-256-GCM through\n * WebCrypto from one configured key; hosts with a KMS provide their own. There is no plaintext\n * mode.\n *\n * Envelope format: `v1.<iv>.<ciphertext>` with base64url segments and a 12-byte IV. Rotating\n * the key requires re-sealing stored credentials; `open` fails with `CryptoFailed` on a mismatch.\n */\nimport { Config, Context, Effect, Layer, Redacted } from \"effect\";\n\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as Aes from \"./internal/aes.ts\";\n\nexport interface Interface {\n readonly seal: (\n plaintext: Redacted.Redacted<string>,\n ) => Effect.Effect<string, Errors.DomainKitError>;\n readonly open: (\n ciphertext: string,\n ) => Effect.Effect<Redacted.Redacted<string>, Errors.DomainKitError>;\n}\n\nexport class Service extends Context.Service<Service, Interface>()(\"@domainkit/Custody\") {}\n\nexport interface Options {\n /** 32 bytes, base64 or hex. Rotating it requires re-sealing stored credentials. */\n readonly key: Redacted.Redacted<string>;\n}\n\nexport const make = (options: Options): Effect.Effect<Interface, Errors.DomainKitError> =>\n Effect.gen(function* () {\n const raw = Aes.decodeKey(Redacted.value(options.key));\n if (raw === null) {\n return yield* Errors.fail(\n new Reason.InvalidInput({\n message: \"Custody key must be 32 bytes encoded as base64 or hex\",\n field: \"key\",\n }),\n );\n }\n const key = yield* Aes.importKey(raw);\n return {\n seal: (plaintext) => Aes.seal(key, plaintext),\n open: (ciphertext) => Aes.open(key, ciphertext),\n };\n });\n\nexport const layer = (options: Options): Layer.Layer<Service, Errors.DomainKitError> =>\n Layer.effect(Service)(make(options));\n\n/** Same options, each field a `Config`. Defaults to `DOMAINKIT_CUSTODY_KEY`. */\nexport const layerConfig = (\n options: { readonly key?: Config.Config<Redacted.Redacted<string>> } = {},\n): Layer.Layer<Service, Errors.DomainKitError> =>\n Layer.effect(Service)(\n Effect.gen(function* () {\n const key = yield* (options.key ?? Config.redacted(\"DOMAINKIT_CUSTODY_KEY\")).pipe(\n Effect.mapError(\n (cause) =>\n new Errors.DomainKitError({\n reason: new Reason.InvalidInput({ message: cause.message, field: \"key\" }),\n }),\n ),\n );\n return yield* make({ key });\n }),\n );\n\n/** A fresh random key in the accepted encoding; for tests, playgrounds, and first-time setup. */\nexport const generateKey = Aes.generateKey;\n\nexport interface AsyncInterface {\n readonly seal: (plaintext: string) => Promise<string>;\n readonly open: (ciphertext: string) => Promise<string>;\n}\n\nconst cryptoFailed = (operation: \"seal\" | \"open\") => (cause: unknown) =>\n Errors.isDomainKitError(cause)\n ? cause\n : new Errors.DomainKitError({ reason: new Reason.CryptoFailed({ operation }) });\n\n/** Wrap a Promise-shaped KMS client; rejections become `CryptoFailed`. */\nexport const fromAsync = (service: AsyncInterface): Interface => ({\n seal: (plaintext) =>\n Effect.tryPromise({\n try: () => service.seal(Redacted.value(plaintext)),\n catch: cryptoFailed(\"seal\"),\n }),\n open: (ciphertext) =>\n Effect.tryPromise({ try: () => service.open(ciphertext), catch: cryptoFailed(\"open\") }).pipe(\n Effect.map((value) => Redacted.make(value)),\n ),\n});\n\nexport const layerFromAsync = (service: AsyncInterface): Layer.Layer<Service> =>\n Layer.succeed(Service)(fromAsync(service));\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAuBA,IAAa,UAAb,cAA6B,QAAQ,QAA4B,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC;AAO1F,MAAa,QAAQ,YACnB,OAAO,IAAI,aAAa;CACtB,MAAM,MAAMA,UAAc,SAAS,MAAM,QAAQ,GAAG,CAAC;CACrD,IAAI,QAAQ,MACV,OAAO,OAAOC,KACZ,IAAIC,aAAoB;EACtB,SAAS;EACT,OAAO;CACT,CAAC,CACH;CAEF,MAAM,MAAM,OAAOC,UAAc,GAAG;CACpC,OAAO;EACL,OAAO,cAAcC,KAAS,KAAK,SAAS;EAC5C,OAAO,eAAeC,KAAS,KAAK,UAAU;CAChD;AACF,CAAC;AAEH,MAAa,SAAS,YACpB,MAAM,OAAO,OAAO,CAAC,CAAC,KAAK,OAAO,CAAC;;AAGrC,MAAa,eACX,UAAuE,CAAC,MAExE,MAAM,OAAO,OAAO,CAAC,CACnB,OAAO,IAAI,aAAa;CACtB,MAAM,MAAM,QAAQ,QAAQ,OAAO,OAAO,SAAS,uBAAuB,EAAA,CAAG,KAC3E,OAAO,UACJ,UACC,IAAIC,eAAsB,EACxB,QAAQ,IAAIJ,aAAoB;EAAE,SAAS,MAAM;EAAS,OAAO;CAAM,CAAC,EAC1E,CAAC,CACL,CACF;CACA,OAAO,OAAO,KAAK,EAAE,IAAI,CAAC;AAC5B,CAAC,CACH;;AAGF,MAAa,cAAcK;AAO3B,MAAM,gBAAgB,eAAgC,UACpDC,iBAAwB,KAAK,IACzB,QACA,IAAIF,eAAsB,EAAE,QAAQ,IAAIG,aAAoB,EAAE,UAAU,CAAC,EAAE,CAAC;;AAGlF,MAAa,aAAa,aAAwC;CAChE,OAAO,cACL,OAAO,WAAW;EAChB,WAAW,QAAQ,KAAK,SAAS,MAAM,SAAS,CAAC;EACjD,OAAO,aAAa,MAAM;CAC5B,CAAC;CACH,OAAO,eACL,OAAO,WAAW;EAAE,WAAW,QAAQ,KAAK,UAAU;EAAG,OAAO,aAAa,MAAM;CAAE,CAAC,CAAC,CAAC,KACtF,OAAO,KAAK,UAAU,SAAS,KAAK,KAAK,CAAC,CAC5C;AACJ;AAEA,MAAa,kBAAkB,YAC7B,MAAM,QAAQ,OAAO,CAAC,CAAC,UAAU,OAAO,CAAC"}
1
+ {"version":3,"file":"Custody.mjs","names":["Aes.decodeKey","Errors.fail","Reason.InvalidInput","Aes.importKey","Aes.seal","Aes.open","Errors.DomainKitError","Aes.generateKey","Errors.isDomainKitError","Reason.CryptoFailed"],"sources":["../src/Custody.ts"],"sourcesContent":["/**\n * Seals provider credentials before Storage writes them. The default is AES-256-GCM through\n * WebCrypto from one configured key; hosts with a KMS provide their own. There is no plaintext\n * mode.\n *\n * Envelope format: `v1.<iv>.<ciphertext>` with base64url segments and a 12-byte IV. Rotating\n * the key requires re-sealing stored credentials; `open` fails with `CryptoFailed` on a mismatch.\n */\nimport { Config, Context, Effect, Layer, Redacted } from \"effect\";\n\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as Aes from \"./internal/aes.ts\";\n\nexport interface Interface {\n readonly seal: (\n plaintext: Redacted.Redacted<string>,\n ) => Effect.Effect<string, Errors.DomainKitError>;\n readonly open: (\n ciphertext: string,\n ) => Effect.Effect<Redacted.Redacted<string>, Errors.DomainKitError>;\n}\n\nexport class Service extends Context.Service<Service, Interface>()(\"@domainkit/Custody\") {}\n\nexport interface Options {\n /** 32 bytes, base64 or hex. Rotating it requires re-sealing stored credentials. */\n readonly key: Redacted.Redacted<string>;\n}\n\nexport const make = (options: Options): Effect.Effect<Interface, Errors.DomainKitError> =>\n Effect.gen(function* () {\n const raw = Aes.decodeKey(Redacted.value(options.key));\n if (raw === null) {\n return yield* Errors.fail(\n new Reason.InvalidInput({\n message: \"Custody key must be 32 bytes encoded as base64 or hex\",\n field: \"key\",\n }),\n );\n }\n const key = yield* Aes.importKey(raw);\n return {\n seal: (plaintext) => Aes.seal(key, plaintext),\n open: (ciphertext) => Aes.open(key, ciphertext),\n };\n });\n\nexport const layer = (options: Options): Layer.Layer<Service, Errors.DomainKitError> =>\n Layer.effect(Service)(make(options));\n\n/** Same options, each field a `Config`. Defaults to `DOMAINKIT_CUSTODY_KEY`. */\nexport const layerConfig = (\n options: { readonly key?: Config.Config<Redacted.Redacted<string>> } = {},\n): Layer.Layer<Service, Errors.DomainKitError> =>\n Layer.effect(Service)(\n Effect.gen(function* () {\n const key = yield* (options.key ?? Config.Redacted(\"DOMAINKIT_CUSTODY_KEY\")).pipe(\n Effect.mapError(\n (cause) =>\n new Errors.DomainKitError({\n reason: new Reason.InvalidInput({ message: cause.message, field: \"key\" }),\n }),\n ),\n );\n return yield* make({ key });\n }),\n );\n\n/** A fresh random key in the accepted encoding; for tests, playgrounds, and first-time setup. */\nexport const generateKey = Aes.generateKey;\n\nexport interface AsyncInterface {\n readonly seal: (plaintext: string) => Promise<string>;\n readonly open: (ciphertext: string) => Promise<string>;\n}\n\nconst cryptoFailed = (operation: \"seal\" | \"open\") => (cause: unknown) =>\n Errors.isDomainKitError(cause)\n ? cause\n : new Errors.DomainKitError({ reason: new Reason.CryptoFailed({ operation }) });\n\n/** Wrap a Promise-shaped KMS client; rejections become `CryptoFailed`. */\nexport const fromAsync = (service: AsyncInterface): Interface => ({\n seal: (plaintext) =>\n Effect.tryPromise({\n try: () => service.seal(Redacted.value(plaintext)),\n catch: cryptoFailed(\"seal\"),\n }),\n open: (ciphertext) =>\n Effect.tryPromise({ try: () => service.open(ciphertext), catch: cryptoFailed(\"open\") }).pipe(\n Effect.map((value) => Redacted.make(value)),\n ),\n});\n\nexport const layerFromAsync = (service: AsyncInterface): Layer.Layer<Service> =>\n Layer.succeed(Service)(fromAsync(service));\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAuBA,IAAa,UAAb,cAA6B,QAAQ,QAA4B,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC;AAO1F,MAAa,QAAQ,YACnB,OAAO,IAAI,aAAa;CACtB,MAAM,MAAMA,UAAc,SAAS,MAAM,QAAQ,GAAG,CAAC;CACrD,IAAI,QAAQ,MACV,OAAO,OAAOC,KACZ,IAAIC,aAAoB;EACtB,SAAS;EACT,OAAO;CACT,CAAC,CACH;CAEF,MAAM,MAAM,OAAOC,UAAc,GAAG;CACpC,OAAO;EACL,OAAO,cAAcC,KAAS,KAAK,SAAS;EAC5C,OAAO,eAAeC,KAAS,KAAK,UAAU;CAChD;AACF,CAAC;AAEH,MAAa,SAAS,YACpB,MAAM,OAAO,OAAO,CAAC,CAAC,KAAK,OAAO,CAAC;;AAGrC,MAAa,eACX,UAAuE,CAAC,MAExE,MAAM,OAAO,OAAO,CAAC,CACnB,OAAO,IAAI,aAAa;CACtB,MAAM,MAAM,QAAQ,QAAQ,OAAO,OAAO,SAAS,uBAAuB,EAAA,CAAG,KAC3E,OAAO,UACJ,UACC,IAAIC,eAAsB,EACxB,QAAQ,IAAIJ,aAAoB;EAAE,SAAS,MAAM;EAAS,OAAO;CAAM,CAAC,EAC1E,CAAC,CACL,CACF;CACA,OAAO,OAAO,KAAK,EAAE,IAAI,CAAC;AAC5B,CAAC,CACH;;AAGF,MAAa,cAAcK;AAO3B,MAAM,gBAAgB,eAAgC,UACpDC,iBAAwB,KAAK,IACzB,QACA,IAAIF,eAAsB,EAAE,QAAQ,IAAIG,aAAoB,EAAE,UAAU,CAAC,EAAE,CAAC;;AAGlF,MAAa,aAAa,aAAwC;CAChE,OAAO,cACL,OAAO,WAAW;EAChB,WAAW,QAAQ,KAAK,SAAS,MAAM,SAAS,CAAC;EACjD,OAAO,aAAa,MAAM;CAC5B,CAAC;CACH,OAAO,eACL,OAAO,WAAW;EAAE,WAAW,QAAQ,KAAK,UAAU;EAAG,OAAO,aAAa,MAAM;CAAE,CAAC,CAAC,CAAC,KACtF,OAAO,KAAK,UAAU,SAAS,KAAK,KAAK,CAAC,CAC5C;AACJ;AAEA,MAAa,kBAAkB,YAC7B,MAAM,QAAQ,OAAO,CAAC,CAAC,UAAU,OAAO,CAAC"}
@@ -21,7 +21,7 @@ var DomainName_exports = /* @__PURE__ */ __exportAll({
21
21
  const labelPattern = /^[a-z0-9_](?:[a-z0-9_-]{0,61}[a-z0-9_])?$/;
22
22
  const Branded = Schema.String.check(Schema.makeFilter((value) => isNormalized(value) ? void 0 : "Expected a fully qualified DNS name", { identifier: "DomainName" })).pipe(Schema.brand("@domainkit/DomainName"));
23
23
  /** Decodes any hostname spelling into its normalized, branded form; encodes back to a string. */
24
- const Model = Schema.String.pipe(Schema.decodeTo(Branded, SchemaTransformation.transformOrFail({
24
+ const Model = Schema.String.pipe(Schema.decodeTo(Branded, SchemaTransformation.transformEffect({
25
25
  decode: (input, options) => Effect.try({
26
26
  try: () => normalize(input),
27
27
  catch: (cause) => new SchemaIssue.InvalidValue({ message: errorMessage(cause) }, input, options)
@@ -1 +1 @@
1
- {"version":3,"file":"DomainName.mjs","names":["Errors.DomainKitError","Reason.InvalidInput","Errors.fail"],"sources":["../src/DomainName.ts"],"sourcesContent":["/**\n * A normalized, IDN-aware hostname. Lifecycle inputs accept plain strings and validate them, so\n * most hosts never construct this directly; it exists for hosts that want the brand in their own\n * types.\n */\nimport { Effect, Option, Schema, SchemaIssue, SchemaTransformation } from \"effect\";\nimport { getDomain } from \"tldts\";\n\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\n\nconst labelPattern = /^[a-z0-9_](?:[a-z0-9_-]{0,61}[a-z0-9_])?$/;\n\nconst Branded = Schema.String.check(\n Schema.makeFilter<string>(\n (value) => (isNormalized(value) ? undefined : \"Expected a fully qualified DNS name\"),\n { identifier: \"DomainName\" },\n ),\n).pipe(Schema.brand(\"@domainkit/DomainName\"));\n\n/** Decodes any hostname spelling into its normalized, branded form; encodes back to a string. */\nexport const Model = Schema.String.pipe(\n Schema.decodeTo(\n Branded,\n SchemaTransformation.transformOrFail({\n decode: (input, options) =>\n Effect.try({\n try: () => normalize(input),\n catch: (cause) =>\n new SchemaIssue.InvalidValue({ message: errorMessage(cause) }, input, options),\n }),\n encode: (domain) => Effect.succeed(domain),\n }),\n ),\n);\nexport type Model = typeof Branded.Type;\n\n/** Validates and normalizes; `None` when the input is not a hostname. */\nexport const fromString = (input: string): Option.Option<Model> => {\n try {\n return Option.some(normalizeUnsafe(input));\n } catch {\n return Option.none();\n }\n};\n\n/** Validates and normalizes; throws `DomainKit.Error` (reason `InvalidInput`) when invalid. */\nexport const fromStringUnsafe = (input: string): Model => {\n try {\n return normalizeUnsafe(input);\n } catch (cause) {\n throw new Errors.DomainKitError({\n reason: new Reason.InvalidInput({ message: errorMessage(cause), field: \"domain\" }),\n });\n }\n};\n\n/** Validates and normalizes inside Effect; fails with reason `InvalidInput`. */\nexport const decode = (\n input: string,\n field = \"domain\",\n): Effect.Effect<Model, Errors.DomainKitError> =>\n Effect.suspend(() => {\n try {\n return Effect.succeed(normalizeUnsafe(input));\n } catch (cause) {\n return Errors.fail(new Reason.InvalidInput({ message: errorMessage(cause), field }));\n }\n });\n\nexport const isDomainName = (input: unknown): input is Model =>\n typeof input === \"string\" && isNormalized(input);\n\n/**\n * Zone candidates from the name itself down to the registrable domain, most specific first:\n * `a.b.example.com` -> `a.b.example.com`, `b.example.com`, `example.com`. Empty when the name has\n * no registrable suffix.\n */\nexport const candidates = (domain: Model): ReadonlyArray<Model> => {\n const registrable = getDomain(domain, { allowPrivateDomains: true });\n if (registrable === null) return [];\n const labels = domain.split(\".\");\n const registrableLabels = registrable.split(\".\").length;\n const values: Array<Model> = [];\n for (let index = 0; index <= labels.length - registrableLabels; index += 1) {\n values.push(labels.slice(index).join(\".\") as Model);\n }\n return values;\n};\n\n/** `true` when `name` is `zone` or a subdomain of it. */\nexport const isWithin = (name: string, zone: string): boolean =>\n name === zone || name.endsWith(`.${zone}`);\n\nfunction isNormalized(value: string): boolean {\n return (\n value.length <= 253 &&\n value.includes(\".\") &&\n value.split(\".\").every((label) => labelPattern.test(label))\n );\n}\n\nfunction normalizeUnsafe(input: string): Model {\n return normalize(input) as Model;\n}\n\nfunction normalize(input: string): string {\n const labels = input\n .trim()\n .replace(/\\.+$/, \"\")\n .split(\".\")\n .map((label) => {\n if (label.includes(\"_\") || [...label].every((character) => character.charCodeAt(0) <= 127)) {\n return label.toLowerCase();\n }\n const hostname = new URL(`http://${label}.example`).hostname;\n const normalized = hostname.split(\".\")[0];\n if (normalized === undefined) throw new Error(\"Domain label could not be normalized\");\n return normalized.toLowerCase();\n });\n const value = labels.join(\".\");\n if (!isNormalized(value)) {\n throw new Error(`Expected a fully qualified DNS name, received ${JSON.stringify(input)}`);\n }\n return value;\n}\n\nfunction errorMessage(cause: unknown): string {\n return cause instanceof Error ? cause.message : String(cause);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"DomainName.mjs","names":["Errors.DomainKitError","Reason.InvalidInput","Errors.fail"],"sources":["../src/DomainName.ts"],"sourcesContent":["/**\n * A normalized, IDN-aware hostname. Lifecycle inputs accept plain strings and validate them, so\n * most hosts never construct this directly; it exists for hosts that want the brand in their own\n * types.\n */\nimport { Effect, Option, Schema, SchemaIssue, SchemaTransformation } from \"effect\";\nimport { getDomain } from \"tldts\";\n\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\n\nconst labelPattern = /^[a-z0-9_](?:[a-z0-9_-]{0,61}[a-z0-9_])?$/;\n\nconst Branded = Schema.String.check(\n Schema.makeFilter<string>(\n (value) => (isNormalized(value) ? undefined : \"Expected a fully qualified DNS name\"),\n { identifier: \"DomainName\" },\n ),\n).pipe(Schema.brand(\"@domainkit/DomainName\"));\n\n/** Decodes any hostname spelling into its normalized, branded form; encodes back to a string. */\nexport const Model = Schema.String.pipe(\n Schema.decodeTo(\n Branded,\n SchemaTransformation.transformEffect({\n decode: (input, options) =>\n Effect.try({\n try: () => normalize(input),\n catch: (cause) =>\n new SchemaIssue.InvalidValue({ message: errorMessage(cause) }, input, options),\n }),\n encode: (domain) => Effect.succeed(domain),\n }),\n ),\n);\nexport type Model = typeof Branded.Type;\n\n/** Validates and normalizes; `None` when the input is not a hostname. */\nexport const fromString = (input: string): Option.Option<Model> => {\n try {\n return Option.some(normalizeUnsafe(input));\n } catch {\n return Option.none();\n }\n};\n\n/** Validates and normalizes; throws `DomainKit.Error` (reason `InvalidInput`) when invalid. */\nexport const fromStringUnsafe = (input: string): Model => {\n try {\n return normalizeUnsafe(input);\n } catch (cause) {\n throw new Errors.DomainKitError({\n reason: new Reason.InvalidInput({ message: errorMessage(cause), field: \"domain\" }),\n });\n }\n};\n\n/** Validates and normalizes inside Effect; fails with reason `InvalidInput`. */\nexport const decode = (\n input: string,\n field = \"domain\",\n): Effect.Effect<Model, Errors.DomainKitError> =>\n Effect.suspend(() => {\n try {\n return Effect.succeed(normalizeUnsafe(input));\n } catch (cause) {\n return Errors.fail(new Reason.InvalidInput({ message: errorMessage(cause), field }));\n }\n });\n\nexport const isDomainName = (input: unknown): input is Model =>\n typeof input === \"string\" && isNormalized(input);\n\n/**\n * Zone candidates from the name itself down to the registrable domain, most specific first:\n * `a.b.example.com` -> `a.b.example.com`, `b.example.com`, `example.com`. Empty when the name has\n * no registrable suffix.\n */\nexport const candidates = (domain: Model): ReadonlyArray<Model> => {\n const registrable = getDomain(domain, { allowPrivateDomains: true });\n if (registrable === null) return [];\n const labels = domain.split(\".\");\n const registrableLabels = registrable.split(\".\").length;\n const values: Array<Model> = [];\n for (let index = 0; index <= labels.length - registrableLabels; index += 1) {\n values.push(labels.slice(index).join(\".\") as Model);\n }\n return values;\n};\n\n/** `true` when `name` is `zone` or a subdomain of it. */\nexport const isWithin = (name: string, zone: string): boolean =>\n name === zone || name.endsWith(`.${zone}`);\n\nfunction isNormalized(value: string): boolean {\n return (\n value.length <= 253 &&\n value.includes(\".\") &&\n value.split(\".\").every((label) => labelPattern.test(label))\n );\n}\n\nfunction normalizeUnsafe(input: string): Model {\n return normalize(input) as Model;\n}\n\nfunction normalize(input: string): string {\n const labels = input\n .trim()\n .replace(/\\.+$/, \"\")\n .split(\".\")\n .map((label) => {\n if (label.includes(\"_\") || [...label].every((character) => character.charCodeAt(0) <= 127)) {\n return label.toLowerCase();\n }\n const hostname = new URL(`http://${label}.example`).hostname;\n const normalized = hostname.split(\".\")[0];\n if (normalized === undefined) throw new Error(\"Domain label could not be normalized\");\n return normalized.toLowerCase();\n });\n const value = labels.join(\".\");\n if (!isNormalized(value)) {\n throw new Error(`Expected a fully qualified DNS name, received ${JSON.stringify(input)}`);\n }\n return value;\n}\n\nfunction errorMessage(cause: unknown): string {\n return cause instanceof Error ? cause.message : String(cause);\n}\n"],"mappings":"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"}
@@ -1,9 +1,12 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
2
2
  import { decode } from "./DomainName.mjs";
3
- import { NotFound } from "./Reason.mjs";
3
+ import { Digest } from "./Plan.mjs";
4
+ import { BatchStale, NotFound } from "./Reason.mjs";
4
5
  import { fail } from "./internal/error.mjs";
5
- import { Service as Service$1 } from "./Storage.mjs";
6
- import { Service as Service$2 } from "./Connect.mjs";
6
+ import { Service as Service$1 } from "./Principal.mjs";
7
+ import { Service as Service$2 } from "./Storage.mjs";
8
+ import { Service as Service$3 } from "./Connect.mjs";
9
+ import { sha256Hex } from "./internal/digest.mjs";
7
10
  import { reconcile } from "./internal/planner.mjs";
8
11
  import { assertWithin, make as make$1 } from "./internal/attempts.mjs";
9
12
  import { Context, Effect, Layer, Option } from "effect";
@@ -18,6 +21,7 @@ var Provision_exports = /* @__PURE__ */ __exportAll({
18
21
  Service: () => Service,
19
22
  apply: () => apply,
20
23
  approve: () => approve,
24
+ batch: () => batch,
21
25
  defaults: () => defaults,
22
26
  get: () => get,
23
27
  latest: () => latest,
@@ -29,12 +33,13 @@ var Provision_exports = /* @__PURE__ */ __exportAll({
29
33
  var Service = class extends Context.Service()("@domainkit/Provision") {};
30
34
  const defaults = {
31
35
  planTtlMs: 36e5,
32
- applyLeaseMs: 12e4
36
+ applyLeaseMs: 12e4,
37
+ batchConcurrency: 4
33
38
  };
34
39
  var Policy = class extends Context.Reference("@domainkit/Provision/Policy", { defaultValue: () => defaults }) {};
35
40
  const make = Effect.gen(function* () {
36
- const storage = yield* Service$1;
37
- const connect = yield* Service$2;
41
+ const storage = yield* Service$2;
42
+ const connect = yield* Service$3;
38
43
  const attempts = make$1(storage, connect, "provisioning");
39
44
  const attachmentFor = (input) => Effect.gen(function* () {
40
45
  const domain = yield* decode(input);
@@ -52,21 +57,113 @@ const make = Effect.gen(function* () {
52
57
  receipt: attempt.receipt,
53
58
  rejection: attempt.rejection
54
59
  });
60
+ /** Read the attached zone and record one `planned` attempt for it. */
61
+ const planFor = (attachment, requirements, ttlMs) => Effect.gen(function* () {
62
+ yield* assertWithin(attachment, requirements);
63
+ const { session, target } = yield* connect.session(attachment.id);
64
+ const observed = yield* session.dns(target).list(target.zone);
65
+ const operations = yield* reconcile(requirements, observed.map(({ record }) => record));
66
+ return yield* attempts.record({
67
+ attachment,
68
+ target,
69
+ operations,
70
+ ttlMs,
71
+ sourceReceiptId: null
72
+ });
73
+ });
74
+ const batchStale = (batch, digest) => fail(new BatchStale({
75
+ batchId: batch.id,
76
+ status: batch.status,
77
+ digest
78
+ }));
79
+ /** Provision reads provisioning batches; a cleanup batch is not its aggregate. */
80
+ const provisioningBatch = (id) => Effect.flatMap(storage.batches.get(id), (aggregate) => aggregate.batch.kind === "provisioning" ? Effect.succeed(aggregate) : fail(new NotFound({
81
+ entity: "batch",
82
+ id
83
+ })));
84
+ /**
85
+ * The digest the principal reviews: SHA-256 over the items' sorted `attachmentId:planDigest`
86
+ * pairs, and null while any item is still unplanned.
87
+ */
88
+ const digestOf = (items) => items.some((item) => item.plan === null) ? Effect.succeed(null) : Effect.map(sha256Hex(items.map((item) => `${item.attachmentId}:${item.plan?.digest ?? ""}`).sort().join("\n")), Digest.make);
89
+ const batchView = (aggregate) => Effect.gen(function* () {
90
+ const policy = yield* Policy;
91
+ const items = yield* Effect.forEach(aggregate.items, (item) => item.attemptId === null ? Effect.succeed({
92
+ attachmentId: item.attachmentId,
93
+ position: item.position,
94
+ plan: null,
95
+ status: null,
96
+ approval: null,
97
+ receipt: null,
98
+ rejection: null,
99
+ failure: null,
100
+ planFailure: item.planFailure
101
+ }) : Effect.map(storage.attempts.get(item.attemptId), (attempt) => ({
102
+ attachmentId: item.attachmentId,
103
+ position: item.position,
104
+ plan: attempt.plan,
105
+ status: attempt.status,
106
+ approval: attempt.approval,
107
+ receipt: attempt.receipt,
108
+ rejection: attempt.rejection,
109
+ failure: attempt.failure,
110
+ planFailure: item.planFailure
111
+ })), { concurrency: policy.batchConcurrency });
112
+ return {
113
+ id: aggregate.batch.id,
114
+ status: aggregate.batch.status,
115
+ digest: aggregate.batch.digest ?? (yield* digestOf(items)),
116
+ approval: aggregate.batch.approval,
117
+ rejection: aggregate.batch.rejection,
118
+ createdAt: aggregate.batch.createdAt,
119
+ updatedAt: aggregate.batch.updatedAt,
120
+ completedAt: aggregate.batch.completedAt,
121
+ items
122
+ };
123
+ });
124
+ /** The batch was declined under a planning pass still in flight; the fence did its work. */
125
+ const ignoreFence = (effect) => effect.pipe(Effect.asVoid, Effect.catch((error) => error.reason._tag === "BatchStale" ? Effect.void : Effect.fail(error)));
126
+ /**
127
+ * Plan every item that has no plan yet and for which this call supplied requirements. An item
128
+ * the caller did not name keeps whatever it holds, so a replayed create never re-plans.
129
+ */
130
+ const planMissing = (aggregate, supplied) => Effect.gen(function* () {
131
+ const policy = yield* Policy;
132
+ const id = aggregate.batch.id;
133
+ const pending = aggregate.items.flatMap((item) => {
134
+ if (item.attemptId !== null) return [];
135
+ const match = supplied.find(({ attachment }) => attachment.id === item.attachmentId);
136
+ return match === void 0 ? [] : [match];
137
+ });
138
+ yield* Effect.forEach(pending, ({ attachment, requirements }) => planFor(attachment, requirements, policy.planTtlMs).pipe(Effect.matchEffect({
139
+ onFailure: (error) => ignoreFence(storage.batches.recordItemPlanFailure(id, attachment.id, error.message)),
140
+ onSuccess: (plan) => storage.batches.recordItemPlan(id, attachment.id, plan.id).pipe(Effect.asVoid, Effect.catch((error) => error.reason._tag === "BatchStale" ? Effect.ignore(attempts.reject(plan.id, { reason: "the batch was declined" })) : Effect.fail(error)))
141
+ })), {
142
+ concurrency: policy.batchConcurrency,
143
+ discard: true
144
+ });
145
+ return yield* batchView(yield* storage.batches.get(id));
146
+ });
147
+ const attachmentsFor = (items) => Effect.forEach(items, (item) => Effect.map(attachmentFor(item.domain), (attachment) => ({
148
+ attachment,
149
+ requirements: item.requirements
150
+ })));
151
+ /**
152
+ * The same resolution, tolerating a domain this owner cannot reach.
153
+ *
154
+ * What a replay uses: the batch it answers with is already stored, so a payload naming a
155
+ * domain that was never attached, or one detached since, plans nothing rather than failing a
156
+ * request the first call already succeeded at.
157
+ */
158
+ const resolvableFor = (items) => Effect.map(Effect.forEach(items, (item) => attachmentFor(item.domain).pipe(Effect.map((attachment) => [{
159
+ attachment,
160
+ requirements: item.requirements
161
+ }]), Effect.catch((error) => error.reason._tag === "NotFound" || error.reason._tag === "InvalidInput" ? Effect.succeed([]) : Effect.fail(error)))), (groups) => groups.flat());
55
162
  return {
56
163
  plan: (input) => Effect.gen(function* () {
57
164
  const policy = yield* Policy;
58
165
  const attachment = yield* attachmentFor(input.domain);
59
- yield* assertWithin(attachment, input.requirements);
60
- const { session, target } = yield* connect.session(attachment.id);
61
- const observed = yield* session.dns(target).list(target.zone);
62
- const operations = yield* reconcile(input.requirements, observed.map(({ record }) => record));
63
- return yield* attempts.record({
64
- attachment,
65
- target,
66
- operations,
67
- ttlMs: policy.planTtlMs,
68
- sourceReceiptId: null
69
- });
166
+ return yield* planFor(attachment, input.requirements, policy.planTtlMs);
70
167
  }),
71
168
  approve: (plan, options) => attempts.approve(plan, options),
72
169
  reject: (plan, options) => Effect.map(attempts.reject(plan, options), view),
@@ -76,7 +173,87 @@ const make = Effect.gen(function* () {
76
173
  const attachment = yield* attachmentFor(domain);
77
174
  const latest = yield* storage.attempts.latest(attachment.id, "provisioning");
78
175
  return Option.isSome(latest) ? view(latest.value) : null;
79
- })
176
+ }),
177
+ batch: {
178
+ create: (input) => Effect.gen(function* () {
179
+ const replay = yield* storage.batches.byIdempotencyKey(input.idempotencyKey);
180
+ if (Option.isSome(replay)) {
181
+ const aggregate = replay.value;
182
+ if (aggregate.batch.kind !== "provisioning") return yield* fail(new NotFound({
183
+ entity: "batch",
184
+ id: aggregate.batch.id
185
+ }));
186
+ return yield* planMissing(aggregate, yield* resolvableFor(input.items));
187
+ }
188
+ const supplied = yield* attachmentsFor(input.items);
189
+ const aggregate = yield* storage.batches.create({
190
+ kind: "provisioning",
191
+ idempotencyKey: input.idempotencyKey,
192
+ attachmentIds: supplied.map(({ attachment }) => attachment.id)
193
+ });
194
+ return yield* planMissing(aggregate, supplied);
195
+ }),
196
+ resumePlanning: (id, input) => Effect.gen(function* () {
197
+ const aggregate = yield* provisioningBatch(id);
198
+ const supplied = yield* attachmentsFor(input.items);
199
+ return yield* planMissing(aggregate, supplied);
200
+ }),
201
+ approve: (id, options) => Effect.gen(function* () {
202
+ const principal = yield* Service$1;
203
+ const aggregate = yield* provisioningBatch(id);
204
+ const current = yield* batchView(aggregate);
205
+ if (current.digest === null || current.digest !== options.digest) return yield* batchStale(aggregate.batch, current.digest);
206
+ if (aggregate.batch.approval !== null) return current;
207
+ const approvals = yield* Effect.forEach(aggregate.items, (item) => Effect.gen(function* () {
208
+ if (item.attemptId === null) return yield* batchStale(aggregate.batch, null);
209
+ const attempt = yield* storage.attempts.get(item.attemptId);
210
+ return {
211
+ attachmentId: item.attachmentId,
212
+ approval: attempt.approval ?? (yield* attempts.draftApproval(attempt))
213
+ };
214
+ }));
215
+ const approved = yield* storage.batches.approve(id, {
216
+ digest: current.digest,
217
+ actorId: principal.actorId,
218
+ approvals
219
+ });
220
+ return yield* batchView(approved);
221
+ }),
222
+ apply: (id) => Effect.gen(function* () {
223
+ const policy = yield* Policy;
224
+ const aggregate = yield* provisioningBatch(id);
225
+ if (aggregate.batch.status === "complete") return yield* batchView(aggregate);
226
+ if (aggregate.batch.approval === null) return yield* batchStale(aggregate.batch, aggregate.batch.digest);
227
+ yield* Effect.forEach(aggregate.items, (item) => Effect.gen(function* () {
228
+ if (item.attemptId === null) return;
229
+ const attempt = yield* storage.attempts.get(item.attemptId);
230
+ if (attempt.receipt !== null || attempt.approval === null) return;
231
+ yield* attempts.apply(attempt.approval.id, policy).pipe(Effect.asVoid, Effect.catch(() => Effect.void));
232
+ }), {
233
+ concurrency: policy.batchConcurrency,
234
+ discard: true
235
+ });
236
+ return yield* batchView(yield* storage.batches.refresh(id));
237
+ }),
238
+ reject: (id, options) => Effect.gen(function* () {
239
+ const principal = yield* Service$1;
240
+ yield* provisioningBatch(id);
241
+ const rejected = yield* storage.batches.reject(id, {
242
+ actorId: principal.actorId,
243
+ reason: options?.reason ?? null
244
+ });
245
+ return yield* batchView(rejected);
246
+ }),
247
+ get: (id) => Effect.flatMap(provisioningBatch(id), batchView),
248
+ list: () => Effect.map(storage.batches.listUnfinished(), (aggregates) => aggregates.filter(({ batch: row }) => row.kind === "provisioning").map(({ batch: row, items }) => ({
249
+ id: row.id,
250
+ status: row.status,
251
+ itemCount: items.length,
252
+ createdAt: row.createdAt,
253
+ updatedAt: row.updatedAt,
254
+ completedAt: row.completedAt
255
+ })))
256
+ }
80
257
  };
81
258
  });
82
259
  const layer = Layer.effect(Service)(make);
@@ -87,7 +264,20 @@ const reject = accessor((service) => service.reject);
87
264
  const apply = accessor((service) => service.apply);
88
265
  const get = accessor((service) => service.get);
89
266
  const latest = accessor((service) => service.latest);
267
+ /**
268
+ * Many domains as one reviewable unit. Every operation needs `Provision.Service` and `Principal`,
269
+ * exactly like the single-domain accessors above.
270
+ */
271
+ const batch = {
272
+ create: accessor((service) => service.batch.create),
273
+ resumePlanning: accessor((service) => service.batch.resumePlanning),
274
+ approve: accessor((service) => service.batch.approve),
275
+ apply: accessor((service) => service.batch.apply),
276
+ reject: accessor((service) => service.batch.reject),
277
+ get: accessor((service) => service.batch.get),
278
+ list: accessor((service) => service.batch.list)
279
+ };
90
280
  //#endregion
91
- export { Policy, Provision_exports, Service, apply, approve, defaults, get, latest, layer, make, plan, reject };
281
+ export { Policy, Provision_exports, Service, apply, approve, batch, defaults, get, latest, layer, make, plan, reject };
92
282
 
93
283
  //# sourceMappingURL=Provision.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"Provision.mjs","names":["Storage.Service","Connect.Service","Attempts.make","DomainName.decode","Errors.fail","Reason.NotFound","Attempts.assertWithin","Planner.reconcile"],"sources":["../src/Provision.ts"],"sourcesContent":["/**\n * plan -> approve -> apply, durable. Every step is a stored attempt, so a host can render the\n * plan in one request, collect consent in another, and apply in a third, and a retry of any step\n * is idempotent.\n */\nimport { Context, Effect, Layer, Option } from \"effect\";\n\nimport type * as Approval from \"./Approval.ts\";\nimport * as Connect from \"./Connect.ts\";\nimport type * as DnsRecord from \"./DnsRecord.ts\";\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as DomainName from \"./DomainName.ts\";\nimport * as Attempts from \"./internal/attempts.ts\";\nimport * as Planner from \"./internal/planner.ts\";\nimport type * as Plan from \"./Plan.ts\";\nimport type * as Principal from \"./Principal.ts\";\nimport type * as Receipt from \"./Receipt.ts\";\nimport * as Storage from \"./Storage.ts\";\n\ntype Fx<A> = Effect.Effect<A, Errors.DomainKitError, Principal.Service>;\n\nexport interface Attempt {\n readonly plan: Plan.Model;\n readonly status: Storage.AttemptStatus;\n readonly approval: Approval.Model | null;\n readonly receipt: Receipt.Model | null;\n readonly rejection: Storage.Rejection | null;\n}\n\nexport interface RejectOptions {\n readonly reason?: string;\n}\n\nexport interface Interface {\n /** Read provider state and build an additive plan. Fails `NotFound` when the domain is not attached. */\n readonly plan: (input: {\n readonly domain: string;\n readonly requirements: ReadonlyArray<DnsRecord.Model>;\n }) => Fx<Plan.Model>;\n /**\n * Record the principal's consent. Fails `Conflict` unless `allowPartial` and the selected\n * operations are conflict-free. Approving an already-approved plan returns the same approval.\n */\n readonly approve: (\n plan: Plan.Model | Plan.PlanId,\n options?: Attempts.ApproveOptions,\n ) => Fx<Approval.Model>;\n /**\n * Decline the plan for the acting principal; terminal. Rejecting again returns the same\n * attempt; a plan that was approved or applied fails `Stale`.\n */\n readonly reject: (plan: Plan.Model | Plan.PlanId, options?: RejectOptions) => Fx<Attempt>;\n /**\n * Re-plan the zone, fail `Stale` if the digest moved, then create records. Partial success is a\n * `partial` receipt. Applying an attempt that already completed returns its receipt.\n */\n readonly apply: (approval: Approval.Model | Approval.ApprovalId) => Fx<Receipt.Model>;\n readonly get: (planId: Plan.PlanId) => Fx<Attempt>;\n readonly latest: (domain: string) => Fx<Attempt | null>;\n}\n\nexport class Service extends Context.Service<Service, Interface>()(\"@domainkit/Provision\") {}\n\nexport interface PolicyShape {\n /** Plan lifetime before it must be rebuilt. Default 1 hour. */\n readonly planTtlMs: number;\n /** Apply lease; a crashed apply can be retried after this. Default 2 minutes. */\n readonly applyLeaseMs: number;\n}\nexport const defaults: PolicyShape = { planTtlMs: 60 * 60_000, applyLeaseMs: 2 * 60_000 };\nexport class Policy extends Context.Reference<PolicyShape>(\"@domainkit/Provision/Policy\", {\n defaultValue: () => defaults,\n}) {}\n\nexport const make: Effect.Effect<Interface, never, Storage.Service | Connect.Service> = Effect.gen(\n function* () {\n const storage = yield* Storage.Service;\n const connect = yield* Connect.Service;\n const attempts = Attempts.make(storage, connect, \"provisioning\");\n\n const attachmentFor = (input: string): Fx<Storage.Attachment> =>\n Effect.gen(function* () {\n const domain = yield* DomainName.decode(input);\n const attachment = yield* storage.attachments.byDomain(domain);\n if (Option.isNone(attachment)) {\n return yield* Errors.fail(new Reason.NotFound({ entity: \"attachment\", id: domain }));\n }\n return attachment.value;\n });\n\n const view = (attempt: Storage.Attempt): Attempt => ({\n plan: attempt.plan,\n status: attempt.status,\n approval: attempt.approval,\n receipt: attempt.receipt,\n rejection: attempt.rejection,\n });\n\n return {\n plan: (input) =>\n Effect.gen(function* () {\n const policy = yield* Policy;\n const attachment = yield* attachmentFor(input.domain);\n yield* Attempts.assertWithin(attachment, input.requirements);\n const { session, target } = yield* connect.session(attachment.id);\n const observed = yield* session.dns(target).list(target.zone);\n const operations = yield* Planner.reconcile(\n input.requirements,\n observed.map(({ record }) => record),\n );\n return yield* attempts.record({\n attachment,\n target,\n operations,\n ttlMs: policy.planTtlMs,\n sourceReceiptId: null,\n });\n }),\n approve: (plan, options) => attempts.approve(plan, options),\n reject: (plan, options) => Effect.map(attempts.reject(plan, options), view),\n apply: (approval) => Effect.flatMap(Policy, (policy) => attempts.apply(approval, policy)),\n get: (planId) => Effect.map(attempts.attemptOf(planId), view),\n latest: (domain) =>\n Effect.gen(function* () {\n const attachment = yield* attachmentFor(domain);\n const latest = yield* storage.attempts.latest(attachment.id, \"provisioning\");\n return Option.isSome(latest) ? view(latest.value) : null;\n }),\n };\n },\n);\n\nexport const layer: Layer.Layer<Service, never, Storage.Service | Connect.Service> =\n Layer.effect(Service)(make);\n\nconst accessor =\n <Args extends ReadonlyArray<unknown>, A>(\n pick: (service: Interface) => (...args: Args) => Fx<A>,\n ): ((...args: Args) => Effect.Effect<A, Errors.DomainKitError, Principal.Service | Service>) =>\n (...args) =>\n Effect.flatMap(Service, (service) => pick(service)(...args));\n\nexport const plan = accessor((service) => service.plan);\nexport const approve = accessor((service) => service.approve);\nexport const reject = accessor((service) => service.reject);\nexport const apply = accessor((service) => service.apply);\nexport const get = accessor((service) => service.get);\nexport const latest = accessor((service) => service.latest);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8DA,IAAa,UAAb,cAA6B,QAAQ,QAA4B,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC;AAQ5F,MAAa,WAAwB;CAAE,WAAW;CAAa,cAAc;AAAW;AACxF,IAAa,SAAb,cAA4B,QAAQ,UAAuB,+BAA+B,EACxF,oBAAoB,SACtB,CAAC,CAAC,CAAC,CAAC;AAEJ,MAAa,OAA2E,OAAO,IAC7F,aAAa;CACX,MAAM,UAAU,OAAOA;CACvB,MAAM,UAAU,OAAOC;CACvB,MAAM,WAAWC,OAAc,SAAS,SAAS,cAAc;CAE/D,MAAM,iBAAiB,UACrB,OAAO,IAAI,aAAa;EACtB,MAAM,SAAS,OAAOC,OAAkB,KAAK;EAC7C,MAAM,aAAa,OAAO,QAAQ,YAAY,SAAS,MAAM;EAC7D,IAAI,OAAO,OAAO,UAAU,GAC1B,OAAO,OAAOC,KAAY,IAAIC,SAAgB;GAAE,QAAQ;GAAc,IAAI;EAAO,CAAC,CAAC;EAErF,OAAO,WAAW;CACpB,CAAC;CAEH,MAAM,QAAQ,aAAuC;EACnD,MAAM,QAAQ;EACd,QAAQ,QAAQ;EAChB,UAAU,QAAQ;EAClB,SAAS,QAAQ;EACjB,WAAW,QAAQ;CACrB;CAEA,OAAO;EACL,OAAO,UACL,OAAO,IAAI,aAAa;GACtB,MAAM,SAAS,OAAO;GACtB,MAAM,aAAa,OAAO,cAAc,MAAM,MAAM;GACpD,OAAOC,aAAsB,YAAY,MAAM,YAAY;GAC3D,MAAM,EAAE,SAAS,WAAW,OAAO,QAAQ,QAAQ,WAAW,EAAE;GAChE,MAAM,WAAW,OAAO,QAAQ,IAAI,MAAM,CAAC,CAAC,KAAK,OAAO,IAAI;GAC5D,MAAM,aAAa,OAAOC,UACxB,MAAM,cACN,SAAS,KAAK,EAAE,aAAa,MAAM,CACrC;GACA,OAAO,OAAO,SAAS,OAAO;IAC5B;IACA;IACA;IACA,OAAO,OAAO;IACd,iBAAiB;GACnB,CAAC;EACH,CAAC;EACH,UAAU,MAAM,YAAY,SAAS,QAAQ,MAAM,OAAO;EAC1D,SAAS,MAAM,YAAY,OAAO,IAAI,SAAS,OAAO,MAAM,OAAO,GAAG,IAAI;EAC1E,QAAQ,aAAa,OAAO,QAAQ,SAAS,WAAW,SAAS,MAAM,UAAU,MAAM,CAAC;EACxF,MAAM,WAAW,OAAO,IAAI,SAAS,UAAU,MAAM,GAAG,IAAI;EAC5D,SAAS,WACP,OAAO,IAAI,aAAa;GACtB,MAAM,aAAa,OAAO,cAAc,MAAM;GAC9C,MAAM,SAAS,OAAO,QAAQ,SAAS,OAAO,WAAW,IAAI,cAAc;GAC3E,OAAO,OAAO,OAAO,MAAM,IAAI,KAAK,OAAO,KAAK,IAAI;EACtD,CAAC;CACL;AACF,CACF;AAEA,MAAa,QACX,MAAM,OAAO,OAAO,CAAC,CAAC,IAAI;AAE5B,MAAM,YAEF,UAED,GAAG,SACF,OAAO,QAAQ,UAAU,YAAY,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;AAE/D,MAAa,OAAO,UAAU,YAAY,QAAQ,IAAI;AACtD,MAAa,UAAU,UAAU,YAAY,QAAQ,OAAO;AAC5D,MAAa,SAAS,UAAU,YAAY,QAAQ,MAAM;AAC1D,MAAa,QAAQ,UAAU,YAAY,QAAQ,KAAK;AACxD,MAAa,MAAM,UAAU,YAAY,QAAQ,GAAG;AACpD,MAAa,SAAS,UAAU,YAAY,QAAQ,MAAM"}
1
+ {"version":3,"file":"Provision.mjs","names":["Storage.Service","Connect.Service","Attempts.make","DomainName.decode","Errors.fail","Reason.NotFound","Attempts.assertWithin","Planner.reconcile","Reason.BatchStale","Principal.Service"],"sources":["../src/Provision.ts"],"sourcesContent":["/**\n * plan -> approve -> apply, durable. Every step is a stored attempt, so a host can render the\n * plan in one request, collect consent in another, and apply in a third, and a retry of any step\n * is idempotent.\n */\nimport { Context, type DateTime, Effect, Layer, Option } from \"effect\";\n\nimport type * as Approval from \"./Approval.ts\";\nimport * as Connect from \"./Connect.ts\";\nimport type * as DnsRecord from \"./DnsRecord.ts\";\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as DomainName from \"./DomainName.ts\";\nimport * as Attempts from \"./internal/attempts.ts\";\nimport { sha256Hex } from \"./internal/digest.ts\";\nimport * as Planner from \"./internal/planner.ts\";\nimport * as Plan from \"./Plan.ts\";\nimport * as Principal from \"./Principal.ts\";\nimport type * as Receipt from \"./Receipt.ts\";\nimport * as Storage from \"./Storage.ts\";\n\ntype Fx<A> = Effect.Effect<A, Errors.DomainKitError, Principal.Service>;\n\nexport interface Attempt {\n readonly plan: Plan.Model;\n readonly status: Storage.AttemptStatus;\n readonly approval: Approval.Model | null;\n readonly receipt: Receipt.Model | null;\n readonly rejection: Storage.Rejection | null;\n}\n\nexport interface RejectOptions {\n readonly reason?: string;\n}\n\n/** One domain to plan inside a batch. The domain must already be attached. */\nexport interface BatchInput {\n readonly domain: string;\n readonly requirements: ReadonlyArray<DnsRecord.Model>;\n}\n\n/**\n * One domain's place in a batch, with whatever its attempt holds.\n *\n * Everything but `planFailure` is read from the attempt the item points at, so a batch never\n * shows a customer a plan, receipt, or failure that disagrees with the attempt it came from.\n */\nexport interface BatchItem {\n readonly attachmentId: string;\n readonly position: number;\n /** Null while the item has no plan; `planFailure` then says why. */\n readonly plan: Plan.Model | null;\n readonly status: Storage.AttemptStatus | null;\n readonly approval: Approval.Model | null;\n readonly receipt: Receipt.Model | null;\n readonly rejection: Storage.Rejection | null;\n /** Why the last apply stopped before any write. */\n readonly failure: string | null;\n /** Why the last planning pass could not build this item's plan. */\n readonly planFailure: string | null;\n}\n\n/** Many domains planned together, approved once, and applied with bounded concurrency. */\nexport interface Batch {\n readonly id: Storage.BatchId;\n readonly status: Storage.BatchStatus;\n /**\n * What `approve` binds: SHA-256 over the items' sorted `attachmentId:planDigest` pairs. The\n * digest the batch was approved at once it is approved, the digest its current plans produce\n * before that, and null while any item is still unplanned.\n */\n readonly digest: Plan.Digest | null;\n readonly approval: Storage.BatchApproval | null;\n readonly rejection: Storage.Rejection | null;\n readonly createdAt: DateTime.Utc;\n readonly updatedAt: DateTime.Utc;\n readonly completedAt: DateTime.Utc | null;\n readonly items: ReadonlyArray<BatchItem>;\n}\n\n/** A batch without its plans, for the index a host renders as a banner. */\nexport interface BatchSummary {\n readonly id: Storage.BatchId;\n readonly status: Storage.BatchStatus;\n readonly itemCount: number;\n readonly createdAt: DateTime.Utc;\n readonly updatedAt: DateTime.Utc;\n readonly completedAt: DateTime.Utc | null;\n}\n\nexport interface BatchInterface {\n /**\n * Plan every domain in `items` as one batch, with `Policy.batchConcurrency` in flight.\n *\n * `idempotencyKey` is unique per owner: a retried create returns the batch the first call made,\n * plans whatever is still unplanned, and ignores items that batch does not hold. An item whose\n * plan fails records the reason and leaves the batch `planning` for `resumePlanning`.\n */\n readonly create: (input: {\n readonly items: ReadonlyArray<BatchInput>;\n readonly idempotencyKey: string;\n }) => Fx<Batch>;\n /**\n * Plan the items that still have none, with the requirements supplied again. A batch stores\n * pointers, not requirements, so the host owns them across the retry.\n */\n readonly resumePlanning: (\n id: Storage.BatchId,\n input: { readonly items: ReadonlyArray<BatchInput> },\n ) => Fx<Batch>;\n /**\n * Record the principal's consent to the whole batch. Fails `BatchStale` unless `digest` is the\n * one the batch's current plans produce. Approving an approved batch at its digest returns it.\n */\n readonly approve: (id: Storage.BatchId, options: { readonly digest: Plan.Digest }) => Fx<Batch>;\n /**\n * Apply every approved item that has no receipt yet, with `Policy.batchConcurrency` in flight\n * and each item under its own attempt lease. An item another apply holds is skipped this round,\n * and an item that fails records its failure on its attempt rather than stopping the others, so\n * calling `apply` again resumes the batch. Applying a complete batch returns it unchanged.\n */\n readonly apply: (id: Storage.BatchId) => Fx<Batch>;\n /**\n * Decline the batch and every plan under it; terminal. Rejecting again returns the same batch;\n * a batch that was approved fails `BatchStale`.\n */\n readonly reject: (id: Storage.BatchId, options?: RejectOptions) => Fx<Batch>;\n readonly get: (id: Storage.BatchId) => Fx<Batch>;\n /** Every batch this owner still owes a move on, most recently touched first. */\n readonly list: (filter: { readonly unfinished: true }) => Fx<ReadonlyArray<BatchSummary>>;\n}\n\nexport interface Interface {\n /** Read provider state and build an additive plan. Fails `NotFound` when the domain is not attached. */\n readonly plan: (input: {\n readonly domain: string;\n readonly requirements: ReadonlyArray<DnsRecord.Model>;\n }) => Fx<Plan.Model>;\n /**\n * Record the principal's consent. Fails `Conflict` unless `allowPartial` and the selected\n * operations are conflict-free. Approving an already-approved plan returns the same approval.\n */\n readonly approve: (\n plan: Plan.Model | Plan.PlanId,\n options?: Attempts.ApproveOptions,\n ) => Fx<Approval.Model>;\n /**\n * Decline the plan for the acting principal; terminal. Rejecting again returns the same\n * attempt; a plan that was approved or applied fails `Stale`.\n */\n readonly reject: (plan: Plan.Model | Plan.PlanId, options?: RejectOptions) => Fx<Attempt>;\n /**\n * Re-plan the zone, fail `Stale` if the digest moved, then create records. Partial success is a\n * `partial` receipt. Applying an attempt that already completed returns its receipt.\n */\n readonly apply: (approval: Approval.Model | Approval.ApprovalId) => Fx<Receipt.Model>;\n readonly get: (planId: Plan.PlanId) => Fx<Attempt>;\n readonly latest: (domain: string) => Fx<Attempt | null>;\n /** Many domains as one reviewable unit: plan together, approve once, apply bounded. */\n readonly batch: BatchInterface;\n}\n\nexport class Service extends Context.Service<Service, Interface>()(\"@domainkit/Provision\") {}\n\nexport interface PolicyShape {\n /** Plan lifetime before it must be rebuilt. Default 1 hour. */\n readonly planTtlMs: number;\n /** Apply lease; a crashed apply can be retried after this. Default 2 minutes. */\n readonly applyLeaseMs: number;\n /** How many of a batch's domains are planned or applied at once. Default 4. */\n readonly batchConcurrency: number;\n}\nexport const defaults: PolicyShape = {\n planTtlMs: 60 * 60_000,\n applyLeaseMs: 2 * 60_000,\n batchConcurrency: 4,\n};\nexport class Policy extends Context.Reference<PolicyShape>(\"@domainkit/Provision/Policy\", {\n defaultValue: () => defaults,\n}) {}\n\nexport const make: Effect.Effect<Interface, never, Storage.Service | Connect.Service> = Effect.gen(\n function* () {\n const storage = yield* Storage.Service;\n const connect = yield* Connect.Service;\n const attempts = Attempts.make(storage, connect, \"provisioning\");\n\n const attachmentFor = (input: string): Fx<Storage.Attachment> =>\n Effect.gen(function* () {\n const domain = yield* DomainName.decode(input);\n const attachment = yield* storage.attachments.byDomain(domain);\n if (Option.isNone(attachment)) {\n return yield* Errors.fail(new Reason.NotFound({ entity: \"attachment\", id: domain }));\n }\n return attachment.value;\n });\n\n const view = (attempt: Storage.Attempt): Attempt => ({\n plan: attempt.plan,\n status: attempt.status,\n approval: attempt.approval,\n receipt: attempt.receipt,\n rejection: attempt.rejection,\n });\n\n /** Read the attached zone and record one `planned` attempt for it. */\n const planFor = (\n attachment: Storage.Attachment,\n requirements: ReadonlyArray<DnsRecord.Model>,\n ttlMs: number,\n ): Fx<Plan.Model> =>\n Effect.gen(function* () {\n yield* Attempts.assertWithin(attachment, requirements);\n const { session, target } = yield* connect.session(attachment.id);\n const observed = yield* session.dns(target).list(target.zone);\n const operations = yield* Planner.reconcile(\n requirements,\n observed.map(({ record }) => record),\n );\n return yield* attempts.record({\n attachment,\n target,\n operations,\n ttlMs,\n sourceReceiptId: null,\n });\n });\n\n const batchStale = (batch: Storage.Batch, digest: Plan.Digest | null) =>\n Errors.fail(new Reason.BatchStale({ batchId: batch.id, status: batch.status, digest }));\n\n /** Provision reads provisioning batches; a cleanup batch is not its aggregate. */\n const provisioningBatch = (id: Storage.BatchId): Fx<Storage.BatchAggregate> =>\n Effect.flatMap(storage.batches.get(id), (aggregate) =>\n aggregate.batch.kind === \"provisioning\"\n ? Effect.succeed(aggregate)\n : Errors.fail(new Reason.NotFound({ entity: \"batch\", id })),\n );\n\n /**\n * The digest the principal reviews: SHA-256 over the items' sorted `attachmentId:planDigest`\n * pairs, and null while any item is still unplanned.\n */\n const digestOf = (items: ReadonlyArray<BatchItem>): Fx<Plan.Digest | null> =>\n items.some((item) => item.plan === null)\n ? Effect.succeed(null)\n : Effect.map(\n sha256Hex(\n items\n .map((item) => `${item.attachmentId}:${item.plan?.digest ?? \"\"}`)\n .sort()\n .join(\"\\n\"),\n ),\n Plan.Digest.make,\n );\n\n const batchView = (aggregate: Storage.BatchAggregate): Fx<Batch> =>\n Effect.gen(function* () {\n const policy = yield* Policy;\n const items = yield* Effect.forEach(\n aggregate.items,\n (item): Fx<BatchItem> =>\n item.attemptId === null\n ? Effect.succeed({\n attachmentId: item.attachmentId,\n position: item.position,\n plan: null,\n status: null,\n approval: null,\n receipt: null,\n rejection: null,\n failure: null,\n planFailure: item.planFailure,\n })\n : Effect.map(storage.attempts.get(item.attemptId), (attempt) => ({\n attachmentId: item.attachmentId,\n position: item.position,\n plan: attempt.plan,\n status: attempt.status,\n approval: attempt.approval,\n receipt: attempt.receipt,\n rejection: attempt.rejection,\n failure: attempt.failure,\n planFailure: item.planFailure,\n })),\n { concurrency: policy.batchConcurrency },\n );\n return {\n id: aggregate.batch.id,\n status: aggregate.batch.status,\n digest: aggregate.batch.digest ?? (yield* digestOf(items)),\n approval: aggregate.batch.approval,\n rejection: aggregate.batch.rejection,\n createdAt: aggregate.batch.createdAt,\n updatedAt: aggregate.batch.updatedAt,\n completedAt: aggregate.batch.completedAt,\n items,\n };\n });\n\n /** The batch was declined under a planning pass still in flight; the fence did its work. */\n const ignoreFence = <A>(\n effect: Fx<A>,\n ): Effect.Effect<void, Errors.DomainKitError, Principal.Service> =>\n effect.pipe(\n Effect.asVoid,\n Effect.catch((error) =>\n error.reason._tag === \"BatchStale\" ? Effect.void : Effect.fail(error),\n ),\n );\n\n /**\n * Plan every item that has no plan yet and for which this call supplied requirements. An item\n * the caller did not name keeps whatever it holds, so a replayed create never re-plans.\n */\n const planMissing = (\n aggregate: Storage.BatchAggregate,\n supplied: ReadonlyArray<{\n readonly attachment: Storage.Attachment;\n readonly requirements: ReadonlyArray<DnsRecord.Model>;\n }>,\n ): Fx<Batch> =>\n Effect.gen(function* () {\n const policy = yield* Policy;\n const id = aggregate.batch.id;\n const pending = aggregate.items.flatMap((item) => {\n if (item.attemptId !== null) return [];\n const match = supplied.find(({ attachment }) => attachment.id === item.attachmentId);\n return match === undefined ? [] : [match];\n });\n yield* Effect.forEach(\n pending,\n ({ attachment, requirements }) =>\n planFor(attachment, requirements, policy.planTtlMs).pipe(\n Effect.matchEffect({\n // A domain that cannot be planned is the batch's news to carry, not a reason to\n // abandon the domains beside it.\n onFailure: (error) =>\n ignoreFence(\n storage.batches.recordItemPlanFailure(id, attachment.id, error.message),\n ),\n onSuccess: (plan) =>\n storage.batches.recordItemPlan(id, attachment.id, plan.id).pipe(\n Effect.asVoid,\n Effect.catch((error) =>\n error.reason._tag === \"BatchStale\"\n ? // The batch was declined while this plan was being built. The fence kept\n // the plan off the aggregate; closing the attempt keeps it out of the\n // single-domain API too, which would otherwise approve and apply it.\n Effect.ignore(\n attempts.reject(plan.id, { reason: \"the batch was declined\" }),\n )\n : Effect.fail(error),\n ),\n ),\n }),\n ),\n { concurrency: policy.batchConcurrency, discard: true },\n );\n return yield* batchView(yield* storage.batches.get(id));\n });\n\n const attachmentsFor = (items: ReadonlyArray<BatchInput>) =>\n Effect.forEach(items, (item) =>\n Effect.map(attachmentFor(item.domain), (attachment) => ({\n attachment,\n requirements: item.requirements,\n })),\n );\n\n /**\n * The same resolution, tolerating a domain this owner cannot reach.\n *\n * What a replay uses: the batch it answers with is already stored, so a payload naming a\n * domain that was never attached, or one detached since, plans nothing rather than failing a\n * request the first call already succeeded at.\n */\n const resolvableFor = (items: ReadonlyArray<BatchInput>) =>\n Effect.map(\n Effect.forEach(items, (item) =>\n attachmentFor(item.domain).pipe(\n Effect.map((attachment) => [{ attachment, requirements: item.requirements }]),\n Effect.catch((error) =>\n error.reason._tag === \"NotFound\" || error.reason._tag === \"InvalidInput\"\n ? Effect.succeed(\n [] as ReadonlyArray<{\n readonly attachment: Storage.Attachment;\n readonly requirements: ReadonlyArray<DnsRecord.Model>;\n }>,\n )\n : Effect.fail(error),\n ),\n ),\n ),\n (groups) => groups.flat(),\n );\n\n const batch: BatchInterface = {\n create: (input) =>\n Effect.gen(function* () {\n // The key decides first: a retried request answers with the batch the first one made,\n // whatever this payload now says, so a domain detached in between cannot fail a create\n // that already succeeded.\n const replay = yield* storage.batches.byIdempotencyKey(input.idempotencyKey);\n if (Option.isSome(replay)) {\n const aggregate = replay.value;\n if (aggregate.batch.kind !== \"provisioning\") {\n return yield* Errors.fail(\n new Reason.NotFound({ entity: \"batch\", id: aggregate.batch.id }),\n );\n }\n return yield* planMissing(aggregate, yield* resolvableFor(input.items));\n }\n const supplied = yield* attachmentsFor(input.items);\n const aggregate = yield* storage.batches.create({\n kind: \"provisioning\",\n idempotencyKey: input.idempotencyKey,\n attachmentIds: supplied.map(({ attachment }) => attachment.id),\n });\n return yield* planMissing(aggregate, supplied);\n }),\n resumePlanning: (id, input) =>\n Effect.gen(function* () {\n const aggregate = yield* provisioningBatch(id);\n const supplied = yield* attachmentsFor(input.items);\n return yield* planMissing(aggregate, supplied);\n }),\n approve: (id, options) =>\n Effect.gen(function* () {\n const principal = yield* Principal.Service;\n const aggregate = yield* provisioningBatch(id);\n const current = yield* batchView(aggregate);\n // Consent is bound to the digest the principal read, so nothing is written until the\n // batch's current plans still produce it.\n if (current.digest === null || current.digest !== options.digest) {\n return yield* batchStale(aggregate.batch, current.digest);\n }\n if (aggregate.batch.approval !== null) return current;\n const approvals = yield* Effect.forEach(aggregate.items, (item) =>\n Effect.gen(function* () {\n if (item.attemptId === null) return yield* batchStale(aggregate.batch, null);\n const attempt = yield* storage.attempts.get(item.attemptId);\n return {\n attachmentId: item.attachmentId,\n approval: attempt.approval ?? (yield* attempts.draftApproval(attempt)),\n };\n }),\n );\n // One transaction: the batch is never approved without the per-attempt approvals\n // `apply` takes.\n const approved = yield* storage.batches.approve(id, {\n digest: current.digest,\n actorId: principal.actorId,\n approvals,\n });\n return yield* batchView(approved);\n }),\n apply: (id) =>\n Effect.gen(function* () {\n const policy = yield* Policy;\n const aggregate = yield* provisioningBatch(id);\n if (aggregate.batch.status === \"complete\") return yield* batchView(aggregate);\n if (aggregate.batch.approval === null) {\n return yield* batchStale(aggregate.batch, aggregate.batch.digest);\n }\n yield* Effect.forEach(\n aggregate.items,\n (item) =>\n Effect.gen(function* () {\n if (item.attemptId === null) return;\n const attempt = yield* storage.attempts.get(item.attemptId);\n if (attempt.receipt !== null || attempt.approval === null) return;\n // The attempt lease is the item lock: an item another apply holds is `Busy` and\n // waits for the next round, and one that fails records that on its own attempt.\n yield* attempts.apply(attempt.approval.id, policy).pipe(\n Effect.asVoid,\n Effect.catch(() => Effect.void),\n );\n }),\n { concurrency: policy.batchConcurrency, discard: true },\n );\n return yield* batchView(yield* storage.batches.refresh(id));\n }),\n reject: (id, options) =>\n Effect.gen(function* () {\n const principal = yield* Principal.Service;\n yield* provisioningBatch(id);\n const rejected = yield* storage.batches.reject(id, {\n actorId: principal.actorId,\n reason: options?.reason ?? null,\n });\n return yield* batchView(rejected);\n }),\n get: (id) => Effect.flatMap(provisioningBatch(id), batchView),\n list: () =>\n Effect.map(storage.batches.listUnfinished(), (aggregates) =>\n aggregates\n .filter(({ batch: row }) => row.kind === \"provisioning\")\n .map(({ batch: row, items }) => ({\n id: row.id,\n status: row.status,\n itemCount: items.length,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n completedAt: row.completedAt,\n })),\n ),\n };\n\n return {\n plan: (input) =>\n Effect.gen(function* () {\n const policy = yield* Policy;\n const attachment = yield* attachmentFor(input.domain);\n return yield* planFor(attachment, input.requirements, policy.planTtlMs);\n }),\n approve: (plan, options) => attempts.approve(plan, options),\n reject: (plan, options) => Effect.map(attempts.reject(plan, options), view),\n apply: (approval) => Effect.flatMap(Policy, (policy) => attempts.apply(approval, policy)),\n get: (planId) => Effect.map(attempts.attemptOf(planId), view),\n latest: (domain) =>\n Effect.gen(function* () {\n const attachment = yield* attachmentFor(domain);\n const latest = yield* storage.attempts.latest(attachment.id, \"provisioning\");\n return Option.isSome(latest) ? view(latest.value) : null;\n }),\n batch,\n };\n },\n);\n\nexport const layer: Layer.Layer<Service, never, Storage.Service | Connect.Service> =\n Layer.effect(Service)(make);\n\nconst accessor =\n <Args extends ReadonlyArray<unknown>, A>(\n pick: (service: Interface) => (...args: Args) => Fx<A>,\n ): ((...args: Args) => Effect.Effect<A, Errors.DomainKitError, Principal.Service | Service>) =>\n (...args) =>\n Effect.flatMap(Service, (service) => pick(service)(...args));\n\nexport const plan = accessor((service) => service.plan);\nexport const approve = accessor((service) => service.approve);\nexport const reject = accessor((service) => service.reject);\nexport const apply = accessor((service) => service.apply);\nexport const get = accessor((service) => service.get);\nexport const latest = accessor((service) => service.latest);\n\n/**\n * Many domains as one reviewable unit. Every operation needs `Provision.Service` and `Principal`,\n * exactly like the single-domain accessors above.\n */\nexport const batch = {\n create: accessor((service) => service.batch.create),\n resumePlanning: accessor((service) => service.batch.resumePlanning),\n approve: accessor((service) => service.batch.approve),\n apply: accessor((service) => service.batch.apply),\n reject: accessor((service) => service.batch.reject),\n get: accessor((service) => service.batch.get),\n list: accessor((service) => service.batch.list),\n};\n"],"mappings":"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"}
package/dist/Reason.mjs CHANGED
@@ -7,6 +7,7 @@ import { Schema } from "effect";
7
7
  * match on `reason._tag` and add a reason here rather than a new error class.
8
8
  */
9
9
  var Reason_exports = /* @__PURE__ */ __exportAll({
10
+ BatchStale: () => BatchStale,
10
11
  Busy: () => Busy,
11
12
  Conflict: () => Conflict,
12
13
  CryptoFailed: () => CryptoFailed,
@@ -45,7 +46,8 @@ var NotFound = class extends Schema.TaggedError("@domainkit/Reason/NotFound")("N
45
46
  "provider",
46
47
  "zone",
47
48
  "authorization",
48
- "record"
49
+ "record",
50
+ "batch"
49
51
  ]),
50
52
  id: Schema.String
51
53
  }) {
@@ -71,6 +73,23 @@ var Stale = class extends Schema.TaggedError("@domainkit/Reason/Stale")("Stale",
71
73
  return `plan ${this.planId} is stale; provider state no longer matches digest ${this.digest}`;
72
74
  }
73
75
  };
76
+ /**
77
+ * The batch moved since it was read: it was declined, it is already approved, or the digest the
78
+ * principal reviewed is no longer the one its plans produce. Read the batch again.
79
+ *
80
+ * The aggregate's own refusal, beside `Stale`: a batch has no plan and no plan digest of its own,
81
+ * so nothing here can name one. `digest` is the batch's current digest, null while its items are
82
+ * still being planned.
83
+ */
84
+ var BatchStale = class extends Schema.TaggedError("@domainkit/Reason/BatchStale")("BatchStale", {
85
+ batchId: Schema.String,
86
+ status: Schema.String,
87
+ digest: Schema.NullOr(Schema.String)
88
+ }) {
89
+ get message() {
90
+ return `batch ${this.batchId} is ${this.status}; its current digest is ${this.digest ?? "not yet known"}`;
91
+ }
92
+ };
74
93
  var Expired = class extends Schema.TaggedError("@domainkit/Reason/Expired")("Expired", {
75
94
  entity: Schema.Literals([
76
95
  "plan",
@@ -145,6 +164,7 @@ const Model = Schema.Union([
145
164
  NotFound,
146
165
  Conflict,
147
166
  Stale,
167
+ BatchStale,
148
168
  Expired,
149
169
  Busy,
150
170
  ProviderRejected,
@@ -157,6 +177,6 @@ const Model = Schema.Union([
157
177
  ResolverFailed
158
178
  ]);
159
179
  //#endregion
160
- export { Busy, Conflict, CryptoFailed, Expired, Forbidden, InvalidInput, Model, NotFound, ProviderConflict, ProviderRejected, ProviderUnavailable, Reason_exports, Reconnect, ResolverFailed, Stale, StorageFailed, Unauthenticated, Unsupported };
180
+ export { BatchStale, Busy, Conflict, CryptoFailed, Expired, Forbidden, InvalidInput, Model, NotFound, ProviderConflict, ProviderRejected, ProviderUnavailable, Reason_exports, Reconnect, ResolverFailed, Stale, StorageFailed, Unauthenticated, Unsupported };
161
181
 
162
182
  //# sourceMappingURL=Reason.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"Reason.mjs","names":["Plan.PlanId","Plan.Digest","Plan.Conflict"],"sources":["../src/Reason.ts"],"sourcesContent":["/**\n * Why an operation failed. Every `DomainKit.Error` carries exactly one of these as `reason`; hosts\n * match on `reason._tag` and add a reason here rather than a new error class.\n */\nimport { Schema } from \"effect\";\n\nimport * as Plan from \"./Plan.ts\";\n\n// Plan imports `internal/error.ts`, which imports this module; read Plan lazily so evaluation\n// order inside the cycle does not matter.\nconst PlanId = Schema.suspend(() => Plan.PlanId);\nconst Digest = Schema.suspend(() => Plan.Digest);\nconst Conflicts = Schema.suspend(() => Schema.Array(Plan.Conflict));\n\nexport class InvalidInput extends Schema.TaggedError<InvalidInput>(\n \"@domainkit/Reason/InvalidInput\",\n)(\"InvalidInput\", { message: Schema.String, field: Schema.optionalKey(Schema.String) }) {}\nexport class Unauthenticated extends Schema.TaggedError<Unauthenticated>(\n \"@domainkit/Reason/Unauthenticated\",\n)(\"Unauthenticated\", { message: Schema.String }) {}\nexport class Forbidden extends Schema.TaggedError<Forbidden>(\"@domainkit/Reason/Forbidden\")(\n \"Forbidden\",\n { message: Schema.String },\n) {}\nexport class NotFound extends Schema.TaggedError<NotFound>(\"@domainkit/Reason/NotFound\")(\n \"NotFound\",\n {\n entity: Schema.Literals([\n \"connection\",\n \"attachment\",\n \"plan\",\n \"approval\",\n \"receipt\",\n \"continuation\",\n \"provider\",\n \"zone\",\n \"authorization\",\n \"record\",\n ]),\n id: Schema.String,\n },\n) {\n override get message(): string {\n return `${this.entity} ${this.id} was not found`;\n }\n}\n/** The plan contains conflicts and cannot be applied. */\nexport class Conflict extends Schema.TaggedError<Conflict>(\"@domainkit/Reason/Conflict\")(\n \"Conflict\",\n {\n planId: PlanId,\n operations: Conflicts,\n },\n) {\n override get message(): string {\n return `plan ${this.planId} has ${this.operations.length} conflicting operation(s)`;\n }\n}\n/** Provider state moved since the plan was built; build a new plan. */\nexport class Stale extends Schema.TaggedError<Stale>(\"@domainkit/Reason/Stale\")(\"Stale\", {\n planId: PlanId,\n digest: Digest,\n}) {\n override get message(): string {\n return `plan ${this.planId} is stale; provider state no longer matches digest ${this.digest}`;\n }\n}\nexport class Expired extends Schema.TaggedError<Expired>(\"@domainkit/Reason/Expired\")(\"Expired\", {\n entity: Schema.Literals([\"plan\", \"approval\", \"continuation\", \"credential\"]),\n id: Schema.String,\n}) {\n override get message(): string {\n return `${this.entity} ${this.id} has expired`;\n }\n}\n/** Another operation holds the lock (e.g. concurrent apply or refresh). Retrying is safe. */\nexport class Busy extends Schema.TaggedError<Busy>(\"@domainkit/Reason/Busy\")(\"Busy\", {\n key: Schema.String,\n}) {\n override get message(): string {\n return `${this.key} is busy`;\n }\n}\nexport class ProviderRejected extends Schema.TaggedError<ProviderRejected>(\n \"@domainkit/Reason/ProviderRejected\",\n)(\"ProviderRejected\", {\n provider: Schema.String,\n code: Schema.optionalKey(Schema.String),\n message: Schema.String,\n}) {}\nexport class ProviderUnavailable extends Schema.TaggedError<ProviderUnavailable>(\n \"@domainkit/Reason/ProviderUnavailable\",\n)(\"ProviderUnavailable\", {\n provider: Schema.String,\n retryAfterMs: Schema.optionalKey(Schema.Number),\n message: Schema.String,\n}) {}\n/** The provider credential no longer works; the customer must reconnect. */\nexport class Reconnect extends Schema.TaggedError<Reconnect>(\"@domainkit/Reason/Reconnect\")(\n \"Reconnect\",\n { provider: Schema.String, connectionId: Schema.String },\n) {\n override get message(): string {\n return `${this.provider} connection ${this.connectionId} must be reconnected`;\n }\n}\nexport class StorageFailed extends Schema.TaggedError<StorageFailed>(\n \"@domainkit/Reason/StorageFailed\",\n)(\"StorageFailed\", { operation: Schema.String, message: Schema.String }) {}\nexport class CryptoFailed extends Schema.TaggedError<CryptoFailed>(\n \"@domainkit/Reason/CryptoFailed\",\n)(\"CryptoFailed\", { operation: Schema.Literals([\"digest\", \"seal\", \"open\"]) }) {\n override get message(): string {\n return `crypto ${this.operation} failed`;\n }\n}\nexport class ResolverFailed extends Schema.TaggedError<ResolverFailed>(\n \"@domainkit/Reason/ResolverFailed\",\n)(\"ResolverFailed\", { resolver: Schema.String, message: Schema.String }) {}\n\n/** The provider refused the write because a conflicting record already exists there. */\nexport class ProviderConflict extends Schema.TaggedError<ProviderConflict>(\n \"@domainkit/Reason/ProviderConflict\",\n)(\"ProviderConflict\", {\n provider: Schema.String,\n code: Schema.optionalKey(Schema.String),\n message: Schema.String,\n}) {}\n/** The provider, or this target within it, cannot do the requested operation. */\nexport class Unsupported extends Schema.TaggedError<Unsupported>(\"@domainkit/Reason/Unsupported\")(\n \"Unsupported\",\n { provider: Schema.String, operation: Schema.String, message: Schema.String },\n) {}\n\nexport const Model = Schema.Union([\n InvalidInput,\n Unauthenticated,\n Forbidden,\n NotFound,\n Conflict,\n Stale,\n Expired,\n Busy,\n ProviderRejected,\n ProviderUnavailable,\n ProviderConflict,\n Unsupported,\n Reconnect,\n StorageFailed,\n CryptoFailed,\n ResolverFailed,\n]);\nexport type Model = typeof Model.Type;\n"],"mappings":"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"}
1
+ {"version":3,"file":"Reason.mjs","names":["Plan.PlanId","Plan.Digest","Plan.Conflict"],"sources":["../src/Reason.ts"],"sourcesContent":["/**\n * Why an operation failed. Every `DomainKit.Error` carries exactly one of these as `reason`; hosts\n * match on `reason._tag` and add a reason here rather than a new error class.\n */\nimport { Schema } from \"effect\";\n\nimport * as Plan from \"./Plan.ts\";\n\n// Plan imports `internal/error.ts`, which imports this module; read Plan lazily so evaluation\n// order inside the cycle does not matter.\nconst PlanId = Schema.suspend(() => Plan.PlanId);\nconst Digest = Schema.suspend(() => Plan.Digest);\nconst Conflicts = Schema.suspend(() => Schema.Array(Plan.Conflict));\n\nexport class InvalidInput extends Schema.TaggedError<InvalidInput>(\n \"@domainkit/Reason/InvalidInput\",\n)(\"InvalidInput\", { message: Schema.String, field: Schema.optionalKey(Schema.String) }) {}\nexport class Unauthenticated extends Schema.TaggedError<Unauthenticated>(\n \"@domainkit/Reason/Unauthenticated\",\n)(\"Unauthenticated\", { message: Schema.String }) {}\nexport class Forbidden extends Schema.TaggedError<Forbidden>(\"@domainkit/Reason/Forbidden\")(\n \"Forbidden\",\n { message: Schema.String },\n) {}\nexport class NotFound extends Schema.TaggedError<NotFound>(\"@domainkit/Reason/NotFound\")(\n \"NotFound\",\n {\n entity: Schema.Literals([\n \"connection\",\n \"attachment\",\n \"plan\",\n \"approval\",\n \"receipt\",\n \"continuation\",\n \"provider\",\n \"zone\",\n \"authorization\",\n \"record\",\n \"batch\",\n ]),\n id: Schema.String,\n },\n) {\n override get message(): string {\n return `${this.entity} ${this.id} was not found`;\n }\n}\n/** The plan contains conflicts and cannot be applied. */\nexport class Conflict extends Schema.TaggedError<Conflict>(\"@domainkit/Reason/Conflict\")(\n \"Conflict\",\n {\n planId: PlanId,\n operations: Conflicts,\n },\n) {\n override get message(): string {\n return `plan ${this.planId} has ${this.operations.length} conflicting operation(s)`;\n }\n}\n/** Provider state moved since the plan was built; build a new plan. */\nexport class Stale extends Schema.TaggedError<Stale>(\"@domainkit/Reason/Stale\")(\"Stale\", {\n planId: PlanId,\n digest: Digest,\n}) {\n override get message(): string {\n return `plan ${this.planId} is stale; provider state no longer matches digest ${this.digest}`;\n }\n}\n/**\n * The batch moved since it was read: it was declined, it is already approved, or the digest the\n * principal reviewed is no longer the one its plans produce. Read the batch again.\n *\n * The aggregate's own refusal, beside `Stale`: a batch has no plan and no plan digest of its own,\n * so nothing here can name one. `digest` is the batch's current digest, null while its items are\n * still being planned.\n */\nexport class BatchStale extends Schema.TaggedError<BatchStale>(\"@domainkit/Reason/BatchStale\")(\n \"BatchStale\",\n { batchId: Schema.String, status: Schema.String, digest: Schema.NullOr(Schema.String) },\n) {\n override get message(): string {\n return `batch ${this.batchId} is ${this.status}; its current digest is ${this.digest ?? \"not yet known\"}`;\n }\n}\nexport class Expired extends Schema.TaggedError<Expired>(\"@domainkit/Reason/Expired\")(\"Expired\", {\n entity: Schema.Literals([\"plan\", \"approval\", \"continuation\", \"credential\"]),\n id: Schema.String,\n}) {\n override get message(): string {\n return `${this.entity} ${this.id} has expired`;\n }\n}\n/** Another operation holds the lock (e.g. concurrent apply or refresh). Retrying is safe. */\nexport class Busy extends Schema.TaggedError<Busy>(\"@domainkit/Reason/Busy\")(\"Busy\", {\n key: Schema.String,\n}) {\n override get message(): string {\n return `${this.key} is busy`;\n }\n}\nexport class ProviderRejected extends Schema.TaggedError<ProviderRejected>(\n \"@domainkit/Reason/ProviderRejected\",\n)(\"ProviderRejected\", {\n provider: Schema.String,\n code: Schema.optionalKey(Schema.String),\n message: Schema.String,\n}) {}\nexport class ProviderUnavailable extends Schema.TaggedError<ProviderUnavailable>(\n \"@domainkit/Reason/ProviderUnavailable\",\n)(\"ProviderUnavailable\", {\n provider: Schema.String,\n retryAfterMs: Schema.optionalKey(Schema.Number),\n message: Schema.String,\n}) {}\n/** The provider credential no longer works; the customer must reconnect. */\nexport class Reconnect extends Schema.TaggedError<Reconnect>(\"@domainkit/Reason/Reconnect\")(\n \"Reconnect\",\n { provider: Schema.String, connectionId: Schema.String },\n) {\n override get message(): string {\n return `${this.provider} connection ${this.connectionId} must be reconnected`;\n }\n}\nexport class StorageFailed extends Schema.TaggedError<StorageFailed>(\n \"@domainkit/Reason/StorageFailed\",\n)(\"StorageFailed\", { operation: Schema.String, message: Schema.String }) {}\nexport class CryptoFailed extends Schema.TaggedError<CryptoFailed>(\n \"@domainkit/Reason/CryptoFailed\",\n)(\"CryptoFailed\", { operation: Schema.Literals([\"digest\", \"seal\", \"open\"]) }) {\n override get message(): string {\n return `crypto ${this.operation} failed`;\n }\n}\nexport class ResolverFailed extends Schema.TaggedError<ResolverFailed>(\n \"@domainkit/Reason/ResolverFailed\",\n)(\"ResolverFailed\", { resolver: Schema.String, message: Schema.String }) {}\n\n/** The provider refused the write because a conflicting record already exists there. */\nexport class ProviderConflict extends Schema.TaggedError<ProviderConflict>(\n \"@domainkit/Reason/ProviderConflict\",\n)(\"ProviderConflict\", {\n provider: Schema.String,\n code: Schema.optionalKey(Schema.String),\n message: Schema.String,\n}) {}\n/** The provider, or this target within it, cannot do the requested operation. */\nexport class Unsupported extends Schema.TaggedError<Unsupported>(\"@domainkit/Reason/Unsupported\")(\n \"Unsupported\",\n { provider: Schema.String, operation: Schema.String, message: Schema.String },\n) {}\n\nexport const Model = Schema.Union([\n InvalidInput,\n Unauthenticated,\n Forbidden,\n NotFound,\n Conflict,\n Stale,\n BatchStale,\n Expired,\n Busy,\n ProviderRejected,\n ProviderUnavailable,\n ProviderConflict,\n Unsupported,\n Reconnect,\n StorageFailed,\n CryptoFailed,\n ResolverFailed,\n]);\nexport type Model = typeof Model.Type;\n"],"mappings":"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"}