@domainkit/react 0.8.0 → 0.9.1

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/dist/index.mjs CHANGED
@@ -1,37 +1,18 @@
1
+ "use client";
1
2
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import { AlertDialog } from "@base-ui/react/alert-dialog";
3
- import { RegistryProvider, useAtomSet, useAtomValue } from "@effect/atom-react";
4
- import { DomainName, Transport } from "domainkit";
5
- import * as Data from "effect/Data";
3
+ import { DnsRecord, DomainKit, Reason, Receipt } from "domainkit";
6
4
  import * as Effect from "effect/Effect";
7
- import * as Atom from "effect/unstable/reactivity/Atom";
8
5
  import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
6
+ import * as Data from "effect/Data";
7
+ import { Transport } from "domainkit/client";
9
8
  import { useRender } from "@base-ui/react/use-render";
10
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
+ import * as Exit from "effect/Exit";
11
+ import * as Option from "effect/Option";
11
12
  import { Dialog } from "@base-ui/react/dialog";
12
- import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
13
- import { Menu } from "@base-ui/react/menu";
14
13
  import { Popover } from "@base-ui/react/popover";
15
- //#region src/atom.ts
16
- const Failure = Data.taggedEnum();
17
- const failureFromError = (error) => Failure.Failure({
18
- message: error.message,
19
- operation: error.operation,
20
- retry: error.retry
21
- });
22
- const failureFromDefect = (operation, message) => Failure.Failure({
23
- message,
24
- operation,
25
- retry: "safe"
26
- });
27
- const recordsIdentity = (records) => JSON.stringify(records.map(({ id, name, priority, type, value }) => ({
28
- id,
29
- name,
30
- priority,
31
- type,
32
- value
33
- })));
34
- //#endregion
14
+ import * as DateTime from "effect/DateTime";
15
+ import { Testing } from "domainkit/testing";
35
16
  //#region src/composition.tsx
36
17
  function usePart(defaultTagName, componentProps, state, internalProps) {
37
18
  const { className, render, style, ...externalProps } = componentProps;
@@ -45,6 +26,12 @@ function usePart(defaultTagName, componentProps, state, internalProps) {
45
26
  };
46
27
  return useRender(parameters);
47
28
  }
29
+ /** A part with no state of its own: one tag, one `data-domainkit-part`, host props on top. */
30
+ function leafPart(defaultTagName, part) {
31
+ return function Leaf(props) {
32
+ return usePart(defaultTagName, props, {}, { "data-domainkit-part": part });
33
+ };
34
+ }
48
35
  //#endregion
49
36
  //#region src/icons.tsx
50
37
  function Glyph({ children }) {
@@ -60,6 +47,7 @@ function Glyph({ children }) {
60
47
  });
61
48
  }
62
49
  const defaultIcons = {
50
+ close: /* @__PURE__ */ jsxs(Glyph, { children: [/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }), /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })] }),
63
51
  copied: /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" }) }),
64
52
  copy: /* @__PURE__ */ jsxs(Glyph, { children: [/* @__PURE__ */ jsx("rect", {
65
53
  height: "14",
@@ -72,14 +60,37 @@ const defaultIcons = {
72
60
  /* @__PURE__ */ jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
73
61
  /* @__PURE__ */ jsx("path", { d: "m7 10 5 5 5-5" }),
74
62
  /* @__PURE__ */ jsx("path", { d: "M12 15V3" })
75
- ] })
63
+ ] }),
64
+ external: /* @__PURE__ */ jsxs(Glyph, { children: [
65
+ /* @__PURE__ */ jsx("path", { d: "M15 3h6v6" }),
66
+ /* @__PURE__ */ jsx("path", { d: "M10 14 21 3" }),
67
+ /* @__PURE__ */ jsx("path", { d: "M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5" })
68
+ ] }),
69
+ failure: /* @__PURE__ */ jsxs(Glyph, { children: [
70
+ /* @__PURE__ */ jsx("circle", {
71
+ cx: "12",
72
+ cy: "12",
73
+ r: "9"
74
+ }),
75
+ /* @__PURE__ */ jsx("path", { d: "M15 9 9 15" }),
76
+ /* @__PURE__ */ jsx("path", { d: "m9 9 6 6" })
77
+ ] }),
78
+ pending: /* @__PURE__ */ jsxs(Glyph, { children: [/* @__PURE__ */ jsx("circle", {
79
+ cx: "12",
80
+ cy: "12",
81
+ r: "9"
82
+ }), /* @__PURE__ */ jsx("path", { d: "M12 7v5l3 2" })] }),
83
+ success: /* @__PURE__ */ jsxs(Glyph, { children: [/* @__PURE__ */ jsx("circle", {
84
+ cx: "12",
85
+ cy: "12",
86
+ r: "9"
87
+ }), /* @__PURE__ */ jsx("path", { d: "m8.5 12.5 2.5 2.5 4.5-5" })] })
76
88
  };
77
89
  const IconsContext = createContext(defaultIcons);
78
90
  function IconsProvider({ children, icons }) {
79
91
  const value = useMemo(() => ({
80
- copied: icons?.copied ?? defaultIcons.copied,
81
- copy: icons?.copy ?? defaultIcons.copy,
82
- download: icons?.download ?? defaultIcons.download
92
+ ...defaultIcons,
93
+ ...icons
83
94
  }), [icons]);
84
95
  return /* @__PURE__ */ jsx(IconsContext.Provider, {
85
96
  value,
@@ -93,54 +104,237 @@ function useIcons() {
93
104
  //#region src/messages.ts
94
105
  var messages_exports = /* @__PURE__ */ __exportAll({
95
106
  english: () => english,
107
+ failure: () => failure,
96
108
  merge: () => merge
97
109
  });
110
+ const entity = {
111
+ approval: "approval",
112
+ attachment: "domain attachment",
113
+ authorization: "provider authorization",
114
+ connection: "provider connection",
115
+ continuation: "sign-in attempt",
116
+ plan: "plan",
117
+ provider: "provider",
118
+ receipt: "receipt",
119
+ record: "DNS record",
120
+ zone: "DNS zone"
121
+ };
122
+ const expiredEntity = {
123
+ approval: "approval",
124
+ continuation: "sign-in attempt",
125
+ credential: "provider credential",
126
+ plan: "plan"
127
+ };
128
+ const recordTypes = {
129
+ A: "A",
130
+ AAAA: "AAAA",
131
+ CAA: "CAA",
132
+ CNAME: "CNAME",
133
+ MX: "MX",
134
+ NS: "NS",
135
+ SRV: "SRV",
136
+ TXT: "TXT"
137
+ };
138
+ /** `accountId` reads as "Account id" until a host names the field itself. */
139
+ const humanize = (name) => {
140
+ const spaced = name.replaceAll(/([a-z0-9])([A-Z])/g, "$1 $2").replaceAll(/[_-]+/g, " ").trim();
141
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
142
+ };
98
143
  const english = {
99
- applyCleanup: "Remove records",
100
- applyDns: "Add records",
101
- applyingDns: "Adding records…",
102
- authenticationAlternative: "or",
103
- automaticUnavailable: "Automatic connection is not available for this domain",
144
+ connect: "Connect",
145
+ connectWith: (method) => method,
146
+ disconnect: "Disconnect",
147
+ detach: "Detach domain",
104
148
  cancel: "Cancel",
105
- checkAgain: "Check again",
106
- checkingDns: "Checking DNS…",
149
+ close: "Close",
150
+ retry: "Try again",
151
+ approve: "Approve",
152
+ decline: "Decline",
153
+ reviewChanges: "Review changes",
154
+ cleanUp: "Remove records",
107
155
  checkDns: "Check DNS",
108
- cleanupConsent: "Only records proven by the apply receipt will be deleted.",
109
- cleanupPartial: "Some DNS records could not be deleted. Review and retry safely.",
110
- cleaningDns: "Removing records…",
111
- connectProvider: () => "Connect",
112
- connected: (provider) => `${provider} connected`,
113
- providerAvailable: (provider) => `${provider} manages DNS for this domain`,
156
+ checkAgain: "Check again",
157
+ moreActions: "More actions",
158
+ copy: "Copy",
159
+ copied: "Copied",
160
+ copyZone: "Copy zone file",
161
+ download: "Download",
162
+ useConnection: (label) => `Use ${label}`,
163
+ chooseZone: "Choose the zone that serves this domain",
164
+ getToken: "Where do I find this?",
165
+ loading: "Loading…",
114
166
  connecting: "Connecting…",
115
- close: "Close",
116
- detectingProvider: "Detecting DNS provider…",
117
- disconnectDomain: "Disconnect",
118
- disconnectConsent: "This domain will be detached from the provider. Existing DNS records will be preserved.",
167
+ redirecting: "Opening the provider…",
168
+ detaching: "Detaching…",
119
169
  disconnecting: "Disconnecting…",
120
- detaching: "Detaching domain…",
121
- disconnectTitle: (provider) => `Detach ${provider}?`,
122
- domainDisconnected: "Domain detached. DNS records were preserved.",
123
- dialogDescription: (domain) => `Authorize DNS changes for ${domain}.`,
124
- dialogTitle: (provider) => `Connect ${provider}`,
125
- openingAuthorization: "Opening provider authorization…",
126
- moreActions: "More connection actions",
127
- noChanges: "No changes needed",
128
- planConsent: "Review the exact DNS operations before approving this plan.",
170
+ planning: "Preparing DNS changes…",
171
+ approving: "Approving…",
172
+ applying: "Adding records…",
173
+ rejecting: "Declining…",
129
174
  planningCleanup: "Preparing cleanup…",
130
- planningDns: "Preparing DNS changes…",
131
- recordsPartiallyApplied: "Some DNS changes failed. Review and retry safely.",
132
- reviewCleanup: "Remove records",
133
- reviewDns: "Review changes",
134
- retry: "Try again",
135
- existingConnection: "Existing provider connection",
136
- targetUnavailable: "No provider targets are available for this connection.",
137
- attachTarget: (target) => `Use ${target}`,
138
- tokenLabel: "API token"
175
+ cleaning: "Removing records…",
176
+ observing: "Checking DNS…",
177
+ connectTitle: (provider) => `Connect ${provider}`,
178
+ connectAnyTitle: "Connect your DNS provider",
179
+ connectDescription: (domain) => `Authorize DNS changes for ${domain}.`,
180
+ connectedTo: (provider) => `${provider} connected`,
181
+ notConnected: "No DNS provider is connected.",
182
+ reconnectRequired: (provider) => `${provider} needs to be reconnected`,
183
+ disconnectTitle: (provider) => `Disconnect ${provider}?`,
184
+ disconnectConsent: "The provider connection is removed. Existing DNS records are preserved.",
185
+ detachConsent: "This domain is detached from the provider. Existing DNS records are preserved.",
186
+ detached: "Domain detached. DNS records were preserved.",
187
+ reusableConnections: "Connections you already have",
188
+ authenticationAlternative: "or",
189
+ discoveredConnection: (label) => `${label} already serves this domain`,
190
+ discoveryAmbiguous: "More than one connection can serve this domain. Pick one.",
191
+ discoveryNotFound: (nameservers) => nameservers.length === 0 ? "No connected provider serves this domain yet." : `No connected provider serves this domain. Its nameservers are ${nameservers.join(", ")}.`,
192
+ noProviders: "No DNS providers are available.",
193
+ fieldLabel: humanize,
194
+ optionalField: "optional",
195
+ planTitle: "Review DNS changes",
196
+ planConsent: "Review the exact DNS operations before approving this plan.",
197
+ cleanupTitle: "Review the records to remove",
198
+ cleanupConsent: "Only records the apply receipt proves DomainKit created will be deleted.",
199
+ noChanges: "No changes needed",
200
+ planExpires: (at) => `This plan expires at ${at}.`,
201
+ declinedBy: (actor) => `Declined by ${actor}. Build a new plan to continue.`,
202
+ declineReason: (reason) => `Reason: ${reason}`,
203
+ operation: (operation) => {
204
+ switch (operation._tag) {
205
+ case "Create": return "Add";
206
+ case "Noop": return "Already in place";
207
+ case "Delete": return "Remove";
208
+ case "Conflict": return "Blocked";
209
+ }
210
+ },
211
+ conflictReason: (reason) => {
212
+ switch (reason) {
213
+ case "exclusive-name": return "Another record already owns this name.";
214
+ case "cname-collision": return "A CNAME cannot share a name with other records.";
215
+ case "value-mismatch": return "A record with this name holds a different value.";
216
+ case "opaque": return "A record here uses a form DomainKit will not overwrite.";
217
+ case "missing": return "The record this plan expected is gone.";
218
+ }
219
+ },
220
+ attemptStatus: (status) => {
221
+ switch (status) {
222
+ case "planned": return "Awaiting review";
223
+ case "approved": return "Approved";
224
+ case "applying": return "Applying";
225
+ case "complete": return "Applied";
226
+ case "partial": return "Partly applied";
227
+ case "failed": return "Failed";
228
+ case "expired": return "Expired";
229
+ case "rejected": return "Declined";
230
+ }
231
+ },
232
+ recordsCaption: (domain) => `DNS records for ${domain}`,
233
+ headingType: "Type",
234
+ headingName: "Name",
235
+ headingValue: "Value",
236
+ headingStatus: "Status",
237
+ headingPurpose: "Purpose",
238
+ priority: (priority) => `Priority ${priority}`,
239
+ ttl: (seconds) => `TTL ${seconds}s`,
240
+ recordType: (record) => record._tag === "Opaque" ? record.type : recordTypes[record._tag],
241
+ applied: (receipt) => receipt.status === "complete" ? "DNS records added." : "Some DNS records were not added.",
242
+ cleaned: (receipt) => receipt.status === "complete" ? "DNS records removed." : "Some DNS records were not removed.",
243
+ partiallyApplied: "Some DNS changes failed. Review and retry safely.",
244
+ partiallyCleaned: "Some DNS records could not be deleted. Review and retry safely.",
245
+ outcome: (outcome) => {
246
+ switch (outcome._tag) {
247
+ case "Applied": return "Done";
248
+ case "Skipped": return "Skipped";
249
+ case "Failed": return outcome.message;
250
+ }
251
+ },
252
+ skippedReason: (reason) => {
253
+ switch (reason) {
254
+ case "noop": return "The record was already in place.";
255
+ case "not-approved": return "The approval left this operation out.";
256
+ case "not-attempted": return "An earlier operation failed before this one ran.";
257
+ }
258
+ },
259
+ requirementStatus: (status) => {
260
+ switch (status) {
261
+ case "satisfied": return "Found";
262
+ case "missing": return "Missing";
263
+ case "mismatch": return "Different value";
264
+ case "unknown": return "Not checked";
265
+ }
266
+ },
267
+ overall: (overall) => {
268
+ switch (overall) {
269
+ case "ready": return "Ready";
270
+ case "pending": return "Pending";
271
+ case "failed": return "Failed";
272
+ }
273
+ },
274
+ evidenceSource: (evidence) => {
275
+ switch (evidence._tag) {
276
+ case "Host": return evidence.label;
277
+ case "Provider": return `${evidence.provider} records`;
278
+ case "PublicDns": return `Public DNS via ${evidence.resolver}`;
279
+ }
280
+ },
281
+ hostEvidenceStatus: (status) => {
282
+ switch (status) {
283
+ case "ok": return "Ready";
284
+ case "pending": return "Pending";
285
+ case "failed": return "Failed";
286
+ }
287
+ },
288
+ expectedValue: (value) => `Expected ${value}`,
289
+ observedValues: (values) => `Found ${values.join(", ")}`,
290
+ observedNothing: "Found nothing",
291
+ checkedAt: (at) => `Checked ${at}`,
292
+ nextCheckAt: (at) => `Next check ${at}`,
293
+ hostEvidence: "Other checks",
294
+ noEvidence: "Nothing has been checked yet.",
295
+ invalidInput: (reason) => reason.field === void 0 ? reason.message : `${humanize(reason.field)}: ${reason.message}`,
296
+ unauthenticated: () => "The provider rejected these credentials.",
297
+ forbidden: () => "You do not have access to this domain.",
298
+ notFound: (reason) => `That ${entity[reason.entity]} no longer exists.`,
299
+ conflict: (reason) => reason.operations.length === 1 ? "One DNS record conflicts with what is already in the zone." : `${reason.operations.length} DNS records conflict with what is already in the zone.`,
300
+ stale: () => "The zone changed while you were reviewing. Build a new plan.",
301
+ expired: (reason) => `That ${expiredEntity[reason.entity]} expired. Start again.`,
302
+ busy: () => "Another change is running. Try again in a moment.",
303
+ providerRejected: (reason) => `${reason.provider} refused the change: ${reason.message}`,
304
+ providerUnavailable: (reason) => `${reason.provider} is unreachable right now.`,
305
+ providerConflict: (reason) => `${reason.provider} already holds a conflicting record.`,
306
+ unsupported: (reason) => `${reason.provider} cannot ${reason.operation}.`,
307
+ reconnect: (reason) => `Reconnect ${reason.provider} to continue.`,
308
+ storageFailed: () => "The change could not be saved. Try again.",
309
+ cryptoFailed: () => "A stored credential could not be read.",
310
+ resolverFailed: (reason) => `DNS lookups through ${reason.resolver} failed.`
139
311
  };
140
312
  const merge = (overrides = {}) => ({
141
313
  ...english,
142
314
  ...overrides
143
315
  });
316
+ /** The user-facing sentence for a failure, chosen by the error's reason. */
317
+ const failure = (error, catalog) => {
318
+ const reason = error.reason;
319
+ switch (reason._tag) {
320
+ case "InvalidInput": return catalog.invalidInput(reason);
321
+ case "Unauthenticated": return catalog.unauthenticated(reason);
322
+ case "Forbidden": return catalog.forbidden(reason);
323
+ case "NotFound": return catalog.notFound(reason);
324
+ case "Conflict": return catalog.conflict(reason);
325
+ case "Stale": return catalog.stale(reason);
326
+ case "Expired": return catalog.expired(reason);
327
+ case "Busy": return catalog.busy(reason);
328
+ case "ProviderRejected": return catalog.providerRejected(reason);
329
+ case "ProviderUnavailable": return catalog.providerUnavailable(reason);
330
+ case "ProviderConflict": return catalog.providerConflict(reason);
331
+ case "Unsupported": return catalog.unsupported(reason);
332
+ case "Reconnect": return catalog.reconnect(reason);
333
+ case "StorageFailed": return catalog.storageFailed(reason);
334
+ case "CryptoFailed": return catalog.cryptoFailed(reason);
335
+ case "ResolverFailed": return catalog.resolverFailed(reason);
336
+ }
337
+ };
144
338
  //#endregion
145
339
  //#region src/theme.ts
146
340
  var theme_exports = /* @__PURE__ */ __exportAll({ toStyle: () => toStyle });
@@ -166,14 +360,45 @@ function toStyle(theme = {}) {
166
360
  //#endregion
167
361
  //#region src/domain-kit.tsx
168
362
  var domain_kit_exports = /* @__PURE__ */ __exportAll({
363
+ ReadOnly: () => ReadOnly,
169
364
  Root: () => Root$6,
170
- useDomainKit: () => useDomainKit
365
+ useCapabilities: () => useCapabilities,
366
+ useDomainKit: () => useDomainKit,
367
+ useReadOnly: () => useReadOnly,
368
+ useTransport: () => useTransport
171
369
  });
172
370
  const Context = createContext(null);
173
371
  const navigateInBrowser = (url) => {
174
- if (typeof window === "undefined") throw new Error("DomainKit OAuth navigation requires a browser or a custom navigate function");
372
+ if (typeof window === "undefined") throw new Error("DomainKit provider authorization requires a browser or a `navigate` prop");
175
373
  window.location.assign(url);
176
374
  };
375
+ /**
376
+ * A transport whose identity survives re-renders. Each method reads the newest transport when it
377
+ * runs, so a host may write `<DomainKit.Root transport={Transport.fromFetch("/api/domainkit")}>`
378
+ * inline and controllers still see one transport for the whole mount. Swapping in a transport
379
+ * that declares different capability groups rebuilds it, and every controller re-runs.
380
+ */
381
+ const useStableTransport = (transport) => {
382
+ const latest = useRef(transport);
383
+ useLayoutEffect(() => {
384
+ latest.current = transport;
385
+ });
386
+ const signature = Transport.capabilities(transport).join(",");
387
+ return useMemo(() => {
388
+ const capabilities = Transport.capabilities(latest.current);
389
+ return {
390
+ capabilities,
391
+ transport: Object.fromEntries(capabilities.map((capability) => {
392
+ const group = latest.current[capability];
393
+ return [capability, Object.fromEntries(Object.keys(group).map((name) => [name, (...args) => Effect.suspend(() => {
394
+ const method = latest.current[capability][name];
395
+ if (method === void 0) throw new Error(`The transport no longer declares ${capability}.${name}`);
396
+ return method(...args);
397
+ })]))];
398
+ }))
399
+ };
400
+ }, [signature]);
401
+ };
177
402
  const resolvePortalContainer = (container, ownerDocument) => {
178
403
  if (container === null || ownerDocument === null) return null;
179
404
  return container.ownerDocument === ownerDocument && container.getRootNode() === ownerDocument ? container : null;
@@ -192,8 +417,8 @@ const usePortalContainer = (container) => {
192
417
  const getSnapshot = useCallback(() => resolvePortalContainer(container, ownerDocument), [container, ownerDocument]);
193
418
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
194
419
  };
195
- function Root$6({ children, colorScheme = "inherit", icons, marks = {}, messages, navigate = navigateInBrowser, onEvent, portalContainer = null, theme, transport, ...props }) {
196
- const runtime = useMemo(() => Atom.runtime(transport), [transport]);
420
+ function Root$6({ children, colorScheme = "inherit", icons, marks = {}, messages, navigate = navigateInBrowser, onEvent, portalContainer = null, readOnly = false, revision = 0, theme, transport, ...props }) {
421
+ const { capabilities, transport: stable } = useStableTransport(transport);
197
422
  const onEventRef = useRef(onEvent);
198
423
  useLayoutEffect(() => {
199
424
  onEventRef.current = onEvent;
@@ -206,844 +431,340 @@ function Root$6({ children, colorScheme = "inherit", icons, marks = {}, messages
206
431
  const themeStyle = toStyle(theme);
207
432
  const resolvedPortalContainer = usePortalContainer(portalContainer);
208
433
  const portalContainerRef = useMemo(() => ({ current: resolvedPortalContainer }), [resolvedPortalContainer]);
434
+ const value = {
435
+ capabilities,
436
+ colorScheme,
437
+ emit,
438
+ marks,
439
+ messages: merge(messages),
440
+ navigate,
441
+ portalContainer: portalContainerRef,
442
+ readOnly,
443
+ revision,
444
+ themeStyle,
445
+ transport: stable
446
+ };
209
447
  const content = usePart("div", props, { colorScheme }, {
210
448
  children,
211
449
  "data-color-scheme": colorScheme,
212
450
  "data-domainkit-root": "",
213
451
  style: themeStyle
214
452
  });
215
- return /* @__PURE__ */ jsx(RegistryProvider, { children: /* @__PURE__ */ jsx(Context.Provider, {
216
- value: {
217
- colorScheme,
218
- emit,
219
- marks,
220
- messages: merge(messages),
221
- navigate,
222
- portalContainer: portalContainerRef,
223
- runtime,
224
- themeStyle
225
- },
453
+ return /* @__PURE__ */ jsx(Context.Provider, {
454
+ value,
226
455
  children: /* @__PURE__ */ jsx(IconsProvider, {
227
456
  ...icons === void 0 ? {} : { icons },
228
457
  children: content
229
458
  })
230
- }) });
459
+ });
231
460
  }
232
461
  function useDomainKit() {
233
462
  const value = useContext(Context);
234
463
  if (value === null) throw new Error("DomainKit components must be rendered inside DomainKit.Root");
235
464
  return value;
236
465
  }
237
- //#endregion
238
- //#region src/lifecycle.ts
239
- var lifecycle_exports = /* @__PURE__ */ __exportAll({ Event: () => Event });
240
- const Event = Data.taggedEnum();
241
- //#endregion
242
- //#region src/operations.tsx
243
- var operations_exports = /* @__PURE__ */ __exportAll({
244
- Item: () => Item,
245
- Kind: () => Kind,
246
- List: () => List,
247
- Name: () => Name,
248
- Priority: () => Priority$1,
249
- Reason: () => Reason,
250
- Record: () => Record,
251
- Root: () => Root$5,
252
- Type: () => Type,
253
- Value: () => Value$1
254
- });
255
- function leafPart$1(defaultTagName, part) {
256
- return function Leaf(props) {
257
- return usePart(defaultTagName, props, {}, { "data-domainkit-part": part });
258
- };
466
+ /** The transport `DomainKit.Root` holds, with the identity it keeps for the whole mount. */
467
+ function useTransport() {
468
+ return useDomainKit().transport;
259
469
  }
260
- const Kind = leafPart$1("span", "operation-kind");
261
- const Type = leafPart$1("strong", "operation-type");
262
- const Record = leafPart$1("span", "operation-record");
263
- const Name = leafPart$1("span", "operation-name");
264
- const Value$1 = leafPart$1("code", "operation-value");
265
- const Priority$1 = leafPart$1("span", "operation-priority");
266
- const Reason = leafPart$1("span", "operation-reason");
267
- function Item({ operation, ...props }) {
268
- return usePart("li", props, { operation: operation._tag }, {
269
- children: /* @__PURE__ */ jsxs(Fragment, { children: [
270
- /* @__PURE__ */ jsx(Kind, { children: operation._tag }),
271
- " ",
272
- /* @__PURE__ */ jsx(Type, { children: operation.record.type }),
273
- " ",
274
- /* @__PURE__ */ jsxs(Record, { children: [
275
- /* @__PURE__ */ jsx(Name, { children: operation.record.name }),
276
- /* @__PURE__ */ jsx(Value$1, { children: operation.record.value }),
277
- operation.record.priority === void 0 ? null : /* @__PURE__ */ jsxs(Priority$1, { children: ["Priority ", operation.record.priority] }),
278
- "reason" in operation ? /* @__PURE__ */ jsx(Reason, { children: operation.reason }) : null
279
- ] })
280
- ] }),
281
- "data-domainkit-part": "operation-item",
282
- "data-operation": operation._tag
470
+ const ReadOnlyContext = createContext(null);
471
+ /** Narrow one subtree to read-only without touching the rest of the page. */
472
+ function ReadOnly({ children, value }) {
473
+ return /* @__PURE__ */ jsx(ReadOnlyContext.Provider, {
474
+ value,
475
+ children
283
476
  });
284
477
  }
285
- function Root$5({ count = 0, lifecycle, ...props }) {
286
- return usePart("ul", props, {
287
- count,
288
- lifecycle
289
- }, {
290
- "data-domainkit-part": lifecycle === "provisioning" ? "plan-operations" : "cleanup-operations",
291
- "data-lifecycle": lifecycle
292
- });
478
+ /** Whether this part may offer controls that change the domain. */
479
+ function useReadOnly() {
480
+ const scoped = useContext(ReadOnlyContext);
481
+ const root = useDomainKit().readOnly;
482
+ return scoped ?? root;
293
483
  }
294
- function List({ lifecycle, operations, ...props }) {
295
- return /* @__PURE__ */ jsx(Root$5, {
296
- count: operations.length,
297
- lifecycle,
298
- ...props,
299
- children: operations.map((operation) => /* @__PURE__ */ jsx(Item, { operation }, operation.id))
300
- });
484
+ /** Which capability groups the host's transport declares, for gating a custom part. */
485
+ function useCapabilities() {
486
+ return useDomainKit().capabilities;
301
487
  }
302
488
  //#endregion
303
- //#region src/cleanup.tsx
304
- var cleanup_exports = /* @__PURE__ */ __exportAll({
305
- Command: () => Command$3,
306
- Dialog: () => Dialog$3,
307
- Flow: () => Flow$3,
308
- Outcome: () => Outcome$2,
309
- Root: () => Root$4,
310
- State: () => State$3,
311
- useController: () => useController$3,
312
- useModel: () => useModel$3
313
- });
314
- const State$3 = Data.taggedEnum();
315
- const Command$3 = Data.taggedEnum();
316
- function useModel$3(connection, receiptId, onCleaned) {
317
- const { emit, runtime } = useDomainKit();
318
- const onCleanedRef = useRef(onCleaned);
319
- onCleanedRef.current = onCleaned;
489
+ //#region src/events.ts
490
+ const Event = Data.taggedEnum();
491
+ //#endregion
492
+ //#region src/task.ts
493
+ /**
494
+ * The one place the package leaves Effect. Every controller runs its transport calls through
495
+ * `useRunner`, which interrupts the call in flight when a newer one starts or the component
496
+ * unmounts, and drops results that arrive after either.
497
+ */
498
+ /** A defect is not a lifecycle failure; report it in the shape the UI already renders. */
499
+ const defect = (cause) => new DomainKit.Error({ reason: new Reason.ProviderUnavailable({
500
+ provider: "domainkit",
501
+ message: `The transport failed: ${String(cause)}`
502
+ }) });
503
+ function useRunner() {
504
+ const state = useRef({
505
+ live: null,
506
+ mounted: true,
507
+ next: 0
508
+ });
509
+ useEffect(() => {
510
+ const current = state.current;
511
+ current.mounted = true;
512
+ return () => {
513
+ current.mounted = false;
514
+ current.live?.interrupt?.();
515
+ current.live = null;
516
+ };
517
+ }, []);
320
518
  return useMemo(() => {
321
- const state = Atom.make(State$3.Idle());
322
- return {
323
- command: runtime.fn()((command, get) => {
324
- const current = get(state);
325
- if (command._tag === "Dismiss") {
326
- if (current._tag === "Planning" || current._tag === "Cleaning") return Effect.void;
327
- get.set(state, State$3.Idle());
328
- return Effect.void;
329
- }
330
- if (command._tag === "Plan") {
331
- if (current._tag === "Planning" || current._tag === "Cleaning") return Effect.void;
332
- get.set(state, State$3.Planning());
333
- return Effect.flatMap(Transport.Service, (transport) => transport.cleanup.plan({
334
- attachmentId: connection.attachment.id,
335
- domain: connection.attachment.domain,
336
- receiptId
337
- })).pipe(Effect.tap((plan) => Effect.sync(() => get.set(state, State$3.Review({ plan })))), Effect.catch((error) => Effect.sync(() => get.set(state, failureFromError(error)))), Effect.asVoid);
338
- }
339
- if (current._tag !== "Review" || current.plan.operations.some((operation) => operation._tag === "Blocked")) return Effect.void;
340
- const plan = current.plan;
341
- get.set(state, State$3.Cleaning({ plan }));
342
- return Effect.flatMap(Transport.Service, (transport) => transport.cleanup.apply({
343
- attachmentId: connection.attachment.id,
344
- domain: connection.attachment.domain,
345
- planDigest: plan.digest,
346
- receiptId
347
- })).pipe(Effect.tap((result) => Effect.sync(() => {
348
- if (result._tag === "Cleaned") {
349
- get.set(state, State$3.Cleaned({ result }));
350
- onCleanedRef.current?.(result);
351
- emit(Event.RecordsCleaned({
352
- connection,
353
- result
354
- }));
355
- } else if (result._tag === "Partial") {
356
- get.set(state, State$3.Partial({ result }));
357
- onCleanedRef.current?.(result);
358
- emit(Event.RecordsPartiallyCleaned({
359
- connection,
360
- result
361
- }));
362
- } else get.set(state, result);
363
- })), Effect.catch((error) => Effect.sync(() => get.set(state, failureFromError(error)))), Effect.asVoid);
364
- }),
365
- state
519
+ const stop = () => {
520
+ state.current.live?.interrupt?.();
521
+ state.current.live = null;
366
522
  };
367
- }, [
368
- connection.attachment.domain,
369
- connection.attachment.id,
370
- emit,
371
- receiptId,
372
- runtime
373
- ]);
374
- }
375
- function useController$3(connection, receiptId, onCleaned) {
376
- const model = useModel$3(connection, receiptId, onCleaned);
377
- const state = useAtomValue(model.state);
378
- const execute = useAtomSet(model.command);
379
- return {
380
- apply: () => execute(Command$3.Apply()),
381
- dismiss: () => execute(Command$3.Dismiss()),
382
- plan: () => execute(Command$3.Plan()),
383
- retry: () => execute(Command$3.Plan()),
384
- state
385
- };
386
- }
387
- function Root$4({ status, ...props }) {
388
- return usePart("div", props, { status }, {
389
- "data-domainkit-part": "cleanup-flow",
390
- "data-state": status
391
- });
392
- }
393
- function Outcome$2({ state }) {
394
- const { messages } = useDomainKit();
395
- if (state._tag === "Partial" || state._tag === "Failure" || state._tag === "Stale") return /* @__PURE__ */ jsx("p", {
396
- "data-domainkit-part": "flow-outcome",
397
- "data-tone": "danger",
398
- role: "alert",
399
- children: state._tag === "Partial" ? messages.cleanupPartial : state.message
400
- });
401
- return null;
402
- }
403
- function Dialog$3({ controller, onOpenChange, onOpenChangeComplete, trigger }) {
404
- const { colorScheme, messages, portalContainer, themeStyle } = useDomainKit();
405
- const state = controller.state;
406
- const planRef = useRef(void 0);
407
- if (state._tag === "Idle") planRef.current = void 0;
408
- if (state._tag === "Review" || state._tag === "Cleaning") planRef.current = state.plan;
409
- const plan = planRef.current;
410
- const busy = state._tag === "Planning" || state._tag === "Cleaning";
411
- const open = state._tag !== "Idle" && state._tag !== "Cleaned";
412
- return /* @__PURE__ */ jsxs(AlertDialog.Root, {
413
- onOpenChange: (nextOpen, eventDetails) => {
414
- if (!nextOpen && busy) {
415
- eventDetails.cancel();
416
- return;
523
+ return {
524
+ cancel: stop,
525
+ run: (effect, handlers) => {
526
+ stop();
527
+ const live = {
528
+ id: state.current.next += 1,
529
+ interrupt: null
530
+ };
531
+ state.current.live = live;
532
+ const interrupt = Effect.runCallback(effect, { onExit: (exit) => {
533
+ if (state.current.live?.id !== live.id || !state.current.mounted) return;
534
+ state.current.live = null;
535
+ if (Exit.isSuccess(exit)) {
536
+ handlers.onSuccess(exit.value);
537
+ return;
538
+ }
539
+ const cause = exit.cause;
540
+ const error = Exit.findErrorOption(exit);
541
+ if (Exit.hasInterrupts(exit)) return;
542
+ handlers.onFailure(Option.isSome(error) ? error.value : defect(cause));
543
+ } });
544
+ if (state.current.live?.id === live.id) live.interrupt = () => interrupt();
417
545
  }
418
- if (!nextOpen) controller.dismiss();
419
- onOpenChange?.(nextOpen);
420
- },
421
- onOpenChangeComplete,
422
- open,
423
- children: [trigger === null ? null : /* @__PURE__ */ jsx(AlertDialog.Trigger, {
424
- "data-domainkit-part": "cleanup-trigger",
425
- disabled: state._tag !== "Idle",
426
- onClick: () => controller.plan(),
427
- ...trigger === void 0 ? {} : { render: trigger },
428
- children: messages.reviewCleanup
429
- }), /* @__PURE__ */ jsxs(AlertDialog.Portal, {
430
- container: portalContainer,
431
- children: [/* @__PURE__ */ jsx(AlertDialog.Backdrop, {
432
- "data-color-scheme": colorScheme,
433
- "data-domainkit-part": "dialog-backdrop",
434
- "data-domainkit-root": "",
435
- style: themeStyle
436
- }), /* @__PURE__ */ jsxs(AlertDialog.Popup, {
437
- "data-color-scheme": colorScheme,
438
- "data-domainkit-part": "cleanup-dialog",
439
- "data-domainkit-root": "",
440
- style: themeStyle,
441
- children: [
442
- /* @__PURE__ */ jsxs("div", {
443
- "data-domainkit-part": "dialog-header",
444
- children: [/* @__PURE__ */ jsxs("div", {
445
- "data-domainkit-part": "dialog-heading",
446
- children: [/* @__PURE__ */ jsx(AlertDialog.Title, {
447
- "data-domainkit-part": "dialog-title",
448
- children: messages.reviewCleanup
449
- }), /* @__PURE__ */ jsx(AlertDialog.Description, {
450
- "data-domainkit-part": "dialog-description",
451
- children: messages.cleanupConsent
452
- })]
453
- }), busy ? null : /* @__PURE__ */ jsx(AlertDialog.Close, {
454
- "aria-label": messages.close,
455
- "data-domainkit-part": "dialog-close",
456
- children: "×"
457
- })]
458
- }),
459
- /* @__PURE__ */ jsxs("div", {
460
- "data-domainkit-part": "dialog-body",
461
- children: [plan === void 0 ? /* @__PURE__ */ jsx("p", {
462
- "data-domainkit-part": "dialog-progress",
463
- role: "status",
464
- children: messages.planningCleanup
465
- }) : /* @__PURE__ */ jsx(List, {
466
- lifecycle: "cleanup",
467
- operations: plan.operations
468
- }), /* @__PURE__ */ jsx(Outcome$2, { state })]
469
- }),
470
- /* @__PURE__ */ jsx("div", {
471
- "data-domainkit-part": "dialog-footer",
472
- children: busy ? /* @__PURE__ */ jsx("button", {
473
- "data-domainkit-part": "cleanup-apply",
474
- disabled: true,
475
- type: "button",
476
- children: state._tag === "Cleaning" ? messages.cleaningDns : messages.planningCleanup
477
- }) : state._tag === "Review" && plan !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(AlertDialog.Close, {
478
- "data-domainkit-part": "dialog-cancel",
479
- children: messages.cancel
480
- }), /* @__PURE__ */ jsx("button", {
481
- "data-domainkit-part": "cleanup-apply",
482
- disabled: plan.operations.some((operation) => operation._tag === "Blocked"),
483
- onClick: () => controller.apply(),
484
- type: "button",
485
- children: messages.applyCleanup
486
- })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(AlertDialog.Close, {
487
- "data-domainkit-part": "dialog-cancel",
488
- children: messages.close
489
- }), /* @__PURE__ */ jsx("button", {
490
- "data-domainkit-part": "cleanup-retry",
491
- onClick: () => controller.retry(),
492
- type: "button",
493
- children: messages.retry
494
- })] })
495
- })
496
- ]
497
- })]
498
- })]
499
- });
500
- }
501
- function Flow$3({ connection, onCleaned, receiptId, trigger, ...props }) {
502
- const controller = useController$3(connection, receiptId, onCleaned);
503
- const state = controller.state;
504
- return /* @__PURE__ */ jsx(Root$4, {
505
- status: state._tag,
506
- ...props,
507
- children: /* @__PURE__ */ jsx(Dialog$3, {
508
- controller,
509
- ...trigger === void 0 ? {} : { trigger }
510
- })
511
- });
546
+ };
547
+ }, []);
512
548
  }
513
549
  //#endregion
514
- //#region src/provider.tsx
515
- var provider_exports = /* @__PURE__ */ __exportAll({ Mark: () => Mark });
516
- const logoHosts = {
517
- amazonroute53: "route53.com",
518
- aws: "amazonaws.com",
519
- bluehost: "bluehost.com",
520
- cloudflare: "cloudflare.com",
521
- digitalocean: "digitalocean.com",
522
- dnsimple: "dnsimple.com",
523
- dreamhost: "dreamhost.com",
524
- dynadot: "dynadot.com",
525
- gandi: "gandi.net",
526
- godaddy: "godaddy.com",
527
- google: "cloud.google.com",
528
- googlecloud: "cloud.google.com",
529
- googleclouddns: "cloud.google.com",
530
- hostgator: "hostgator.com",
531
- hostinger: "hostinger.com",
532
- hover: "hover.com",
533
- ionos: "ionos.com",
534
- namecheap: "namecheap.com",
535
- namecom: "name.com",
536
- netlify: "netlify.com",
537
- ns1: "ns1.com",
538
- ovh: "ovh.com",
539
- porkbun: "porkbun.com",
540
- route53: "route53.com",
541
- spaceship: "spaceship.com",
542
- squarespace: "squarespace.com",
543
- vercel: "vercel.com"
544
- };
545
- const logoKey = (providerId) => providerId.trim().toLowerCase().replace(/[^a-z0-9]+/g, "");
546
- const logoHost = (providerId) => logoHosts[logoKey(providerId)];
547
- const letterMark = (provider) => provider.name.trim().charAt(0).toUpperCase() || "?";
548
- function DefaultMark({ provider }) {
549
- const host = logoHost(provider.id);
550
- const [failed, setFailed] = useState(false);
551
- if (host === void 0 || failed) return letterMark(provider);
552
- return /* @__PURE__ */ jsx("img", {
553
- alt: "",
554
- height: 32,
555
- onError: () => setFailed(true),
556
- referrerPolicy: "no-referrer",
557
- src: `https://integrations.sh/logo/${host}?sz=64`,
558
- width: 32
550
+ //#region src/attempt.ts
551
+ const State$2 = Data.taggedEnum();
552
+ /** Reasons that mean the plan itself is gone: retrying the same step would fail the same way. */
553
+ const needsNewPlan = (error) => error.reason._tag === "Stale" || error.reason._tag === "Expired" || error.reason._tag === "Conflict";
554
+ function useAttempt(options) {
555
+ const { domain, done, group, key, onDone } = options;
556
+ const { emit } = useDomainKit();
557
+ const readOnly = useReadOnly();
558
+ const runner = useRunner();
559
+ const [state, setState] = useState(State$2.Idle());
560
+ const held = useRef({
561
+ approval: null,
562
+ key,
563
+ plan: null
559
564
  });
560
- }
561
- function Mark({ provider, ...props }) {
562
- const { marks } = useDomainKit();
563
- const replacement = marks[provider.id];
564
- const content = typeof replacement === "function" ? replacement(provider) : replacement ?? /* @__PURE__ */ jsx(DefaultMark, { provider }, provider.id);
565
- return usePart("span", props, { providerId: provider.id }, {
566
- "aria-label": provider.name,
567
- children: content,
568
- "data-domainkit-part": "provider-mark",
569
- "data-domainkit-provider": logoKey(provider.id),
570
- role: "img"
565
+ const lastCommand = useRef(null);
566
+ const build = useRef(options.plan);
567
+ useEffect(() => {
568
+ build.current = options.plan;
571
569
  });
572
- }
573
- //#endregion
574
- //#region src/connection.tsx
575
- var connection_exports = /* @__PURE__ */ __exportAll({
576
- AttachAction: () => AttachAction,
577
- Card: () => Card$1,
578
- CardActions: () => CardActions,
579
- CardIdentity: () => CardIdentity,
580
- Command: () => Command$2,
581
- ConnectTrigger: () => ConnectTrigger,
582
- Dialog: () => Dialog$2,
583
- DisconnectDialog: () => DisconnectDialog,
584
- Flow: () => Flow$2,
585
- IntegrationAction: () => IntegrationAction,
586
- OAuthAction: () => OAuthAction,
587
- RetryAction: () => RetryAction$1,
588
- Root: () => Root$3,
589
- State: () => State$2,
590
- Status: () => Status$2,
591
- TokenAction: () => TokenAction,
592
- Trigger: () => Trigger,
593
- targetLabel: () => targetLabel,
594
- useController: () => useController$2,
595
- useModel: () => useModel$2
596
- });
597
- const State$2 = Data.taggedEnum();
598
- const sameTarget = (left, right) => left.accountId === right.accountId && left.accountKind === right.accountKind && left.zoneId === right.zoneId && left.zoneName === right.zoneName;
599
- const Command$2 = Data.taggedEnum();
600
- function useModel$2(domain) {
601
- const { emit, navigate, runtime } = useDomainKit();
602
- return useMemo(() => {
603
- const actionState = Atom.make(void 0);
604
- const inspect = runtime.atom(Effect.flatMap(Transport.Service, (transport) => transport.connection.inspect({ domain })));
605
- const state = Atom.make((get) => {
606
- const action = get(actionState);
607
- if (action !== void 0) return action;
608
- const inspection = get(inspect);
609
- return AsyncResult.matchWithError(inspection, {
610
- onDefect: () => failureFromDefect("connection.inspect", "Provider detection failed"),
611
- onError: failureFromError,
612
- onInitial: () => State$2.Loading(),
613
- onSuccess: ({ value }) => value
570
+ const [current, setCurrent] = useState(key);
571
+ if (current !== key) {
572
+ setCurrent(key);
573
+ runner.cancel();
574
+ held.current = {
575
+ approval: null,
576
+ key,
577
+ plan: null
578
+ };
579
+ lastCommand.current = null;
580
+ setState(State$2.Idle());
581
+ }
582
+ const onFailure = useCallback((error) => {
583
+ setState(State$2.Failure({ error }));
584
+ emit(Event.Failed({
585
+ domain,
586
+ error
587
+ }));
588
+ }, [domain, emit]);
589
+ const applyWith = useCallback((plan, approval) => {
590
+ if (group === void 0 || held.current.key !== key) return;
591
+ const command = () => {
592
+ setState(State$2.Applying({
593
+ approval,
594
+ plan
595
+ }));
596
+ runner.run(group.apply(approval.id), {
597
+ onFailure,
598
+ onSuccess: (receipt) => {
599
+ setState(State$2.Applied({
600
+ plan,
601
+ receipt
602
+ }));
603
+ emit(done(receipt));
604
+ onDone?.(receipt);
605
+ }
614
606
  });
615
- });
616
- return {
617
- command: runtime.fn()((input, get) => {
618
- if (input._tag === "Retry") {
619
- get.set(actionState, void 0);
620
- get.refresh(inspect);
621
- return Effect.void;
607
+ };
608
+ lastCommand.current = {
609
+ key,
610
+ run: command
611
+ };
612
+ command();
613
+ }, [
614
+ done,
615
+ emit,
616
+ group,
617
+ key,
618
+ onDone,
619
+ onFailure,
620
+ runner
621
+ ]);
622
+ const buildPlan = useCallback(() => {
623
+ const effect = build.current();
624
+ if (effect === null) return;
625
+ const command = () => {
626
+ setState(State$2.Planning());
627
+ runner.run(effect, {
628
+ onFailure,
629
+ onSuccess: (plan) => {
630
+ if (held.current.key !== key) return;
631
+ held.current = {
632
+ approval: null,
633
+ key,
634
+ plan
635
+ };
636
+ setState(State$2.Planned({ plan }));
622
637
  }
623
- const snapshot = get(state);
624
- if (input._tag === "Detach") {
625
- if (snapshot._tag !== "Connected") return Effect.void;
626
- get.set(actionState, State$2.Detaching({ snapshot }));
627
- return Effect.flatMap(Transport.Service, (transport) => Effect.flatMap(transport.connection.detach({
628
- attachmentId: snapshot.attachment.id,
629
- preserveDns: true
630
- }), (result) => {
631
- const detached = Transport.Disconnected({
632
- domain: snapshot.attachment.domain,
633
- provider: snapshot.provider,
634
- reusableConnections: []
635
- });
636
- return Effect.flatMap(Effect.sync(() => {
637
- get.set(actionState, detached);
638
- emit(Event.DomainDetached({
639
- connection: snapshot,
640
- result
641
- }));
642
- }), () => transport.connection.inspect({ domain }).pipe(Effect.catch(() => Effect.succeed(detached)), Effect.tap((refreshed) => Effect.sync(() => get.set(actionState, refreshed)))));
643
- })).pipe(Effect.catch((error) => Effect.sync(() => get.set(actionState, failureFromError(error)))), Effect.asVoid);
638
+ });
639
+ };
640
+ lastCommand.current = {
641
+ key,
642
+ run: command
643
+ };
644
+ command();
645
+ }, [
646
+ key,
647
+ onFailure,
648
+ runner
649
+ ]);
650
+ const approve = useCallback((operationIds) => {
651
+ const plan = held.current.plan;
652
+ if (group === void 0 || plan === null || held.current.key !== key) return;
653
+ const command = () => {
654
+ setState(State$2.Approving({ plan }));
655
+ runner.run(group.approve({
656
+ planId: plan.id,
657
+ ...operationIds === void 0 ? {} : { operationIds }
658
+ }), {
659
+ onFailure,
660
+ onSuccess: (approval) => {
661
+ if (held.current.key !== key) return;
662
+ held.current = {
663
+ approval,
664
+ key,
665
+ plan
666
+ };
667
+ applyWith(plan, approval);
644
668
  }
645
- if (snapshot._tag !== "Disconnected") return Effect.void;
646
- const complete = (request, source) => request.pipe(Effect.tap((result) => Effect.sync(() => {
647
- if (result._tag === "Redirect") {
648
- get.set(actionState, State$2.Redirecting({ snapshot }));
649
- navigate(result.authorizationUrl);
650
- } else {
651
- get.set(actionState, result);
652
- emit(Event.ConnectionEstablished({
653
- connection: result,
654
- source
655
- }));
656
- }
657
- })), Effect.catch((error) => Effect.sync(() => get.set(actionState, failureFromError(error)))), Effect.asVoid);
658
- if (input._tag === "Attach") {
659
- const reusableConnection = snapshot.reusableConnections.find(({ connection }) => connection.id === input.connectionId);
660
- if (reusableConnection === void 0) return Effect.void;
661
- const target = reusableConnection.targets.find((candidate) => sameTarget(candidate, input.target));
662
- if (target === void 0) return Effect.void;
663
- get.set(actionState, State$2.Submitting({ snapshot }));
664
- return complete(Effect.flatMap(Transport.Service, (transport) => transport.connection.attach({
665
- connectionId: input.connectionId,
666
- domain,
667
- target
668
- })), "attach");
669
+ });
670
+ };
671
+ lastCommand.current = {
672
+ key,
673
+ run: command
674
+ };
675
+ command();
676
+ }, [
677
+ applyWith,
678
+ group,
679
+ key,
680
+ onFailure,
681
+ runner
682
+ ]);
683
+ const reject = useCallback((reason) => {
684
+ const plan = held.current.plan;
685
+ if (group === void 0 || plan === null || held.current.key !== key) return;
686
+ const command = () => {
687
+ setState(State$2.Rejecting({ plan }));
688
+ runner.run(group.reject({
689
+ planId: plan.id,
690
+ ...reason === void 0 ? {} : { reason }
691
+ }), {
692
+ onFailure,
693
+ onSuccess: (attempt) => {
694
+ setState(State$2.Rejected({
695
+ attempt,
696
+ plan
697
+ }));
698
+ emit(Event.Declined({
699
+ attempt,
700
+ domain
701
+ }));
669
702
  }
670
- get.set(actionState, State$2.Submitting({ snapshot }));
671
- if (input._tag === "Connect") return complete(Effect.flatMap(Transport.Service, (transport) => transport.connection.connect({
672
- domain,
673
- method: input.method,
674
- providerId: snapshot.provider.id
675
- })), "connect");
676
- return Effect.void;
677
- }),
678
- state
703
+ });
679
704
  };
705
+ lastCommand.current = {
706
+ key,
707
+ run: command
708
+ };
709
+ command();
680
710
  }, [
681
711
  domain,
682
712
  emit,
683
- navigate,
684
- runtime
713
+ group,
714
+ key,
715
+ onFailure,
716
+ runner
717
+ ]);
718
+ const apply = useCallback(() => {
719
+ const { approval, plan } = held.current;
720
+ if (approval === null || plan === null) return;
721
+ applyWith(plan, approval);
722
+ }, [applyWith]);
723
+ const retry = useCallback(() => {
724
+ if (readOnly) return;
725
+ if (state._tag === "Failure" && needsNewPlan(state.error)) {
726
+ held.current = {
727
+ approval: null,
728
+ key,
729
+ plan: null
730
+ };
731
+ buildPlan();
732
+ return;
733
+ }
734
+ const command = lastCommand.current;
735
+ if (command === null || command.key !== key) buildPlan();
736
+ else command.run();
737
+ }, [
738
+ buildPlan,
739
+ key,
740
+ readOnly,
741
+ state
685
742
  ]);
686
- }
687
- function useController$2(domain) {
688
- const model = useModel$2(domain);
689
- const state = useAtomValue(model.state);
690
- const execute = useAtomSet(model.command);
691
743
  return {
692
- attach: (connection, target) => execute(Command$2.Attach({
693
- connectionId: connection.connection.id,
694
- target
695
- })),
696
- connect: (method) => execute(Command$2.Connect({ method })),
697
- detach: () => execute(Command$2.Detach()),
698
- retry: () => execute(Command$2.Retry()),
744
+ apply,
745
+ approve,
746
+ plan: buildPlan,
747
+ reject,
748
+ reset: useCallback(() => {
749
+ runner.cancel();
750
+ held.current = {
751
+ approval: null,
752
+ key,
753
+ plan: null
754
+ };
755
+ lastCommand.current = null;
756
+ setState(State$2.Idle());
757
+ }, [key, runner]),
758
+ retry,
699
759
  state
700
760
  };
701
761
  }
702
- function Root$3({ children, status = "Loading", ...props }) {
703
- return usePart("div", props, { status }, {
704
- children,
705
- "data-domainkit-part": "connection-root"
706
- });
707
- }
708
- function Status$2({ children, state, ...props }) {
709
- const { messages } = useDomainKit();
710
- const text = state._tag === "Loading" ? messages.detectingProvider : state._tag === "Connected" ? messages.connected(state.provider.name) : state._tag === "Detaching" ? messages.detaching : state._tag === "Unsupported" ? messages.automaticUnavailable : state._tag === "Failure" ? state.message : state._tag === "Redirecting" ? messages.openingAuthorization : state._tag === "Submitting" ? messages.connecting : messages.providerAvailable(state.provider.name);
711
- return usePart("div", props, { status: state._tag }, {
712
- children: children ?? text,
713
- "data-domainkit-part": "connection-status",
714
- "data-state": state._tag
715
- });
716
- }
717
- function Trigger({ children, ...props }) {
718
- return /* @__PURE__ */ jsx(Dialog.Trigger, {
719
- "data-domainkit-part": "connection-trigger",
720
- ...props,
721
- children
722
- });
723
- }
724
- function ConnectTrigger({ children, provider, ...props }) {
725
- const { messages } = useDomainKit();
726
- return /* @__PURE__ */ jsx(Trigger, {
727
- ...props,
728
- "data-domainkit-recipe": "connect",
729
- children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Mark, {
730
- "aria-hidden": "true",
731
- provider
732
- }), messages.connectProvider(provider.name)] })
733
- });
734
- }
735
- function OAuthAction({ controller, label, ...props }) {
736
- return usePart("button", props, { authentication: "oauth" }, {
737
- children: label,
738
- "data-domainkit-part": "oauth-connect",
739
- disabled: controller.state._tag === "Submitting" || controller.state._tag === "Redirecting",
740
- onClick: () => controller.connect(Transport.Method.OAuth()),
741
- type: "button"
742
- });
743
- }
744
- function IntegrationAction({ controller, label, ...props }) {
745
- return usePart("button", props, { authentication: "integration" }, {
746
- children: label,
747
- "data-domainkit-part": "integration-connect",
748
- disabled: controller.state._tag === "Submitting" || controller.state._tag === "Redirecting",
749
- onClick: () => controller.connect(Transport.Method.Integration()),
750
- type: "button"
751
- });
752
- }
753
- function AttachAction({ connection, controller, label, target, ...props }) {
754
- return usePart("button", props, { authentication: "attach" }, {
755
- children: label,
756
- "data-domainkit-part": "attach-target",
757
- disabled: controller.state._tag === "Submitting" || controller.state._tag === "Redirecting",
758
- onClick: () => controller.attach(connection, target),
759
- type: "button"
760
- });
761
- }
762
- function TokenAction({ controller, method, ...props }) {
763
- const { messages } = useDomainKit();
764
- const pending = controller.state._tag === "Submitting" || controller.state._tag === "Redirecting";
765
- const [token, setToken] = useState("");
766
- const [parameters, setParameters] = useState({});
767
- const submit = (event) => {
768
- event.preventDefault();
769
- if (token.length > 0) {
770
- const populated = Object.fromEntries(Object.entries(parameters).filter(([, value]) => value.length > 0));
771
- controller.connect(Transport.Method.Token({
772
- token,
773
- ...Object.keys(populated).length === 0 ? {} : { parameters: populated }
774
- }));
775
- }
776
- };
777
- const missingRequiredParameter = method.parameters?.some((parameter) => parameter.required === true && !parameters[parameter.key]);
778
- return usePart("form", props, { authentication: "token" }, {
779
- children: /* @__PURE__ */ jsxs(Fragment, { children: [
780
- method.parameters?.map((parameter) => /* @__PURE__ */ jsxs("label", { children: [
781
- parameter.label,
782
- /* @__PURE__ */ jsx("input", {
783
- name: parameter.key,
784
- disabled: pending,
785
- onChange: (event) => {
786
- const value = event.currentTarget.value;
787
- setParameters((current) => ({
788
- ...current,
789
- [parameter.key]: value
790
- }));
791
- },
792
- placeholder: parameter.placeholder,
793
- required: parameter.required,
794
- type: "text",
795
- value: parameters[parameter.key] ?? ""
796
- }),
797
- parameter.description === void 0 ? null : /* @__PURE__ */ jsx("small", { children: parameter.description })
798
- ] }, parameter.key)),
799
- /* @__PURE__ */ jsxs("label", { children: [messages.tokenLabel, /* @__PURE__ */ jsx("input", {
800
- autoComplete: "off",
801
- disabled: pending,
802
- name: "token",
803
- onChange: (event) => setToken(event.currentTarget.value),
804
- placeholder: method.placeholder,
805
- type: "password",
806
- value: token
807
- })] }),
808
- /* @__PURE__ */ jsx("button", {
809
- disabled: pending || token.length === 0 || missingRequiredParameter === true,
810
- type: "submit",
811
- children: method.label
812
- })
813
- ] }),
814
- "data-domainkit-part": "token-connect",
815
- onSubmit: submit
816
- });
817
- }
818
- const targetLabel = (target) => `${target.evidence?.accountName ?? target.accountId} · ${target.zoneName}`;
819
- function Dialog$2({ controller, snapshot }) {
820
- const { colorScheme, messages, portalContainer, themeStyle } = useDomainKit();
821
- const pending = controller.state._tag === "Submitting" || controller.state._tag === "Redirecting";
822
- const integrationMethods = snapshot.provider.authentication.filter((method) => method._tag === "Integration");
823
- const oauthMethods = snapshot.provider.authentication.filter((method) => method._tag === "OAuth");
824
- const tokenMethods = snapshot.provider.authentication.filter((method) => method._tag === "Token");
825
- const hasProviderAccountPath = integrationMethods.length > 0 || oauthMethods.length > 0 || snapshot.reusableConnections.length > 0;
826
- return /* @__PURE__ */ jsxs(Dialog.Portal, {
827
- container: portalContainer,
828
- children: [/* @__PURE__ */ jsx(Dialog.Backdrop, {
829
- "data-color-scheme": colorScheme,
830
- "data-domainkit-part": "dialog-backdrop",
831
- "data-domainkit-root": "",
832
- style: themeStyle
833
- }), /* @__PURE__ */ jsxs(Dialog.Popup, {
834
- "data-domainkit-part": "connection-dialog",
835
- "data-domainkit-root": "",
836
- "data-color-scheme": colorScheme,
837
- style: themeStyle,
838
- children: [
839
- /* @__PURE__ */ jsxs("div", {
840
- "data-domainkit-part": "dialog-header",
841
- children: [
842
- /* @__PURE__ */ jsx(Mark, { provider: snapshot.provider }),
843
- /* @__PURE__ */ jsxs("div", {
844
- "data-domainkit-part": "dialog-heading",
845
- children: [/* @__PURE__ */ jsx(Dialog.Title, {
846
- "data-domainkit-part": "dialog-title",
847
- children: messages.dialogTitle(snapshot.provider.name)
848
- }), /* @__PURE__ */ jsx(Dialog.Description, {
849
- "data-domainkit-part": "dialog-description",
850
- children: messages.dialogDescription(snapshot.domain)
851
- })]
852
- }),
853
- pending ? null : /* @__PURE__ */ jsx(Dialog.Close, {
854
- "aria-label": messages.close,
855
- "data-domainkit-part": "dialog-close",
856
- children: "×"
857
- })
858
- ]
859
- }),
860
- /* @__PURE__ */ jsxs("div", {
861
- "data-domainkit-part": "dialog-body",
862
- children: [
863
- hasProviderAccountPath ? /* @__PURE__ */ jsxs("div", {
864
- "data-domainkit-part": "provider-authentication",
865
- children: [
866
- integrationMethods.map((method) => /* @__PURE__ */ jsx(IntegrationAction, {
867
- controller,
868
- label: method.label
869
- }, method._tag)),
870
- oauthMethods.map((method) => /* @__PURE__ */ jsx(OAuthAction, {
871
- controller,
872
- label: method.label
873
- }, method._tag)),
874
- snapshot.reusableConnections.map((connection) => /* @__PURE__ */ jsxs("section", {
875
- "data-connection-id": connection.connection.id,
876
- "data-domainkit-part": "reusable-connection",
877
- children: [/* @__PURE__ */ jsxs("div", {
878
- "data-domainkit-part": "reusable-connection-heading",
879
- children: [/* @__PURE__ */ jsx("strong", { children: messages.existingConnection }), /* @__PURE__ */ jsx("span", { children: connection.connection.providerId })]
880
- }), /* @__PURE__ */ jsx("div", {
881
- "data-domainkit-part": "target-list",
882
- "data-state": connection.targets.length === 0 ? "unavailable" : connection.targets.length === 1 ? "unique" : "ambiguous",
883
- children: connection.targets.length === 0 ? /* @__PURE__ */ jsx("p", {
884
- "data-domainkit-part": "target-unavailable",
885
- children: messages.targetUnavailable
886
- }) : connection.targets.map((target) => /* @__PURE__ */ jsx(AttachAction, {
887
- connection,
888
- controller,
889
- label: messages.attachTarget(targetLabel(target)),
890
- target
891
- }, `${target.accountId}:${target.zoneId}`))
892
- })]
893
- }, connection.connection.id))
894
- ]
895
- }) : null,
896
- hasProviderAccountPath && tokenMethods.length > 0 ? /* @__PURE__ */ jsx("div", {
897
- "aria-label": messages.authenticationAlternative,
898
- "data-domainkit-part": "authentication-separator",
899
- role: "separator",
900
- children: /* @__PURE__ */ jsx("span", {
901
- "aria-hidden": "true",
902
- children: messages.authenticationAlternative
903
- })
904
- }) : null,
905
- tokenMethods.map((method) => /* @__PURE__ */ jsx(TokenAction, {
906
- controller,
907
- method
908
- }, method._tag))
909
- ]
910
- }),
911
- pending ? null : /* @__PURE__ */ jsx("div", {
912
- "data-domainkit-part": "dialog-footer",
913
- children: /* @__PURE__ */ jsx(Dialog.Close, {
914
- "data-domainkit-part": "dialog-cancel",
915
- children: messages.cancel
916
- })
917
- })
918
- ]
919
- })]
920
- });
921
- }
922
- function Flow$2({ domain, ...props }) {
923
- const controller = useController$2(domain);
924
- const state = controller.state;
925
- const snapshot = state._tag === "Disconnected" ? state : state._tag === "Submitting" || state._tag === "Redirecting" ? state.snapshot : void 0;
926
- return /* @__PURE__ */ jsxs(Root$3, {
927
- ...props,
928
- status: state._tag,
929
- children: [
930
- /* @__PURE__ */ jsx(Status$2, { state }),
931
- state._tag === "Failure" && state.retry !== "never" ? /* @__PURE__ */ jsx(RetryAction$1, {
932
- controller,
933
- kind: state.retry === "after-user-action" ? "after-user-action" : "retry"
934
- }) : null,
935
- snapshot === void 0 ? null : /* @__PURE__ */ jsxs(Dialog.Root, {
936
- onOpenChange: (open, eventDetails) => {
937
- if (!open && (state._tag === "Submitting" || state._tag === "Redirecting")) eventDetails.cancel();
938
- },
939
- children: [/* @__PURE__ */ jsx(ConnectTrigger, { provider: snapshot.provider }), /* @__PURE__ */ jsx(Dialog$2, {
940
- controller,
941
- snapshot
942
- })]
943
- })
944
- ]
945
- });
946
- }
947
- function RetryAction$1({ controller, kind = "retry", ...props }) {
948
- const { messages } = useDomainKit();
949
- return usePart("button", props, { status: "Failure" }, {
950
- children: kind === "after-user-action" ? messages.checkAgain : messages.retry,
951
- "data-domainkit-part": "connection-retry",
952
- onClick: controller.retry,
953
- type: "button"
954
- });
955
- }
956
- function Card$1({ status, ...props }) {
957
- return usePart("div", props, { status }, {
958
- "data-domainkit-part": "connected-card",
959
- "data-state": status
960
- });
961
- }
962
- function CardIdentity({ status, ...props }) {
963
- return usePart("div", props, { status }, {
964
- "data-domainkit-part": "connected-identity",
965
- "data-state": status
966
- });
967
- }
968
- function CardActions({ status, ...props }) {
969
- return usePart("div", props, { status }, {
970
- "data-domainkit-part": "connected-actions",
971
- "data-state": status
972
- });
973
- }
974
- function DisconnectDialog({ connection, controller, onOpenChange, open: openProp, trigger }) {
975
- const { colorScheme, messages, portalContainer, themeStyle } = useDomainKit();
976
- const [internalOpen, setInternalOpen] = useState(false);
977
- const open = openProp ?? internalOpen;
978
- const setOpen = (nextOpen) => {
979
- if (openProp === void 0) setInternalOpen(nextOpen);
980
- onOpenChange?.(nextOpen);
981
- };
982
- const detaching = controller.state._tag === "Detaching";
983
- return /* @__PURE__ */ jsxs(AlertDialog.Root, {
984
- onOpenChange: (nextOpen, eventDetails) => {
985
- if (!nextOpen && detaching) {
986
- eventDetails.cancel();
987
- return;
988
- }
989
- setOpen(nextOpen);
990
- },
991
- open,
992
- children: [trigger === null ? null : /* @__PURE__ */ jsx(AlertDialog.Trigger, {
993
- "data-domainkit-part": "disconnect-trigger",
994
- ...trigger === void 0 ? {} : { render: trigger },
995
- children: messages.disconnectDomain
996
- }), /* @__PURE__ */ jsxs(AlertDialog.Portal, {
997
- container: portalContainer,
998
- children: [/* @__PURE__ */ jsx(AlertDialog.Backdrop, {
999
- "data-color-scheme": colorScheme,
1000
- "data-domainkit-part": "dialog-backdrop",
1001
- "data-domainkit-root": "",
1002
- style: themeStyle
1003
- }), /* @__PURE__ */ jsxs(AlertDialog.Popup, {
1004
- "data-color-scheme": colorScheme,
1005
- "data-domainkit-part": "disconnect-dialog",
1006
- "data-domainkit-root": "",
1007
- style: themeStyle,
1008
- children: [/* @__PURE__ */ jsxs("div", {
1009
- "data-domainkit-part": "dialog-header",
1010
- children: [/* @__PURE__ */ jsxs("div", {
1011
- "data-domainkit-part": "dialog-heading",
1012
- children: [/* @__PURE__ */ jsx(AlertDialog.Title, {
1013
- "data-domainkit-part": "dialog-title",
1014
- children: messages.disconnectTitle(connection.provider.name)
1015
- }), /* @__PURE__ */ jsx(AlertDialog.Description, {
1016
- "data-domainkit-part": "dialog-description",
1017
- children: messages.disconnectConsent
1018
- })]
1019
- }), detaching ? null : /* @__PURE__ */ jsx(AlertDialog.Close, {
1020
- "aria-label": messages.close,
1021
- "data-domainkit-part": "dialog-close",
1022
- children: "×"
1023
- })]
1024
- }), /* @__PURE__ */ jsxs("div", {
1025
- "data-domainkit-part": "dialog-footer",
1026
- children: [detaching ? null : /* @__PURE__ */ jsx(AlertDialog.Close, {
1027
- "data-domainkit-part": "dialog-cancel",
1028
- children: messages.cancel
1029
- }), /* @__PURE__ */ jsx("button", {
1030
- "data-domainkit-part": "disconnect-action",
1031
- disabled: detaching,
1032
- onClick: () => controller.detach(),
1033
- type: "button",
1034
- children: detaching ? messages.detaching : messages.disconnectDomain
1035
- })]
1036
- })]
1037
- })]
1038
- })]
1039
- });
1040
- }
1041
762
  //#endregion
1042
763
  //#region src/records.tsx
1043
764
  var records_exports = /* @__PURE__ */ __exportAll({
1044
- Body: () => Body,
765
+ Body: () => Body$1,
1045
766
  Caption: () => Caption,
1046
- Card: () => Card,
767
+ Card: () => Card$1,
1047
768
  CardContent: () => CardContent,
1048
769
  CardHeader: () => CardHeader,
1049
770
  CardTitle: () => CardTitle,
@@ -1052,35 +773,22 @@ var records_exports = /* @__PURE__ */ __exportAll({
1052
773
  Footer: () => Footer,
1053
774
  Head: () => Head,
1054
775
  Header: () => Header,
1055
- Priority: () => Priority,
1056
- Root: () => Root$2,
776
+ Purpose: () => Purpose,
777
+ Root: () => Root$5,
1057
778
  Row: () => Row,
1058
- Status: () => Status$1,
779
+ Status: () => Status$5,
1059
780
  Table: () => Table,
1060
781
  Value: () => Value,
1061
782
  ZoneFile: () => ZoneFile,
1062
783
  copyText: () => copyText,
1063
784
  downloadZoneFile: () => downloadZoneFile,
785
+ identity: () => identity,
786
+ requirementsKey: () => requirementsKey,
787
+ statusOf: () => statusOf,
1064
788
  toZoneFile: () => toZoneFile,
1065
789
  useCopy: () => useCopy
1066
790
  });
1067
- const copyText = (value) => {
1068
- if (typeof navigator === "undefined" || navigator.clipboard === void 0) return Promise.resolve(false);
1069
- return navigator.clipboard.writeText(value).then(() => true, () => false);
1070
- };
1071
- const downloadText = (filename, contents) => {
1072
- if (typeof document === "undefined") return;
1073
- const blob = new Blob([contents], { type: "text/plain;charset=utf-8" });
1074
- const url = URL.createObjectURL(blob);
1075
- const anchor = document.createElement("a");
1076
- anchor.download = filename;
1077
- anchor.href = url;
1078
- document.body.append(anchor);
1079
- anchor.click();
1080
- anchor.remove();
1081
- URL.revokeObjectURL(url);
1082
- };
1083
- const absoluteDomainName = (value) => value.endsWith(".") ? value : `${value}.`;
791
+ const absolute = (value) => value.endsWith(".") ? value : `${value}.`;
1084
792
  const textEncoder = new TextEncoder();
1085
793
  const escapeTxtCharacter = (character) => {
1086
794
  if (character === "\"" || character === "\\") return `\\${character}`;
@@ -1088,6 +796,7 @@ const escapeTxtCharacter = (character) => {
1088
796
  if (codePoint !== void 0 && (codePoint < 32 || codePoint === 127)) return `\\${codePoint.toString().padStart(3, "0")}`;
1089
797
  return character;
1090
798
  };
799
+ /** TXT strings are capped at 255 bytes each, so long values are split and quoted per chunk. */
1091
800
  const txtValue = (value) => {
1092
801
  const chunks = [];
1093
802
  let chunk = "";
@@ -1105,29 +814,38 @@ const txtValue = (value) => {
1105
814
  chunks.push(chunk);
1106
815
  return chunks.map((part) => `"${part}"`).join(" ");
1107
816
  };
1108
- const srvValue = (value) => {
1109
- const fields = value.trim().split(/\s+/);
1110
- if (fields.length !== 3) throw new Error(`Invalid SRV value: ${value}`);
1111
- const [weight, port, target] = fields;
1112
- if (weight === void 0 || port === void 0 || target === void 0) throw new Error(`Invalid SRV value: ${value}`);
1113
- return `${weight} ${port} ${absoluteDomainName(target)}`;
1114
- };
1115
- const zoneValue = (record) => {
1116
- switch (record.type.toUpperCase()) {
1117
- case "CNAME":
1118
- case "MX":
1119
- case "NS":
1120
- case "PTR": return absoluteDomainName(record.value);
1121
- case "SRV": return srvValue(record.value);
817
+ /** The RR type as a zone file spells it. The tag and the type name are the same string. */
818
+ const rrType = (record) => record._tag;
819
+ const zoneData = (record) => {
820
+ switch (record._tag) {
821
+ case "A":
822
+ case "AAAA": return record.address;
823
+ case "CNAME": return absolute(record.target);
824
+ case "NS": return absolute(record.nameserver);
1122
825
  case "TXT": return txtValue(record.value);
1123
- default: return record.value;
826
+ case "MX": return `${record.priority} ${absolute(record.exchange)}`;
827
+ case "CAA": return `${record.flags} ${record.tag} "${record.value}"`;
828
+ case "SRV": return `${record.priority} ${record.weight} ${record.port} ${absolute(record.target)}`;
1124
829
  }
1125
830
  };
1126
- function leafPart(defaultTagName, part) {
1127
- return function Leaf(props) {
1128
- return usePart(defaultTagName, props, {}, { "data-domainkit-part": part });
1129
- };
1130
- }
831
+ const toZoneFile = (records) => `${records.map((record) => `${absolute(record.name)}${record.ttl === null ? "" : ` ${record.ttl}`} IN ${rrType(record)} ${zoneData(record)}`).join("\n")}\n`;
832
+ const downloadText = (filename, contents) => {
833
+ if (typeof document === "undefined") return;
834
+ const blob = new Blob([contents], { type: "text/plain;charset=utf-8" });
835
+ const url = URL.createObjectURL(blob);
836
+ const anchor = document.createElement("a");
837
+ anchor.download = filename;
838
+ anchor.href = url;
839
+ document.body.append(anchor);
840
+ anchor.click();
841
+ anchor.remove();
842
+ URL.revokeObjectURL(url);
843
+ };
844
+ const downloadZoneFile = (domain, records) => downloadText(`${domain}.txt`, toZoneFile(records));
845
+ const copyText = (value) => {
846
+ if (typeof navigator === "undefined" || navigator.clipboard === void 0) return Promise.resolve(false);
847
+ return navigator.clipboard.writeText(value).then(() => true, () => false);
848
+ };
1131
849
  function useCopy(value, resetAfter = 2e3) {
1132
850
  const [copied, setCopied] = useState(false);
1133
851
  const reset = useRef(void 0);
@@ -1144,279 +862,365 @@ function useCopy(value, resetAfter = 2e3) {
1144
862
  }
1145
863
  };
1146
864
  }
1147
- function CopyValue({ copiedIcon, copiedLabel = "Copied", copyIcon, copyLabel = "Copy", value, ...props }) {
865
+ function CopyValue({ value, ...props }) {
866
+ const { messages } = useDomainKit();
1148
867
  const icons = useIcons();
1149
- const controller = useCopy(value);
1150
- const copied = controller.copied;
868
+ const { copied, copy } = useCopy(value);
1151
869
  return usePart("span", props, { copied }, {
1152
870
  children: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("code", { children: value }), /* @__PURE__ */ jsxs("button", {
1153
- "aria-label": `${copied ? copiedLabel : copyLabel} ${value}`,
871
+ "aria-label": `${copied ? messages.copied : messages.copy} ${value}`,
1154
872
  "data-domainkit-part": "copy-action",
1155
- onClick: controller.copy,
873
+ onClick: copy,
1156
874
  type: "button",
1157
875
  children: [/* @__PURE__ */ jsx("span", {
1158
876
  "aria-hidden": "true",
1159
877
  "data-domainkit-part": "copy-glyph",
1160
878
  "data-icon": "",
1161
879
  "data-state": "idle",
1162
- children: copyIcon ?? icons.copy
880
+ children: icons.copy
1163
881
  }), /* @__PURE__ */ jsx("span", {
1164
882
  "aria-hidden": "true",
1165
883
  "data-domainkit-part": "copy-glyph",
1166
884
  "data-icon": "",
1167
885
  "data-state": "done",
1168
- children: copiedIcon ?? icons.copied
886
+ children: icons.copied
1169
887
  })]
1170
888
  })] }),
1171
889
  "data-copied": copied ? "" : void 0,
1172
890
  "data-domainkit-part": "copy-value"
1173
891
  });
1174
892
  }
1175
- function ZoneFile({ copiedIcon, copiedLabel = "Copied", copyIcon, copyLabel = "Copy zone", domain, downloadIcon, downloadLabel = "Download", records, ...props }) {
893
+ function ZoneFile({ domain, records, ...props }) {
894
+ const { messages } = useDomainKit();
1176
895
  const icons = useIcons();
1177
- const controller = useCopy(toZoneFile(records));
1178
- const copied = controller.copied;
896
+ const { copied, copy } = useCopy(toZoneFile(records));
1179
897
  return usePart("div", props, { count: records.length }, {
1180
898
  children: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("button", {
1181
- "aria-label": copied ? copiedLabel : copyLabel,
899
+ "aria-label": copied ? messages.copied : messages.copyZone,
1182
900
  "data-domainkit-part": "zone-copy",
1183
- onClick: controller.copy,
901
+ onClick: copy,
1184
902
  type: "button",
1185
903
  children: [/* @__PURE__ */ jsx("span", {
1186
904
  "aria-hidden": "true",
1187
905
  "data-icon": "inline-start",
1188
- children: copied ? copiedIcon ?? icons.copied : copyIcon ?? icons.copy
1189
- }), copied ? copiedLabel : copyLabel]
906
+ children: copied ? icons.copied : icons.copy
907
+ }), copied ? messages.copied : messages.copyZone]
1190
908
  }), /* @__PURE__ */ jsxs("button", {
1191
- "aria-label": downloadLabel,
909
+ "aria-label": messages.download,
1192
910
  "data-domainkit-part": "zone-download",
1193
911
  onClick: () => downloadZoneFile(domain, records),
1194
912
  type: "button",
1195
913
  children: [/* @__PURE__ */ jsx("span", {
1196
914
  "aria-hidden": "true",
1197
915
  "data-icon": "inline-start",
1198
- children: downloadIcon ?? icons.download
1199
- }), downloadLabel]
916
+ children: icons.download
917
+ }), messages.download]
1200
918
  })] }),
1201
919
  "data-domainkit-part": "zone-file"
1202
920
  });
1203
921
  }
1204
- function Status$1({ evidence, ...props }) {
1205
- return usePart("span", props, { status: evidence._tag }, {
1206
- children: evidence._tag,
922
+ /** The readiness this record reached, or `null` when nothing has observed it. */
923
+ const statusOf = (record, readiness) => readiness?.requirements.find((requirement) => DnsRecord.equals(requirement.record, record))?.status ?? null;
924
+ function Status$5({ status, ...props }) {
925
+ const { messages } = useDomainKit();
926
+ const icons = useIcons();
927
+ const glyph = status === "satisfied" ? icons.success : status === "unknown" ? icons.pending : icons.failure;
928
+ return usePart("span", props, { status }, {
929
+ children: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
930
+ "aria-hidden": "true",
931
+ "data-icon": "inline-start",
932
+ children: glyph
933
+ }), messages.requirementStatus(status)] }),
1207
934
  "data-domainkit-part": "record-status",
1208
- "data-status": evidence._tag
1209
- });
1210
- }
1211
- function Priority({ children, priority = 0, ...props }) {
1212
- return usePart("span", props, { priority }, {
1213
- children: children ?? `Priority ${priority}`,
1214
- "data-domainkit-part": "record-priority"
935
+ "data-status": status
1215
936
  });
1216
937
  }
1217
938
  const Header = leafPart("thead", "records-header");
1218
- const Body = leafPart("tbody", "records-body");
939
+ const Body$1 = leafPart("tbody", "records-body");
1219
940
  const Footer = leafPart("tfoot", "records-footer");
1220
941
  const Row = leafPart("tr", "records-row");
1221
942
  const Head = leafPart("th", "records-head");
1222
943
  const Cell = leafPart("td", "records-cell");
1223
944
  const Caption = leafPart("caption", "records-caption");
1224
945
  const Value = leafPart("span", "record-value");
946
+ const Purpose = leafPart("span", "record-purpose");
1225
947
  const CardHeader = leafPart("div", "record-card-head");
1226
948
  const CardTitle = leafPart("strong", "record-card-title");
1227
949
  const CardContent = leafPart("dl", "record-card-content");
1228
- function Root$2({ count = 0, ...props }) {
950
+ function Root$5({ count = 0, ...props }) {
1229
951
  return /* @__PURE__ */ jsx("div", {
1230
952
  "data-domainkit-part": "records-panel",
1231
953
  children: usePart("table", props, { count }, { "data-domainkit-part": "records-table" })
1232
954
  });
1233
955
  }
1234
- function Card({ copiedIcon, copyIcon, evidence, record, ...props }) {
1235
- const matching = evidence === void 0 ? [] : evidence.filter((item) => item.recordId === record.id);
1236
- return usePart("section", props, { recordId: record.id }, {
1237
- children: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(CardHeader, { children: [/* @__PURE__ */ jsx(CardTitle, { children: record.type }), matching.map((item, index) => /* @__PURE__ */ jsx(Status$1, { evidence: item }, `${item._tag}-${index}`))] }), /* @__PURE__ */ jsxs(CardContent, { children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: "Name" }), /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx(CopyValue, {
1238
- copiedIcon,
1239
- copyIcon,
1240
- value: record.name
1241
- }) })] }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: "Value" }), /* @__PURE__ */ jsxs("dd", { children: [/* @__PURE__ */ jsx(CopyValue, {
1242
- copiedIcon,
1243
- copyIcon,
1244
- value: record.value
1245
- }), record.priority === void 0 ? null : /* @__PURE__ */ jsx(Priority, { priority: record.priority })] })] })] })] }),
956
+ /** A stable React key for a requirement: type, name, and data identify a record. */
957
+ const identity = (record) => [
958
+ rrType(record),
959
+ record.name,
960
+ DnsRecord.data(record)
961
+ ].join(":");
962
+ /**
963
+ * A requirement set keyed by everything it carries, for memos that decide whether to re-send it.
964
+ * `identity` is deliberately narrower: it answers "is this the same record" for a React key, while
965
+ * a plan and an observation are built from `ttl`, `policy`, and `purpose` as well. `policy` in
966
+ * particular decides whether a name may hold anything else, so readiness turns on it.
967
+ */
968
+ const requirementsKey = (records) => JSON.stringify(records);
969
+ function Card$1({ readiness, record, ...props }) {
970
+ const { messages } = useDomainKit();
971
+ const status = statusOf(record, readiness);
972
+ return usePart("section", props, { record: identity(record) }, {
973
+ children: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(CardHeader, { children: [/* @__PURE__ */ jsx(CardTitle, { children: messages.recordType(record) }), status === null ? null : /* @__PURE__ */ jsx(Status$5, { status })] }), /* @__PURE__ */ jsxs(CardContent, { children: [
974
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: messages.headingName }), /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx(CopyValue, { value: record.name }) })] }),
975
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: messages.headingValue }), /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx(CopyValue, { value: DnsRecord.data(record) }) })] }),
976
+ record.purpose === void 0 ? null : /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: messages.headingPurpose }), /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx(Purpose, { children: record.purpose }) })] })
977
+ ] })] }),
1246
978
  "data-domainkit-part": "record-card"
1247
979
  });
1248
980
  }
1249
- function Table({ copiedIcon, copyIcon, evidence, records, ...props }) {
1250
- const status = evidence !== void 0;
1251
- return /* @__PURE__ */ jsxs(Root$2, {
981
+ /** The default records presentation: one row per requirement, with readiness when there is any. */
982
+ function Table({ caption, readiness, records, ...props }) {
983
+ const { messages } = useDomainKit();
984
+ const withStatus = readiness !== null && readiness !== void 0;
985
+ return /* @__PURE__ */ jsxs(Root$5, {
1252
986
  count: records.length,
1253
987
  ...props,
1254
- children: [/* @__PURE__ */ jsx(Header, { children: /* @__PURE__ */ jsxs(Row, { children: [
1255
- /* @__PURE__ */ jsx(Head, {
1256
- scope: "col",
1257
- children: "Type"
1258
- }),
1259
- /* @__PURE__ */ jsx(Head, {
1260
- scope: "col",
1261
- children: "Name"
1262
- }),
1263
- /* @__PURE__ */ jsx(Head, {
1264
- scope: "col",
1265
- children: "Value"
1266
- }),
1267
- status ? /* @__PURE__ */ jsx(Head, {
1268
- "data-column": "status",
1269
- scope: "col",
1270
- children: "Status"
1271
- }) : null
1272
- ] }) }), /* @__PURE__ */ jsx(Body, { children: records.map((record) => /* @__PURE__ */ jsxs(Row, { children: [
1273
- /* @__PURE__ */ jsx(Cell, { children: record.type }),
1274
- /* @__PURE__ */ jsx(Cell, { children: /* @__PURE__ */ jsx(CopyValue, {
1275
- copiedIcon,
1276
- copyIcon,
1277
- value: record.name
1278
- }) }),
1279
- /* @__PURE__ */ jsx(Cell, { children: /* @__PURE__ */ jsxs(Value, { children: [/* @__PURE__ */ jsx(CopyValue, {
1280
- copiedIcon,
1281
- copyIcon,
1282
- value: record.value
1283
- }), record.priority === void 0 ? null : /* @__PURE__ */ jsx(Priority, { priority: record.priority })] }) }),
1284
- status ? /* @__PURE__ */ jsx(Cell, {
1285
- "data-column": "status",
1286
- children: evidence.filter((item) => item.recordId === record.id).map((item, index) => /* @__PURE__ */ jsx(Status$1, { evidence: item }, `${item._tag}-${index}`))
1287
- }) : null
1288
- ] }, record.id)) })]
988
+ children: [
989
+ caption === void 0 ? null : /* @__PURE__ */ jsx(Caption, { children: caption }),
990
+ /* @__PURE__ */ jsx(Header, { children: /* @__PURE__ */ jsxs(Row, { children: [
991
+ /* @__PURE__ */ jsx(Head, {
992
+ scope: "col",
993
+ children: messages.headingType
994
+ }),
995
+ /* @__PURE__ */ jsx(Head, {
996
+ scope: "col",
997
+ children: messages.headingName
998
+ }),
999
+ /* @__PURE__ */ jsx(Head, {
1000
+ scope: "col",
1001
+ children: messages.headingValue
1002
+ }),
1003
+ withStatus ? /* @__PURE__ */ jsx(Head, {
1004
+ "data-column": "status",
1005
+ scope: "col",
1006
+ children: messages.headingStatus
1007
+ }) : null
1008
+ ] }) }),
1009
+ /* @__PURE__ */ jsx(Body$1, { children: records.map((record) => {
1010
+ const status = statusOf(record, readiness);
1011
+ return /* @__PURE__ */ jsxs(Row, { children: [
1012
+ /* @__PURE__ */ jsx(Cell, { children: messages.recordType(record) }),
1013
+ /* @__PURE__ */ jsx(Cell, { children: /* @__PURE__ */ jsx(CopyValue, { value: record.name }) }),
1014
+ /* @__PURE__ */ jsx(Cell, { children: /* @__PURE__ */ jsxs(Value, { children: [/* @__PURE__ */ jsx(CopyValue, { value: DnsRecord.data(record) }), record.purpose === void 0 ? null : /* @__PURE__ */ jsx(Purpose, { children: record.purpose })] }) }),
1015
+ withStatus ? /* @__PURE__ */ jsx(Cell, {
1016
+ "data-column": "status",
1017
+ children: status === null ? null : /* @__PURE__ */ jsx(Status$5, { status })
1018
+ }) : null
1019
+ ] }, identity(record));
1020
+ }) })
1021
+ ]
1289
1022
  });
1290
1023
  }
1291
- const toZoneFile = (records) => `${records.map((record) => `${absoluteDomainName(record.name)} IN ${record.type} ${record.priority === void 0 ? "" : `${record.priority} `}${zoneValue(record)}`).join("\n")}\n`;
1292
- const downloadZoneFile = (domain, records) => downloadText(`${domain}.txt`, toZoneFile(records));
1293
1024
  //#endregion
1294
- //#region src/provisioning.tsx
1295
- var provisioning_exports = /* @__PURE__ */ __exportAll({
1296
- Command: () => Command$1,
1297
- Dialog: () => Dialog$1,
1298
- Flow: () => Flow$1,
1299
- Outcome: () => Outcome$1,
1300
- RetryAction: () => RetryAction,
1301
- Root: () => Root$1,
1302
- State: () => State$1,
1303
- useController: () => useController$1,
1304
- useModel: () => useModel$1
1025
+ //#region src/operations.tsx
1026
+ var operations_exports = /* @__PURE__ */ __exportAll({
1027
+ Item: () => Item,
1028
+ Kind: () => Kind,
1029
+ List: () => List,
1030
+ Name: () => Name,
1031
+ Reason: () => Reason$1,
1032
+ Record: () => Record,
1033
+ Root: () => Root$4,
1034
+ Type: () => Type
1305
1035
  });
1306
- const State$1 = Data.taggedEnum();
1307
- const Command$1 = Data.taggedEnum();
1308
- function useModel$1(connection, records, onApplied) {
1309
- const { emit, runtime } = useDomainKit();
1310
- const recordKey = recordsIdentity(records);
1311
- const onAppliedRef = useRef(onApplied);
1312
- onAppliedRef.current = onApplied;
1313
- return useMemo(() => {
1314
- const state = Atom.make(State$1.Idle());
1315
- return {
1316
- command: runtime.fn()((command, get) => {
1317
- const current = get(state);
1318
- if (command._tag === "Dismiss") {
1319
- if (current._tag === "Planning" || current._tag === "Applying") return Effect.void;
1320
- get.set(state, State$1.Idle());
1321
- return Effect.void;
1322
- }
1323
- if (command._tag === "Plan") {
1324
- if (current._tag === "Planning" || current._tag === "Applying") return Effect.void;
1325
- get.set(state, State$1.Planning());
1326
- return Effect.flatMap(Transport.Service, (transport) => transport.provisioning.plan({
1327
- attachmentId: connection.attachment.id,
1328
- domain: connection.attachment.domain,
1329
- records
1330
- })).pipe(Effect.tap((plan) => Effect.sync(() => get.set(state, State$1.Review({ plan })))), Effect.catch((error) => Effect.sync(() => get.set(state, failureFromError(error)))), Effect.asVoid);
1331
- }
1332
- if (current._tag !== "Review" || current.plan.operations.some((operation) => operation._tag === "Conflict")) return Effect.void;
1333
- const plan = current.plan;
1334
- get.set(state, State$1.Applying({ plan }));
1335
- return Effect.flatMap(Transport.Service, (transport) => transport.provisioning.apply({
1336
- attachmentId: connection.attachment.id,
1337
- domain: connection.attachment.domain,
1338
- planDigest: plan.digest
1339
- })).pipe(Effect.tap((result) => Effect.sync(() => {
1340
- if (result._tag === "Applied") {
1341
- get.set(state, State$1.Complete({ result }));
1342
- emit(Event.RecordsApplied({
1343
- connection,
1344
- result
1345
- }));
1346
- onAppliedRef.current?.(result);
1347
- } else if (result._tag === "Partial") {
1348
- get.set(state, State$1.Partial({ result }));
1349
- emit(Event.RecordsPartiallyApplied({
1350
- connection,
1351
- result
1352
- }));
1353
- onAppliedRef.current?.(result);
1354
- } else get.set(state, result);
1355
- })), Effect.catch((error) => Effect.sync(() => get.set(state, failureFromError(error)))), Effect.asVoid);
1356
- }),
1357
- state
1358
- };
1359
- }, [
1360
- connection.attachment.domain,
1361
- connection.attachment.id,
1362
- emit,
1363
- recordKey,
1364
- runtime
1365
- ]);
1036
+ const Kind = leafPart("span", "operation-kind");
1037
+ const Type = leafPart("strong", "operation-type");
1038
+ const Record = leafPart("span", "operation-record");
1039
+ const Name = leafPart("span", "operation-name");
1040
+ const Reason$1 = leafPart("span", "operation-reason");
1041
+ function Item({ operation, ...props }) {
1042
+ const { messages } = useDomainKit();
1043
+ return usePart("li", props, { operation: operation._tag }, {
1044
+ children: /* @__PURE__ */ jsxs(Fragment, { children: [
1045
+ /* @__PURE__ */ jsx(Kind, { children: messages.operation(operation) }),
1046
+ " ",
1047
+ /* @__PURE__ */ jsx(Type, { children: messages.recordType(operation.record) }),
1048
+ " ",
1049
+ /* @__PURE__ */ jsxs(Record, { children: [/* @__PURE__ */ jsx(Name, { children: operation.record.name }), /* @__PURE__ */ jsx(CopyValue, { value: DnsRecord.data(operation.record) })] }),
1050
+ operation._tag === "Conflict" ? /* @__PURE__ */ jsx(Reason$1, { children: messages.conflictReason(operation.reason) }) : null
1051
+ ] }),
1052
+ "data-domainkit-part": "operation-item",
1053
+ "data-operation": operation._tag
1054
+ });
1366
1055
  }
1367
- function useController$1(connection, records, onApplied) {
1368
- const model = useModel$1(connection, records, onApplied);
1369
- const state = useAtomValue(model.state);
1370
- const execute = useAtomSet(model.command);
1371
- return {
1372
- apply: () => execute(Command$1.Apply()),
1373
- dismiss: () => execute(Command$1.Dismiss()),
1374
- plan: () => execute(Command$1.Plan()),
1375
- retry: () => execute(Command$1.Plan()),
1376
- state
1377
- };
1056
+ function Root$4({ count = 0, kind, ...props }) {
1057
+ return usePart("ul", props, {
1058
+ count,
1059
+ kind
1060
+ }, {
1061
+ "data-domainkit-part": kind === "provisioning" ? "plan-operations" : "cleanup-operations",
1062
+ "data-kind": kind
1063
+ });
1378
1064
  }
1379
- function Root$1({ status, ...props }) {
1380
- return usePart("div", props, { status }, {
1381
- "data-domainkit-part": "provisioning-flow",
1382
- "data-state": status
1065
+ /** Every operation the plan holds, in plan order, so a customer reviews exactly what will run. */
1066
+ function List({ plan, ...props }) {
1067
+ return /* @__PURE__ */ jsx(Root$4, {
1068
+ count: plan.operations.length,
1069
+ kind: plan.kind,
1070
+ ...props,
1071
+ children: plan.operations.map((operation) => /* @__PURE__ */ jsx(Item, { operation }, operation.id))
1072
+ });
1073
+ }
1074
+ //#endregion
1075
+ //#region src/review.tsx
1076
+ /**
1077
+ * The review surface both `Provision` and `Cleanup` render: what the plan will do, the Approve
1078
+ * and Decline actions D35 defines, and the outcome. `kind` picks the wording; nothing else about
1079
+ * the two flows differs on screen.
1080
+ */
1081
+ const busy = (status) => status === "Planning" || status === "Approving" || status === "Applying" || status === "Rejecting";
1082
+ /** What the flow is doing right now, or what it finished doing. */
1083
+ function Status$4({ controller, kind, ...props }) {
1084
+ const { messages } = useDomainKit();
1085
+ const state = controller.state;
1086
+ const text = () => {
1087
+ switch (state._tag) {
1088
+ case "Idle": return "";
1089
+ case "Planning": return kind === "provisioning" ? messages.planning : messages.planningCleanup;
1090
+ case "Planned": return kind === "provisioning" ? messages.planConsent : messages.cleanupConsent;
1091
+ case "Approving": return messages.approving;
1092
+ case "Applying": return kind === "provisioning" ? messages.applying : messages.cleaning;
1093
+ case "Rejecting": return messages.rejecting;
1094
+ case "Rejected": return messages.declinedBy(state.attempt.rejection?.actorId ?? "");
1095
+ case "Applied": return kind === "provisioning" ? messages.applied(state.receipt) : messages.cleaned(state.receipt);
1096
+ case "Failure": return "";
1097
+ }
1098
+ };
1099
+ return usePart("p", props, { status: state._tag }, {
1100
+ children: text(),
1101
+ "data-domainkit-part": kind === "provisioning" ? "provisioning-status" : "cleanup-status",
1102
+ "data-state": state._tag,
1103
+ role: busy(state._tag) ? "status" : void 0
1383
1104
  });
1384
1105
  }
1385
- function Outcome$1({ state }) {
1106
+ /** The failure sentence from the error's reason, with the retry the flow allows. */
1107
+ function Outcome$4({ controller, kind, ...props }) {
1386
1108
  const { messages } = useDomainKit();
1387
- if (state._tag === "Partial" || state._tag === "Failure" || state._tag === "Stale") return /* @__PURE__ */ jsx("p", {
1109
+ const readOnly = useReadOnly();
1110
+ const state = controller.state;
1111
+ const partial = state._tag === "Applied" && state.receipt.status === "partial";
1112
+ const element = usePart("p", props, { status: state._tag }, {
1113
+ children: state._tag === "Failure" ? /* @__PURE__ */ jsxs(Fragment, { children: [failure(state.error, messages), readOnly ? null : /* @__PURE__ */ jsxs(Fragment, { children: [" ", /* @__PURE__ */ jsx("button", {
1114
+ "data-domainkit-part": kind === "provisioning" ? "provisioning-retry" : "cleanup-retry",
1115
+ onClick: controller.retry,
1116
+ type: "button",
1117
+ children: messages.retry
1118
+ })] })] }) : partial ? kind === "provisioning" ? messages.partiallyApplied : messages.partiallyCleaned : null,
1388
1119
  "data-domainkit-part": "flow-outcome",
1389
1120
  "data-tone": "danger",
1390
- role: "alert",
1391
- children: state._tag === "Partial" ? messages.recordsPartiallyApplied : state.message
1121
+ role: "alert"
1392
1122
  });
1393
- return null;
1123
+ return state._tag === "Failure" || partial ? element : null;
1394
1124
  }
1395
- function Dialog$1({ controller, trigger }) {
1396
- const { colorScheme, messages, portalContainer, themeStyle } = useDomainKit();
1125
+ /**
1126
+ * Approve and Decline. Approve authorizes the digest and applies it; Decline records the refusal
1127
+ * and ends the attempt. Neither renders until there is a plan to act on.
1128
+ */
1129
+ function Actions$2({ controller, kind, ...props }) {
1130
+ const { messages } = useDomainKit();
1131
+ const readOnly = useReadOnly();
1397
1132
  const state = controller.state;
1398
- const planRef = useRef(void 0);
1399
- if (state._tag === "Idle") planRef.current = void 0;
1400
- if (state._tag === "Review" || state._tag === "Applying") planRef.current = state.plan;
1401
- const plan = planRef.current;
1402
- const busy = state._tag === "Planning" || state._tag === "Applying";
1403
- const open = state._tag !== "Idle" && state._tag !== "Complete";
1404
- const isNoopOnly = plan !== void 0 && plan.operations.every((operation) => operation._tag === "Noop");
1133
+ const plan = state._tag === "Planned" ? state.plan : null;
1134
+ const running = busy(state._tag);
1135
+ const element = usePart("div", props, { status: state._tag }, {
1136
+ children: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
1137
+ "data-domainkit-part": kind === "provisioning" ? "plan-apply" : "cleanup-apply",
1138
+ disabled: running || plan === null || plan.operations.length === 0,
1139
+ onClick: () => controller.approve(),
1140
+ type: "button",
1141
+ children: messages.approve
1142
+ }), /* @__PURE__ */ jsx("button", {
1143
+ "data-domainkit-part": "plan-decline",
1144
+ disabled: running || plan === null,
1145
+ onClick: () => controller.reject(),
1146
+ type: "button",
1147
+ children: messages.decline
1148
+ })] }),
1149
+ "data-domainkit-part": "review-actions"
1150
+ });
1151
+ if (readOnly) return null;
1152
+ return plan === null && !running ? null : element;
1153
+ }
1154
+ /** The operations under review, or the reason there is nothing to review. */
1155
+ function Body({ controller, kind }) {
1156
+ const { messages } = useDomainKit();
1157
+ const state = controller.state;
1158
+ const plan = state._tag === "Planned" || state._tag === "Approving" || state._tag === "Applying" || state._tag === "Rejecting" || state._tag === "Rejected" ? state.plan : state._tag === "Applied" ? state.plan : null;
1159
+ return /* @__PURE__ */ jsxs("div", {
1160
+ "data-domainkit-part": "dialog-body",
1161
+ children: [
1162
+ /* @__PURE__ */ jsx(Status$4, {
1163
+ controller,
1164
+ kind
1165
+ }),
1166
+ plan === null ? null : plan.operations.length === 0 ? /* @__PURE__ */ jsx("p", {
1167
+ "data-domainkit-part": "plan-empty",
1168
+ children: messages.noChanges
1169
+ }) : /* @__PURE__ */ jsx(List, { plan }),
1170
+ state._tag === "Rejected" && state.attempt.rejection?.reason != null ? /* @__PURE__ */ jsx("p", {
1171
+ "data-domainkit-part": "plan-decline-reason",
1172
+ children: messages.declineReason(state.attempt.rejection.reason)
1173
+ }) : null,
1174
+ /* @__PURE__ */ jsx(Outcome$4, {
1175
+ controller,
1176
+ kind
1177
+ })
1178
+ ]
1179
+ });
1180
+ }
1181
+ /** The review dialog. `render` swaps the surface; the contents stay the flow's. */
1182
+ function Dialog$4({ children, controller, kind, onOpenChange, open, render, trigger }) {
1183
+ const { colorScheme, messages, portalContainer, themeStyle } = useDomainKit();
1184
+ const readOnly = useReadOnly();
1185
+ const [uncontrolled, setUncontrolled] = useState(false);
1186
+ const isOpen = open ?? uncontrolled;
1187
+ const setOpen = (next) => {
1188
+ if (open === void 0) setUncontrolled(next);
1189
+ onOpenChange?.(next);
1190
+ };
1191
+ const running = busy(controller.state._tag);
1192
+ const body = children ?? /* @__PURE__ */ jsx(Body, {
1193
+ controller,
1194
+ kind
1195
+ });
1196
+ const title = kind === "provisioning" ? messages.planTitle : messages.cleanupTitle;
1197
+ const label = trigger ?? (kind === "provisioning" ? messages.reviewChanges : messages.cleanUp);
1198
+ if (readOnly) return /* @__PURE__ */ jsx(Fragment, {});
1199
+ if (render !== void 0) return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
1200
+ "data-domainkit-part": kind === "provisioning" ? "plan-trigger" : "cleanup-trigger",
1201
+ onClick: () => {
1202
+ setOpen(true);
1203
+ controller.plan();
1204
+ },
1205
+ type: "button",
1206
+ children: label
1207
+ }), render({
1208
+ children: body,
1209
+ open: isOpen
1210
+ })] });
1405
1211
  return /* @__PURE__ */ jsxs(Dialog.Root, {
1406
- onOpenChange: (nextOpen, eventDetails) => {
1407
- if (!nextOpen && busy) {
1408
- eventDetails.cancel();
1212
+ onOpenChange: (next, details) => {
1213
+ if (!next && running) {
1214
+ details.cancel();
1409
1215
  return;
1410
1216
  }
1411
- if (!nextOpen) controller.dismiss();
1217
+ setOpen(next);
1412
1218
  },
1413
- open,
1219
+ open: isOpen,
1414
1220
  children: [/* @__PURE__ */ jsx(Dialog.Trigger, {
1415
- "data-domainkit-part": "plan-trigger",
1416
- disabled: state._tag !== "Idle",
1417
- onClick: () => controller.plan(),
1418
- ...trigger === void 0 ? {} : { render: trigger },
1419
- children: messages.reviewDns
1221
+ "data-domainkit-part": kind === "provisioning" ? "plan-trigger" : "cleanup-trigger",
1222
+ onClick: controller.plan,
1223
+ children: label
1420
1224
  }), /* @__PURE__ */ jsxs(Dialog.Portal, {
1421
1225
  container: portalContainer,
1422
1226
  children: [/* @__PURE__ */ jsx(Dialog.Backdrop, {
@@ -1426,7 +1230,7 @@ function Dialog$1({ controller, trigger }) {
1426
1230
  style: themeStyle
1427
1231
  }), /* @__PURE__ */ jsxs(Dialog.Popup, {
1428
1232
  "data-color-scheme": colorScheme,
1429
- "data-domainkit-part": "plan-dialog",
1233
+ "data-domainkit-part": kind === "provisioning" ? "plan-dialog" : "cleanup-dialog",
1430
1234
  "data-domainkit-root": "",
1431
1235
  style: themeStyle,
1432
1236
  children: [
@@ -1436,203 +1240,911 @@ function Dialog$1({ controller, trigger }) {
1436
1240
  "data-domainkit-part": "dialog-heading",
1437
1241
  children: [/* @__PURE__ */ jsx(Dialog.Title, {
1438
1242
  "data-domainkit-part": "dialog-title",
1439
- children: messages.reviewDns
1243
+ children: title
1440
1244
  }), /* @__PURE__ */ jsx(Dialog.Description, {
1441
1245
  "data-domainkit-part": "dialog-description",
1442
- children: messages.planConsent
1246
+ children: kind === "provisioning" ? messages.planConsent : messages.cleanupConsent
1443
1247
  })]
1444
- }), busy ? null : /* @__PURE__ */ jsx(Dialog.Close, {
1248
+ }), running ? null : /* @__PURE__ */ jsx(Dialog.Close, {
1445
1249
  "aria-label": messages.close,
1446
1250
  "data-domainkit-part": "dialog-close",
1447
1251
  children: "×"
1448
1252
  })]
1449
1253
  }),
1254
+ body,
1450
1255
  /* @__PURE__ */ jsxs("div", {
1451
- "data-domainkit-part": "dialog-body",
1452
- children: [plan === void 0 ? /* @__PURE__ */ jsx("p", {
1453
- "data-domainkit-part": "dialog-progress",
1454
- role: "status",
1455
- children: messages.planningDns
1456
- }) : /* @__PURE__ */ jsx(List, {
1457
- lifecycle: "provisioning",
1458
- operations: plan.operations
1459
- }), /* @__PURE__ */ jsx(Outcome$1, { state })]
1460
- }),
1461
- /* @__PURE__ */ jsx("div", {
1462
1256
  "data-domainkit-part": "dialog-footer",
1463
- children: busy ? /* @__PURE__ */ jsx("button", {
1464
- "data-domainkit-part": "plan-apply",
1465
- disabled: true,
1466
- type: "button",
1467
- children: state._tag === "Applying" ? messages.applyingDns : messages.planningDns
1468
- }) : state._tag === "Review" && plan !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Dialog.Close, {
1257
+ children: [/* @__PURE__ */ jsx(Actions$2, {
1258
+ controller,
1259
+ kind
1260
+ }), running ? null : /* @__PURE__ */ jsx(Dialog.Close, {
1469
1261
  "data-domainkit-part": "dialog-cancel",
1470
1262
  children: messages.cancel
1471
- }), /* @__PURE__ */ jsx("button", {
1472
- "data-domainkit-part": "plan-apply",
1473
- disabled: isNoopOnly || plan.operations.some((operation) => operation._tag === "Conflict"),
1474
- onClick: () => controller.apply(),
1475
- type: "button",
1476
- children: isNoopOnly ? messages.noChanges : messages.applyDns
1477
- })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Dialog.Close, {
1478
- "data-domainkit-part": "dialog-cancel",
1479
- children: messages.close
1480
- }), /* @__PURE__ */ jsx("button", {
1481
- "data-domainkit-part": "provisioning-retry",
1482
- onClick: () => controller.retry(),
1483
- type: "button",
1484
- children: messages.retry
1485
- })] })
1263
+ })]
1486
1264
  })
1487
1265
  ]
1488
1266
  })]
1489
1267
  })]
1490
1268
  });
1491
1269
  }
1492
- function RetryAction({ controller }) {
1493
- const { messages } = useDomainKit();
1494
- const state = controller.state;
1495
- if (state._tag !== "Failure" && state._tag !== "Stale" && state._tag !== "Partial") return null;
1496
- return /* @__PURE__ */ jsx("button", {
1497
- "data-domainkit-part": "provisioning-retry",
1498
- onClick: () => controller.retry(),
1499
- type: "button",
1500
- children: messages.retry
1270
+ //#endregion
1271
+ //#region src/cleanup.tsx
1272
+ var cleanup_exports = /* @__PURE__ */ __exportAll({
1273
+ Actions: () => Actions$1,
1274
+ Dialog: () => Dialog$3,
1275
+ Flow: () => Flow$3,
1276
+ Outcome: () => Outcome$3,
1277
+ Root: () => Root$3,
1278
+ State: () => State$2,
1279
+ Status: () => Status$3,
1280
+ useController: () => useController$3
1281
+ });
1282
+ /**
1283
+ * Cleanup is bound to a receipt: it removes only what an apply proved DomainKit created. When the
1284
+ * host does not name one, the controller reads the domain's latest from the snapshot.
1285
+ */
1286
+ function useController$3({ domain, onCleaned, receiptId }) {
1287
+ const { transport } = useDomainKit();
1288
+ const group = transport.cleanup;
1289
+ const connection = transport.connection;
1290
+ return useAttempt({
1291
+ domain,
1292
+ key: [domain, receiptId ?? ""].join("|"),
1293
+ done: (receipt) => Event.Cleaned({
1294
+ domain,
1295
+ receipt
1296
+ }),
1297
+ group,
1298
+ onDone: onCleaned,
1299
+ plan: useCallback(() => {
1300
+ if (group === void 0) return null;
1301
+ if (receiptId !== void 0) return group.plan(receiptId);
1302
+ if (connection === void 0) return null;
1303
+ return Effect.flatMap(connection.inspect(domain), (snapshot) => snapshot.lastReceiptId === null ? Effect.fail(new DomainKit.Error({ reason: new Reason.NotFound({
1304
+ entity: "receipt",
1305
+ id: snapshot.domain
1306
+ }) })) : group.plan(Receipt.ReceiptId.make(snapshot.lastReceiptId)));
1307
+ }, [
1308
+ connection,
1309
+ domain,
1310
+ group,
1311
+ receiptId
1312
+ ])
1501
1313
  });
1502
1314
  }
1503
- function Flow$1({ connection, onApplied, records, showRecords = true, trigger, ...props }) {
1504
- const controller = useController$1(connection, records, onApplied);
1505
- const state = controller.state;
1506
- return /* @__PURE__ */ jsxs(Root$1, {
1507
- status: state._tag,
1315
+ function Root$3({ controller, ...props }) {
1316
+ return usePart("div", props, { status: controller.state._tag }, {
1317
+ "data-domainkit-part": "cleanup-flow",
1318
+ "data-state": controller.state._tag
1319
+ });
1320
+ }
1321
+ function Dialog$3(props) {
1322
+ return /* @__PURE__ */ jsx(Dialog$4, {
1508
1323
  ...props,
1509
- children: [showRecords ? /* @__PURE__ */ jsx(Table, { records }) : null, /* @__PURE__ */ jsx(Dialog$1, {
1510
- controller,
1511
- ...trigger === void 0 ? {} : { trigger }
1512
- })]
1324
+ kind: "cleanup"
1513
1325
  });
1514
1326
  }
1515
- //#endregion
1516
- //#region src/verification.tsx
1517
- var verification_exports = /* @__PURE__ */ __exportAll({
1518
- Command: () => Command,
1519
- Evidence: () => Evidence,
1520
- ObserveAction: () => ObserveAction,
1521
- Outcome: () => Outcome,
1522
- Root: () => Root,
1523
- State: () => State,
1524
- Status: () => Status,
1525
- useController: () => useController,
1526
- useModel: () => useModel
1527
- });
1528
- const State = Data.taggedEnum();
1529
- const Command = Data.taggedEnum();
1530
- function useModel(config) {
1531
- const { runtime } = useDomainKit();
1532
- const provider = config.sources?.provider ?? config.connection !== void 0;
1533
- const publicDns = config.sources?.publicDns ?? true;
1534
- const recordKey = recordsIdentity(config.records);
1535
- return useMemo(() => {
1536
- const state = Atom.make(State.Idle());
1537
- return {
1538
- command: runtime.fn()((command, get) => {
1539
- if (command._tag === "Reset") {
1540
- get.set(state, State.Idle());
1541
- return Effect.void;
1542
- }
1543
- const current = get(state);
1544
- if (current._tag === "Observing") return Effect.void;
1545
- const previous = current._tag === "Observation" ? current : current._tag === "Failed" ? current.previous : void 0;
1546
- get.set(state, State.Observing({ ...previous === void 0 ? {} : { previous } }));
1547
- return Effect.flatMap(Transport.Service, (transport) => transport.verification.observe({
1548
- ...config.connection === void 0 ? {} : { attachmentId: config.connection.attachment.id },
1549
- domain: config.domain,
1550
- records: config.records,
1551
- sources: {
1552
- provider,
1553
- publicDns
1554
- }
1555
- })).pipe(Effect.tap((observation) => Effect.sync(() => get.set(state, observation))), Effect.catch((error) => Effect.sync(() => get.set(state, State.Failed({
1556
- failure: failureFromError(error),
1557
- ...previous === void 0 ? {} : { previous }
1558
- })))), Effect.asVoid);
1559
- }),
1560
- state
1327
+ function Actions$1(props) {
1328
+ return /* @__PURE__ */ jsx(Actions$2, {
1329
+ ...props,
1330
+ kind: "cleanup"
1331
+ });
1332
+ }
1333
+ function Outcome$3(props) {
1334
+ return /* @__PURE__ */ jsx(Outcome$4, {
1335
+ ...props,
1336
+ kind: "cleanup"
1337
+ });
1338
+ }
1339
+ function Status$3(props) {
1340
+ return /* @__PURE__ */ jsx(Status$4, {
1341
+ ...props,
1342
+ kind: "cleanup"
1343
+ });
1344
+ }
1345
+ /** Cleanup on its own: a trigger that plans the removal, then the same review dialog. */
1346
+ function Flow$3({ domain, onCleaned, receiptId, trigger, ...props }) {
1347
+ const controller = useController$3({
1348
+ domain,
1349
+ ...onCleaned === void 0 ? {} : { onCleaned },
1350
+ ...receiptId === void 0 ? {} : { receiptId }
1351
+ });
1352
+ return /* @__PURE__ */ jsxs(Root$3, {
1353
+ controller,
1354
+ ...props,
1355
+ children: [/* @__PURE__ */ jsx(Dialog$3, {
1356
+ controller,
1357
+ ...trigger === void 0 ? {} : { trigger }
1358
+ }), /* @__PURE__ */ jsx(Outcome$3, { controller })]
1359
+ });
1360
+ }
1361
+ //#endregion
1362
+ //#region src/provider.tsx
1363
+ var provider_exports = /* @__PURE__ */ __exportAll({ Mark: () => Mark });
1364
+ const key = (providerId) => providerId.trim().toLowerCase().replaceAll(/[^a-z0-9]+/g, "");
1365
+ /** No network call: the fallback is the provider's own initial, drawn with the theme's tokens. */
1366
+ const monogram = (provider) => provider.name.trim().charAt(0).toUpperCase() || "?";
1367
+ /**
1368
+ * A provider's artwork. `DomainKit.Root`'s `marks` decides what a provider looks like; without an
1369
+ * entry the mark is the provider's initial. Nothing is fetched at render time.
1370
+ */
1371
+ function Mark({ provider, ...props }) {
1372
+ const { marks } = useDomainKit();
1373
+ const replacement = marks[provider.id];
1374
+ const content = typeof replacement === "function" ? replacement(provider) : replacement ?? monogram(provider);
1375
+ return usePart("span", props, { providerId: provider.id }, {
1376
+ "aria-label": provider.name,
1377
+ children: content,
1378
+ "data-domainkit-part": "provider-mark",
1379
+ "data-domainkit-provider": key(provider.id),
1380
+ role: "img"
1381
+ });
1382
+ }
1383
+ //#endregion
1384
+ //#region src/connect.tsx
1385
+ var connect_exports = /* @__PURE__ */ __exportAll({
1386
+ Card: () => Card,
1387
+ Dialog: () => Dialog$2,
1388
+ Flow: () => Flow$2,
1389
+ Form: () => Form,
1390
+ Outcome: () => Outcome$2,
1391
+ Root: () => Root$2,
1392
+ State: () => State$1,
1393
+ Status: () => Status$2,
1394
+ useController: () => useController$2
1395
+ });
1396
+ const State$1 = Data.taggedEnum();
1397
+ /** Read at connect time, not at render, so it is the page the customer is actually on. */
1398
+ const currentUrl = () => typeof window === "undefined" ? null : window.location.href;
1399
+ const snapshotOf = (state) => {
1400
+ switch (state._tag) {
1401
+ case "Connected":
1402
+ case "Reconnect":
1403
+ case "Disconnected": return state.snapshot;
1404
+ case "Loading":
1405
+ case "Submitting":
1406
+ case "SelectionRequired": return state.snapshot;
1407
+ case "Redirecting":
1408
+ case "Failure": return null;
1409
+ }
1410
+ };
1411
+ const settled = (snapshot, discovery) => {
1412
+ switch (snapshot.status) {
1413
+ case "connected": return State$1.Connected({ snapshot });
1414
+ case "reconnect": return State$1.Reconnect({ snapshot });
1415
+ case "disconnected": return State$1.Disconnected({
1416
+ discovery,
1417
+ snapshot
1418
+ });
1419
+ }
1420
+ };
1421
+ /**
1422
+ * The domain's connection, and the four ways to change it. Discovery runs on mount whenever the
1423
+ * transport declares it and the domain has no connection yet; a discovery failure is not the
1424
+ * customer's problem, so the provider list still renders.
1425
+ */
1426
+ function useController$2({ domain, returnTo }) {
1427
+ const { emit, navigate, revision, transport } = useDomainKit();
1428
+ const readOnly = useReadOnly();
1429
+ const connection = transport.connection;
1430
+ const runner = useRunner();
1431
+ const [state, setState] = useState(State$1.Loading({ snapshot: null }));
1432
+ const held = useRef({
1433
+ discovery: null,
1434
+ domain,
1435
+ snapshot: null
1436
+ });
1437
+ const lastCommand = useRef(null);
1438
+ const [inspected, setInspected] = useState(domain);
1439
+ if (inspected !== domain) {
1440
+ setInspected(domain);
1441
+ runner.cancel();
1442
+ held.current = {
1443
+ discovery: null,
1444
+ domain,
1445
+ snapshot: null
1446
+ };
1447
+ lastCommand.current = null;
1448
+ setState(State$1.Loading({ snapshot: null }));
1449
+ }
1450
+ const onFailure = useCallback((error) => {
1451
+ setState(State$1.Failure({ error }));
1452
+ emit(Event.Failed({
1453
+ domain,
1454
+ error
1455
+ }));
1456
+ }, [domain, emit]);
1457
+ const load = useCallback(() => {
1458
+ if (connection === void 0) return;
1459
+ lastCommand.current = null;
1460
+ held.current = {
1461
+ discovery: null,
1462
+ domain,
1463
+ snapshot: null
1561
1464
  };
1465
+ setState((previous) => State$1.Loading({ snapshot: snapshotOf(previous) }));
1466
+ runner.run(Effect.flatMap(connection.inspect(domain), (snapshot) => snapshot.status === "disconnected" ? Effect.map(connection.discover(domain).pipe(Effect.catch(() => Effect.succeed(null))), (discovery) => ({
1467
+ discovery,
1468
+ snapshot
1469
+ })) : Effect.succeed({
1470
+ discovery: null,
1471
+ snapshot
1472
+ })), {
1473
+ onFailure,
1474
+ onSuccess: ({ discovery, snapshot }) => {
1475
+ if (held.current.domain !== domain) return;
1476
+ held.current = {
1477
+ discovery,
1478
+ domain,
1479
+ snapshot
1480
+ };
1481
+ setState(settled(snapshot, discovery));
1482
+ }
1483
+ });
1562
1484
  }, [
1563
- config.connection?.attachment.id,
1564
- config.domain,
1565
- provider,
1566
- publicDns,
1567
- recordKey,
1568
- runtime
1485
+ connection,
1486
+ domain,
1487
+ onFailure,
1488
+ runner
1489
+ ]);
1490
+ useEffect(load, [load, revision]);
1491
+ const started = useCallback((result) => {
1492
+ if (held.current.domain !== domain) return;
1493
+ switch (result._tag) {
1494
+ case "Connected":
1495
+ held.current = {
1496
+ discovery: null,
1497
+ domain,
1498
+ snapshot: result.snapshot
1499
+ };
1500
+ setState(settled(result.snapshot, null));
1501
+ if (result.snapshot.connectionId !== null) emit(Event.Connected({
1502
+ connectionId: result.snapshot.connectionId,
1503
+ domain,
1504
+ snapshot: result.snapshot
1505
+ }));
1506
+ return;
1507
+ case "Redirect":
1508
+ setState(State$1.Redirecting({ url: result.authorizationUrl }));
1509
+ navigate(result.authorizationUrl);
1510
+ return;
1511
+ case "SelectionRequired":
1512
+ setState(State$1.SelectionRequired({
1513
+ candidates: result.candidates,
1514
+ connectionId: result.connectionId,
1515
+ snapshot: held.current.snapshot
1516
+ }));
1517
+ return;
1518
+ }
1519
+ }, [
1520
+ domain,
1521
+ emit,
1522
+ navigate
1523
+ ]);
1524
+ const submit = useCallback((effect) => {
1525
+ if (held.current.domain !== domain) return;
1526
+ const command = () => {
1527
+ setState(State$1.Submitting({ snapshot: held.current.snapshot }));
1528
+ runner.run(effect, {
1529
+ onFailure,
1530
+ onSuccess: started
1531
+ });
1532
+ };
1533
+ lastCommand.current = {
1534
+ domain,
1535
+ run: command
1536
+ };
1537
+ command();
1538
+ }, [
1539
+ domain,
1540
+ onFailure,
1541
+ runner,
1542
+ started
1543
+ ]);
1544
+ const connect = useCallback((input) => {
1545
+ if (connection === void 0) return;
1546
+ const destination = input.returnTo ?? (returnTo === void 0 ? currentUrl() : returnTo);
1547
+ const interactive = destination === null ? {} : { returnTo: destination };
1548
+ const method = input.method === "token" ? Transport.Method.token(input.values ?? {}) : input.method === "oauth" ? Transport.Method.oauth(interactive) : Transport.Method.integration(interactive);
1549
+ submit(connection.start({
1550
+ domain,
1551
+ method,
1552
+ provider: input.provider
1553
+ }));
1554
+ }, [
1555
+ connection,
1556
+ domain,
1557
+ returnTo,
1558
+ submit
1559
+ ]);
1560
+ const reuse = useCallback((input) => {
1561
+ if (connection === void 0) return;
1562
+ submit(connection.attach({
1563
+ connectionId: input.connectionId,
1564
+ domain,
1565
+ ...input.zone === void 0 ? {} : { zone: input.zone }
1566
+ }));
1567
+ }, [
1568
+ connection,
1569
+ domain,
1570
+ submit
1571
+ ]);
1572
+ const select = useCallback((zone) => {
1573
+ if (state._tag !== "SelectionRequired") return;
1574
+ reuse({
1575
+ connectionId: state.connectionId,
1576
+ zone
1577
+ });
1578
+ }, [reuse, state]);
1579
+ const release = useCallback((effect, event) => {
1580
+ if (held.current.domain !== domain) return;
1581
+ const command = () => {
1582
+ setState(State$1.Submitting({ snapshot: held.current.snapshot }));
1583
+ runner.run(effect, {
1584
+ onFailure,
1585
+ onSuccess: () => {
1586
+ emit(event);
1587
+ load();
1588
+ }
1589
+ });
1590
+ };
1591
+ lastCommand.current = {
1592
+ domain,
1593
+ run: command
1594
+ };
1595
+ command();
1596
+ }, [
1597
+ domain,
1598
+ emit,
1599
+ load,
1600
+ onFailure,
1601
+ runner
1602
+ ]);
1603
+ const detach = useCallback(() => {
1604
+ const attachmentId = held.current.snapshot?.attachmentId;
1605
+ if (connection === void 0 || attachmentId == null || held.current.domain !== domain) return;
1606
+ release(connection.detach(attachmentId), Event.Detached({ domain }));
1607
+ }, [
1608
+ connection,
1609
+ domain,
1610
+ release
1611
+ ]);
1612
+ const disconnect = useCallback(() => {
1613
+ const connectionId = held.current.snapshot?.connectionId;
1614
+ if (connection === void 0 || connectionId == null || held.current.domain !== domain) return;
1615
+ release(connection.disconnect(connectionId), Event.Disconnected({
1616
+ connectionId,
1617
+ domain
1618
+ }));
1619
+ }, [
1620
+ connection,
1621
+ domain,
1622
+ release
1623
+ ]);
1624
+ const retry = useCallback(() => {
1625
+ const command = lastCommand.current;
1626
+ if (readOnly || command === null || command.domain !== domain) load();
1627
+ else command.run();
1628
+ }, [
1629
+ domain,
1630
+ load,
1631
+ readOnly
1569
1632
  ]);
1570
- }
1571
- function useController(config) {
1572
- const model = useModel(config);
1573
- const state = useAtomValue(model.state);
1574
- const dispatch = useAtomSet(model.command);
1575
- const observe = () => dispatch(Command.Observe());
1576
- const reset = () => dispatch(Command.Reset());
1577
1633
  return {
1578
- observe,
1579
- reset,
1634
+ connect,
1635
+ detach,
1636
+ disconnect,
1637
+ discovery: state._tag === "Disconnected" ? state.discovery : null,
1638
+ providers: snapshotOf(state)?.providers ?? [],
1639
+ refresh: load,
1640
+ retry,
1641
+ reuse,
1642
+ select,
1643
+ snapshot: snapshotOf(state),
1580
1644
  state
1581
1645
  };
1582
1646
  }
1583
- const evidenceNote = (evidence) => {
1584
- switch (evidence._tag) {
1585
- case "Mismatch":
1586
- case "Unavailable": return evidence.message;
1587
- case "Found":
1588
- case "Missing": return;
1589
- default: return evidence;
1590
- }
1591
- };
1592
- const observationGroups = (observation) => [{
1593
- evidence: observation.provider,
1594
- label: "Provider"
1595
- }, {
1596
- evidence: observation.publicDns,
1597
- label: "Public DNS"
1598
- }].filter((group) => group.evidence.length > 0);
1599
- function Root({ status, ...props }) {
1600
- return usePart("div", props, { status }, {
1601
- "data-domainkit-part": "verification-status",
1602
- "data-state": status
1647
+ function Root$2({ controller, ...props }) {
1648
+ return usePart("div", props, { status: controller.state._tag }, {
1649
+ "data-domainkit-part": "connection-root",
1650
+ "data-state": controller.state._tag
1603
1651
  });
1604
1652
  }
1605
- function Evidence({ observation }) {
1606
- return /* @__PURE__ */ jsx("div", {
1607
- "data-domainkit-part": "observation-list",
1608
- children: observationGroups(observation).map((group) => /* @__PURE__ */ jsxs("section", {
1609
- "data-domainkit-part": "observation-group",
1610
- children: [/* @__PURE__ */ jsx("h3", {
1611
- "data-domainkit-part": "observation-source",
1612
- children: group.label
1613
- }), /* @__PURE__ */ jsx("ul", { children: group.evidence.map((evidence, index) => {
1614
- const note = evidenceNote(evidence);
1615
- return /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsxs("div", {
1616
- "data-domainkit-part": "observation-row",
1617
- children: [/* @__PURE__ */ jsx("span", {
1618
- "data-domainkit-part": "observation-record",
1619
- children: evidence.recordId
1620
- }), /* @__PURE__ */ jsx(Status$1, { evidence })]
1621
- }), note === void 0 ? null : /* @__PURE__ */ jsx("p", {
1622
- "data-domainkit-part": "observation-note",
1623
- children: note
1624
- })] }, `${evidence._tag}-${evidence.recordId}-${index}`);
1625
- }) })]
1626
- }, group.label))
1627
- });
1628
- }
1629
- function Outcome({ state }) {
1630
- if (state._tag !== "Failed") return null;
1631
- return /* @__PURE__ */ jsx("p", {
1653
+ /** The failure sentence, chosen by the error's reason, plus the retry that reason allows. */
1654
+ function Outcome$2({ controller, ...props }) {
1655
+ const { messages } = useDomainKit();
1656
+ const readOnly = useReadOnly();
1657
+ const state = controller.state;
1658
+ const element = usePart("p", props, { status: state._tag }, {
1659
+ children: state._tag === "Failure" ? /* @__PURE__ */ jsxs(Fragment, { children: [failure(state.error, messages), readOnly ? null : /* @__PURE__ */ jsxs(Fragment, { children: [" ", /* @__PURE__ */ jsx("button", {
1660
+ "data-domainkit-part": "connection-retry",
1661
+ onClick: controller.retry,
1662
+ type: "button",
1663
+ children: messages.retry
1664
+ })] })] }) : null,
1632
1665
  "data-domainkit-part": "flow-outcome",
1633
1666
  "data-tone": "danger",
1634
- role: "alert",
1635
- children: state.failure.message
1667
+ role: "alert"
1668
+ });
1669
+ return state._tag === "Failure" ? element : null;
1670
+ }
1671
+ function Status$2({ controller, ...props }) {
1672
+ const { messages } = useDomainKit();
1673
+ const state = controller.state;
1674
+ const provider = controller.snapshot?.provider ?? "";
1675
+ const text = () => {
1676
+ switch (state._tag) {
1677
+ case "Loading": return messages.loading;
1678
+ case "Submitting": return messages.connecting;
1679
+ case "Redirecting": return messages.redirecting;
1680
+ case "Connected": return messages.connectedTo(provider);
1681
+ case "Reconnect": return messages.reconnectRequired(provider);
1682
+ case "SelectionRequired": return messages.chooseZone;
1683
+ case "Disconnected": return messages.notConnected;
1684
+ case "Failure": return "";
1685
+ }
1686
+ };
1687
+ return usePart("p", props, { status: state._tag }, {
1688
+ children: text(),
1689
+ "data-domainkit-part": "connection-status",
1690
+ "data-state": state._tag,
1691
+ role: state._tag === "Submitting" || state._tag === "Loading" ? "status" : void 0
1692
+ });
1693
+ }
1694
+ /**
1695
+ * One auth method, rendered from its descriptor. A token method draws an input per declared
1696
+ * field, so a provider that needs an account id alongside a token needs no code here.
1697
+ */
1698
+ function Method({ controller, method, provider }) {
1699
+ const { messages } = useDomainKit();
1700
+ const icons = useIcons();
1701
+ const busy = controller.state._tag === "Submitting";
1702
+ if (method.fields === null) return /* @__PURE__ */ jsx("button", {
1703
+ "data-domainkit-part": method.kind === "oauth" ? "oauth-connect" : "integration-connect",
1704
+ "data-provider": provider.id,
1705
+ disabled: busy,
1706
+ onClick: () => controller.connect({
1707
+ method: method.kind,
1708
+ provider: provider.id
1709
+ }),
1710
+ type: "button",
1711
+ children: messages.connectWith(method.label)
1712
+ });
1713
+ const fields = method.fields;
1714
+ return /* @__PURE__ */ jsxs("form", {
1715
+ "data-domainkit-part": "token-connect",
1716
+ "data-provider": provider.id,
1717
+ onSubmit: (event) => {
1718
+ event.preventDefault();
1719
+ const data = new FormData(event.currentTarget);
1720
+ const values = {};
1721
+ for (const field of fields) {
1722
+ const value = data.get(field.name);
1723
+ if (typeof value === "string" && (value !== "" || field.required)) values[field.name] = value;
1724
+ }
1725
+ controller.connect({
1726
+ method: method.kind,
1727
+ provider: provider.id,
1728
+ values
1729
+ });
1730
+ },
1731
+ children: [
1732
+ fields.map((field) => /* @__PURE__ */ jsxs("label", {
1733
+ "data-domainkit-part": "token-field",
1734
+ children: [/* @__PURE__ */ jsxs("span", {
1735
+ "data-domainkit-part": "token-field-label",
1736
+ children: [messages.fieldLabel(field.name), field.required ? null : /* @__PURE__ */ jsx("span", {
1737
+ "data-domainkit-part": "token-field-optional",
1738
+ children: messages.optionalField
1739
+ })]
1740
+ }), /* @__PURE__ */ jsx("input", {
1741
+ autoComplete: "off",
1742
+ "data-domainkit-part": "token-field-input",
1743
+ name: field.name,
1744
+ required: field.required,
1745
+ type: field.secret ? "password" : "text"
1746
+ })]
1747
+ }, field.name)),
1748
+ method.docsUrl === null ? null : /* @__PURE__ */ jsxs("a", {
1749
+ "data-domainkit-part": "token-docs",
1750
+ href: method.docsUrl,
1751
+ rel: "noreferrer",
1752
+ target: "_blank",
1753
+ children: [messages.getToken, /* @__PURE__ */ jsx("span", {
1754
+ "aria-hidden": "true",
1755
+ "data-icon": "inline-end",
1756
+ children: icons.external
1757
+ })]
1758
+ }),
1759
+ /* @__PURE__ */ jsx("button", {
1760
+ "data-domainkit-part": "token-submit",
1761
+ disabled: busy,
1762
+ type: "submit",
1763
+ children: messages.connectWith(method.label)
1764
+ })
1765
+ ]
1766
+ });
1767
+ }
1768
+ /**
1769
+ * Everything a disconnected domain can do: a connection discovery already found, the connections
1770
+ * this owner already has, then each provider's declared methods.
1771
+ */
1772
+ function Form({ controller, ...props }) {
1773
+ const { messages } = useDomainKit();
1774
+ const state = controller.state;
1775
+ const snapshot = controller.snapshot;
1776
+ const discovery = controller.discovery;
1777
+ const candidates = state._tag === "SelectionRequired" ? state.candidates.map((candidate) => ({
1778
+ ...candidate,
1779
+ connectionId: state.connectionId
1780
+ })) : discovery !== null && discovery._tag === "SelectionRequired" ? discovery.candidates : [];
1781
+ const resolved = discovery !== null && discovery._tag === "Resolved" ? discovery : null;
1782
+ const reusable = snapshot?.reusable ?? [];
1783
+ const providers = controller.providers;
1784
+ return usePart("div", props, { status: state._tag }, {
1785
+ children: /* @__PURE__ */ jsxs(Fragment, { children: [
1786
+ resolved === null ? null : /* @__PURE__ */ jsxs("section", {
1787
+ "data-domainkit-part": "discovery-resolved",
1788
+ children: [/* @__PURE__ */ jsx("p", { children: messages.discoveredConnection(resolved.label) }), /* @__PURE__ */ jsx("button", {
1789
+ "data-domainkit-part": "attach-target",
1790
+ onClick: () => controller.reuse({
1791
+ connectionId: resolved.connectionId,
1792
+ zone: resolved.zone
1793
+ }),
1794
+ type: "button",
1795
+ children: messages.useConnection(resolved.label)
1796
+ })]
1797
+ }),
1798
+ candidates.length === 0 ? null : /* @__PURE__ */ jsxs("section", {
1799
+ "data-domainkit-part": "discovery-candidates",
1800
+ children: [/* @__PURE__ */ jsx("p", { children: state._tag === "SelectionRequired" ? messages.chooseZone : messages.discoveryAmbiguous }), /* @__PURE__ */ jsx("div", {
1801
+ "data-domainkit-part": "target-list",
1802
+ "data-state": "ambiguous",
1803
+ children: candidates.map((candidate) => /* @__PURE__ */ jsx("button", {
1804
+ "data-domainkit-part": "attach-target",
1805
+ onClick: () => controller.reuse({
1806
+ connectionId: candidate.connectionId,
1807
+ zone: candidate.zone
1808
+ }),
1809
+ type: "button",
1810
+ children: messages.useConnection(candidate.label)
1811
+ }, `${candidate.connectionId}:${candidate.zone}`))
1812
+ })]
1813
+ }),
1814
+ reusable.length === 0 ? null : /* @__PURE__ */ jsxs("section", {
1815
+ "data-domainkit-part": "reusable-connection",
1816
+ children: [/* @__PURE__ */ jsx("div", {
1817
+ "data-domainkit-part": "reusable-connection-heading",
1818
+ children: /* @__PURE__ */ jsx("strong", { children: messages.reusableConnections })
1819
+ }), reusable.map((connection) => /* @__PURE__ */ jsx("button", {
1820
+ "data-connection-id": connection.connectionId,
1821
+ "data-domainkit-part": "attach-target",
1822
+ onClick: () => controller.reuse({ connectionId: connection.connectionId }),
1823
+ type: "button",
1824
+ children: messages.useConnection(connection.provider)
1825
+ }, connection.connectionId))]
1826
+ }),
1827
+ providers.length === 0 ? /* @__PURE__ */ jsx("p", {
1828
+ "data-domainkit-part": "target-unavailable",
1829
+ children: messages.noProviders
1830
+ }) : providers.map((provider) => /* @__PURE__ */ jsxs("section", {
1831
+ "data-domainkit-part": "provider-authentication",
1832
+ children: [/* @__PURE__ */ jsxs("div", {
1833
+ "data-domainkit-part": "provider-heading",
1834
+ children: [/* @__PURE__ */ jsx(Mark, { provider }), /* @__PURE__ */ jsx("strong", { children: provider.name })]
1835
+ }), provider.methods.map((method) => /* @__PURE__ */ jsx(Method, {
1836
+ controller,
1837
+ method,
1838
+ provider
1839
+ }, `${provider.id}:${method.kind}`))]
1840
+ }, provider.id))
1841
+ ] }),
1842
+ "data-domainkit-part": "connection-form"
1843
+ });
1844
+ }
1845
+ /** The connect surface. Without `render` it is a Base UI dialog behind `trigger`. */
1846
+ function Dialog$2({ children, controller, render, title, trigger }) {
1847
+ const { colorScheme, messages, portalContainer, themeStyle } = useDomainKit();
1848
+ const readOnly = useReadOnly();
1849
+ const [open, setOpen] = useState(false);
1850
+ const busy = controller.state._tag === "Submitting";
1851
+ const snapshot = controller.snapshot;
1852
+ const provider = snapshot?.provider;
1853
+ const heading = title ?? (provider === null || provider === void 0 ? messages.connectAnyTitle : messages.connectTitle(provider));
1854
+ const body = children ?? /* @__PURE__ */ jsx(Form, { controller });
1855
+ if (readOnly) return /* @__PURE__ */ jsx(Fragment, {});
1856
+ if (render !== void 0) return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
1857
+ "data-domainkit-part": "connection-trigger",
1858
+ onClick: () => setOpen(!open),
1859
+ type: "button",
1860
+ children: trigger ?? messages.connect
1861
+ }), render({
1862
+ children: body,
1863
+ open
1864
+ })] });
1865
+ return /* @__PURE__ */ jsxs(Dialog.Root, {
1866
+ onOpenChange: (next, details) => {
1867
+ if (!next && busy) {
1868
+ details.cancel();
1869
+ return;
1870
+ }
1871
+ setOpen(next);
1872
+ },
1873
+ open,
1874
+ children: [/* @__PURE__ */ jsx(Dialog.Trigger, {
1875
+ "data-domainkit-part": "connection-trigger",
1876
+ children: trigger ?? messages.connect
1877
+ }), /* @__PURE__ */ jsxs(Dialog.Portal, {
1878
+ container: portalContainer,
1879
+ children: [/* @__PURE__ */ jsx(Dialog.Backdrop, {
1880
+ "data-color-scheme": colorScheme,
1881
+ "data-domainkit-part": "dialog-backdrop",
1882
+ "data-domainkit-root": "",
1883
+ style: themeStyle
1884
+ }), /* @__PURE__ */ jsxs(Dialog.Popup, {
1885
+ "data-color-scheme": colorScheme,
1886
+ "data-domainkit-part": "connection-dialog",
1887
+ "data-domainkit-root": "",
1888
+ style: themeStyle,
1889
+ children: [
1890
+ /* @__PURE__ */ jsxs("div", {
1891
+ "data-domainkit-part": "dialog-header",
1892
+ children: [/* @__PURE__ */ jsxs("div", {
1893
+ "data-domainkit-part": "dialog-heading",
1894
+ children: [/* @__PURE__ */ jsx(Dialog.Title, {
1895
+ "data-domainkit-part": "dialog-title",
1896
+ children: heading
1897
+ }), /* @__PURE__ */ jsx(Dialog.Description, {
1898
+ "data-domainkit-part": "dialog-description",
1899
+ children: messages.connectDescription(snapshot?.domain ?? "")
1900
+ })]
1901
+ }), busy ? null : /* @__PURE__ */ jsx(Dialog.Close, {
1902
+ "aria-label": messages.close,
1903
+ "data-domainkit-part": "dialog-close",
1904
+ children: "×"
1905
+ })]
1906
+ }),
1907
+ /* @__PURE__ */ jsxs("div", {
1908
+ "data-domainkit-part": "dialog-body",
1909
+ children: [
1910
+ /* @__PURE__ */ jsx(Status$2, { controller }),
1911
+ body,
1912
+ /* @__PURE__ */ jsx(Outcome$2, { controller })
1913
+ ]
1914
+ }),
1915
+ busy ? null : /* @__PURE__ */ jsx("div", {
1916
+ "data-domainkit-part": "dialog-footer",
1917
+ children: /* @__PURE__ */ jsx(Dialog.Close, {
1918
+ "data-domainkit-part": "dialog-cancel",
1919
+ children: messages.cancel
1920
+ })
1921
+ })
1922
+ ]
1923
+ })]
1924
+ })]
1925
+ });
1926
+ }
1927
+ /** A connected domain: which provider holds it, and how to let it go. */
1928
+ function Card({ controller, ...props }) {
1929
+ const { messages } = useDomainKit();
1930
+ const readOnly = useReadOnly();
1931
+ const state = controller.state;
1932
+ const snapshot = controller.snapshot;
1933
+ const provider = controller.providers.find((entry) => entry.id === snapshot?.provider);
1934
+ return usePart("div", props, { status: state._tag }, {
1935
+ children: /* @__PURE__ */ jsxs(Fragment, { children: [
1936
+ /* @__PURE__ */ jsxs("div", {
1937
+ "data-domainkit-part": "connected-identity",
1938
+ children: [provider === void 0 ? null : /* @__PURE__ */ jsx(Mark, { provider }), /* @__PURE__ */ jsx(Status$2, { controller })]
1939
+ }),
1940
+ readOnly ? null : /* @__PURE__ */ jsxs("div", {
1941
+ "data-domainkit-part": "connected-actions",
1942
+ children: [/* @__PURE__ */ jsx("button", {
1943
+ "data-domainkit-part": "disconnect-trigger",
1944
+ disabled: state._tag === "Submitting",
1945
+ onClick: controller.detach,
1946
+ type: "button",
1947
+ children: messages.detach
1948
+ }), /* @__PURE__ */ jsx("button", {
1949
+ "data-domainkit-part": "disconnect-action",
1950
+ disabled: state._tag === "Submitting",
1951
+ onClick: controller.disconnect,
1952
+ type: "button",
1953
+ children: messages.disconnect
1954
+ })]
1955
+ }),
1956
+ /* @__PURE__ */ jsx(Outcome$2, { controller })
1957
+ ] }),
1958
+ "data-domainkit-part": "connected-card"
1959
+ });
1960
+ }
1961
+ /** Connection on its own: the card once connected, the dialog until then. */
1962
+ function Flow$2({ domain, returnTo, ...props }) {
1963
+ const controller = useController$2({
1964
+ domain,
1965
+ ...returnTo === void 0 ? {} : { returnTo }
1966
+ });
1967
+ const state = controller.state;
1968
+ return /* @__PURE__ */ jsx(Root$2, {
1969
+ controller,
1970
+ ...props,
1971
+ children: state._tag === "Connected" ? /* @__PURE__ */ jsx(Card, { controller }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Dialog$2, { controller }), /* @__PURE__ */ jsx(Outcome$2, { controller })] })
1972
+ });
1973
+ }
1974
+ //#endregion
1975
+ //#region src/provision.tsx
1976
+ var provision_exports = /* @__PURE__ */ __exportAll({
1977
+ Actions: () => Actions,
1978
+ Dialog: () => Dialog$1,
1979
+ Flow: () => Flow$1,
1980
+ Outcome: () => Outcome$1,
1981
+ Root: () => Root$1,
1982
+ State: () => State$2,
1983
+ Status: () => Status$1,
1984
+ useController: () => useController$1
1985
+ });
1986
+ /** Plan, approve, apply. `approve` authorizes the digest and applies it in the same action. */
1987
+ /**
1988
+ * Requirements identify themselves by content, so an inline array does not abandon the attempt.
1989
+ * Every field counts: a plan turns on `policy`, and `ttl` and `purpose` ride the wire with it.
1990
+ */
1991
+ const keyOf = (domain, requirements) => [domain, requirementsKey(requirements)].join("|");
1992
+ function useController$1({ domain, onApplied, requirements }) {
1993
+ const { transport } = useDomainKit();
1994
+ const group = transport.provisioning;
1995
+ return useAttempt({
1996
+ domain,
1997
+ key: keyOf(domain, requirements),
1998
+ done: (receipt) => Event.Applied({
1999
+ domain,
2000
+ receipt
2001
+ }),
2002
+ group,
2003
+ onDone: onApplied,
2004
+ plan: useCallback(() => group === void 0 ? null : group.plan({
2005
+ domain,
2006
+ requirements
2007
+ }), [
2008
+ domain,
2009
+ group,
2010
+ requirements
2011
+ ])
2012
+ });
2013
+ }
2014
+ function Root$1({ controller, ...props }) {
2015
+ return usePart("div", props, { status: controller.state._tag }, {
2016
+ "data-domainkit-part": "provisioning-flow",
2017
+ "data-state": controller.state._tag
2018
+ });
2019
+ }
2020
+ function Dialog$1(props) {
2021
+ return /* @__PURE__ */ jsx(Dialog$4, {
2022
+ ...props,
2023
+ kind: "provisioning"
2024
+ });
2025
+ }
2026
+ /** Approve and Decline for a plan already under review. */
2027
+ function Actions(props) {
2028
+ return /* @__PURE__ */ jsx(Actions$2, {
2029
+ ...props,
2030
+ kind: "provisioning"
2031
+ });
2032
+ }
2033
+ function Outcome$1(props) {
2034
+ return /* @__PURE__ */ jsx(Outcome$4, {
2035
+ ...props,
2036
+ kind: "provisioning"
2037
+ });
2038
+ }
2039
+ function Status$1(props) {
2040
+ return /* @__PURE__ */ jsx(Status$4, {
2041
+ ...props,
2042
+ kind: "provisioning"
2043
+ });
2044
+ }
2045
+ /** Provisioning on its own: a trigger that plans, then the review dialog. */
2046
+ function Flow$1({ domain, onApplied, requirements, trigger, ...props }) {
2047
+ const controller = useController$1({
2048
+ domain,
2049
+ requirements,
2050
+ ...onApplied === void 0 ? {} : { onApplied }
2051
+ });
2052
+ return /* @__PURE__ */ jsxs(Root$1, {
2053
+ controller,
2054
+ ...props,
2055
+ children: [/* @__PURE__ */ jsx(Dialog$1, {
2056
+ controller,
2057
+ ...trigger === void 0 ? {} : { trigger }
2058
+ }), /* @__PURE__ */ jsx(Outcome$1, { controller })]
2059
+ });
2060
+ }
2061
+ //#endregion
2062
+ //#region src/verify.tsx
2063
+ var verify_exports = /* @__PURE__ */ __exportAll({
2064
+ Evidence: () => Evidence,
2065
+ ObserveAction: () => ObserveAction,
2066
+ Outcome: () => Outcome,
2067
+ Root: () => Root,
2068
+ State: () => State,
2069
+ Status: () => Status,
2070
+ useController: () => useController
2071
+ });
2072
+ /**
2073
+ * What the observer read back for the requirement's name, or `null` when it read nothing back at
2074
+ * all. Host evidence reports a status the host reached rather than values read off a name, and an
2075
+ * `unknown` observation is a lookup that never answered, which is not the same as finding nothing.
2076
+ */
2077
+ const valuesOf = (evidence) => evidence._tag === "Host" || evidence.status === "unknown" ? null : evidence.values;
2078
+ const State = Data.taggedEnum();
2079
+ const readinessOf = (state) => state._tag === "Idle" ? null : state.readiness;
2080
+ /** Observe once on mount, then follow the readiness's own `nextCheckAt` while polling is on. */
2081
+ function useController({ domain, polling = true, requirements }) {
2082
+ const { emit, revision, transport } = useDomainKit();
2083
+ const verification = transport.verification;
2084
+ const runner = useRunner();
2085
+ const [state, setState] = useState(State.Idle());
2086
+ const timer = useRef(void 0);
2087
+ const signature = requirements === void 0 ? null : requirementsKey(requirements);
2088
+ const requested = useMemo(() => requirements === void 0 ? void 0 : { requirements }, [signature]);
2089
+ const [observed, setObserved] = useState(domain);
2090
+ if (observed !== domain) {
2091
+ setObserved(domain);
2092
+ setState(State.Idle());
2093
+ }
2094
+ const observe = useCallback(() => {
2095
+ if (verification === void 0) return;
2096
+ clearTimeout(timer.current);
2097
+ setState((previous) => State.Observing({ readiness: readinessOf(previous) }));
2098
+ runner.run(requested === void 0 ? verification.observe(domain) : verification.observe(domain, requested), {
2099
+ onFailure: (error) => {
2100
+ setState((previous) => State.Failure({
2101
+ error,
2102
+ readiness: readinessOf(previous)
2103
+ }));
2104
+ emit(Event.Failed({
2105
+ domain,
2106
+ error
2107
+ }));
2108
+ },
2109
+ onSuccess: (readiness) => setState(State.Observed({ readiness }))
2110
+ });
2111
+ }, [
2112
+ domain,
2113
+ emit,
2114
+ requested,
2115
+ runner,
2116
+ verification
2117
+ ]);
2118
+ useEffect(() => {
2119
+ observe();
2120
+ }, [observe, revision]);
2121
+ useEffect(() => {
2122
+ if (!polling || state._tag !== "Observed") return;
2123
+ const next = state.readiness.nextCheckAt;
2124
+ if (next === null) return;
2125
+ const delay = Math.max(0, DateTime.toEpochMillis(next) - Date.now());
2126
+ timer.current = setTimeout(observe, delay);
2127
+ return () => clearTimeout(timer.current);
2128
+ }, [
2129
+ observe,
2130
+ polling,
2131
+ state
2132
+ ]);
2133
+ useEffect(() => () => clearTimeout(timer.current), []);
2134
+ return {
2135
+ observe,
2136
+ polling,
2137
+ readiness: readinessOf(state),
2138
+ retry: observe,
2139
+ state
2140
+ };
2141
+ }
2142
+ function Root({ controller, ...props }) {
2143
+ const overall = controller.readiness?.overall ?? null;
2144
+ return usePart("div", props, { status: controller.state._tag }, {
2145
+ "data-domainkit-part": "verification-status",
2146
+ "data-overall": overall ?? void 0,
2147
+ "data-state": controller.state._tag
1636
2148
  });
1637
2149
  }
1638
2150
  function ObserveAction({ controller, ...props }) {
@@ -1640,402 +2152,292 @@ function ObserveAction({ controller, ...props }) {
1640
2152
  const observing = controller.state._tag === "Observing";
1641
2153
  return usePart("button", props, { status: controller.state._tag }, {
1642
2154
  "aria-busy": observing,
1643
- children: messages.checkDns,
2155
+ children: controller.readiness === null ? messages.checkDns : messages.checkAgain,
1644
2156
  "data-domainkit-part": "observe-action",
1645
2157
  "data-loading": observing ? "" : void 0,
1646
2158
  disabled: observing,
1647
- onClick: () => controller.observe(),
2159
+ onClick: controller.observe,
1648
2160
  type: "button"
1649
2161
  });
1650
2162
  }
1651
- function Status({ config, ...props }) {
1652
- const { colorScheme, messages, portalContainer, themeStyle } = useDomainKit();
1653
- const controller = useController(config);
2163
+ /** Evidence carries no id; its source and position identify it within one requirement. */
2164
+ const evidenceKey = (evidence, index) => [evidence._tag, index].join("-");
2165
+ /**
2166
+ * Per-requirement readiness and, below it, the host's own evidence: an SES identity, a certificate,
2167
+ * anything DomainKit cannot observe but the host feeds in.
2168
+ */
2169
+ function Evidence({ readiness }) {
2170
+ const { messages } = useDomainKit();
2171
+ const icons = useIcons();
2172
+ return /* @__PURE__ */ jsxs("div", {
2173
+ "data-domainkit-part": "observation-list",
2174
+ children: [readiness.requirements.map((requirement) => {
2175
+ const settled = requirement.status === "satisfied";
2176
+ return /* @__PURE__ */ jsxs("section", {
2177
+ "data-domainkit-part": "observation-group",
2178
+ children: [
2179
+ /* @__PURE__ */ jsxs("div", {
2180
+ "data-domainkit-part": "observation-row",
2181
+ children: [/* @__PURE__ */ jsxs("span", {
2182
+ "data-domainkit-part": "observation-record",
2183
+ children: [
2184
+ messages.recordType(requirement.record),
2185
+ " ",
2186
+ requirement.record.name
2187
+ ]
2188
+ }), /* @__PURE__ */ jsx(Status$5, { status: requirement.status })]
2189
+ }),
2190
+ settled ? null : /* @__PURE__ */ jsx("p", {
2191
+ "data-domainkit-part": "observation-expected",
2192
+ children: messages.expectedValue(DnsRecord.data(requirement.record))
2193
+ }),
2194
+ /* @__PURE__ */ jsx("ul", { children: requirement.evidence.map((evidence, index) => {
2195
+ const values = settled ? null : valuesOf(evidence);
2196
+ return /* @__PURE__ */ jsxs("li", {
2197
+ "data-domainkit-part": "observation-source",
2198
+ children: [
2199
+ messages.evidenceSource(evidence),
2200
+ values === null ? null : /* @__PURE__ */ jsx("span", {
2201
+ "data-domainkit-part": "observation-observed",
2202
+ children: values.length === 0 ? messages.observedNothing : messages.observedValues(values)
2203
+ }),
2204
+ evidence.detail === null ? null : /* @__PURE__ */ jsx("p", {
2205
+ "data-domainkit-part": "observation-note",
2206
+ children: evidence.detail
2207
+ })
2208
+ ]
2209
+ }, evidenceKey(evidence, index));
2210
+ }) })
2211
+ ]
2212
+ }, identity(requirement.record));
2213
+ }), readiness.host.length === 0 ? null : /* @__PURE__ */ jsxs("section", {
2214
+ "data-domainkit-part": "observation-group",
2215
+ "data-source": "host",
2216
+ children: [/* @__PURE__ */ jsx("h3", {
2217
+ "data-domainkit-part": "observation-source",
2218
+ children: messages.hostEvidence
2219
+ }), /* @__PURE__ */ jsx("ul", { children: readiness.host.map((evidence, index) => /* @__PURE__ */ jsxs("li", {
2220
+ "data-domainkit-part": "host-evidence",
2221
+ children: [
2222
+ /* @__PURE__ */ jsx("span", {
2223
+ "aria-hidden": "true",
2224
+ "data-icon": "inline-start",
2225
+ children: evidence.status === "ok" ? icons.success : evidence.status === "pending" ? icons.pending : icons.failure
2226
+ }),
2227
+ /* @__PURE__ */ jsx("span", {
2228
+ "data-domainkit-part": "observation-record",
2229
+ children: evidence.label
2230
+ }),
2231
+ /* @__PURE__ */ jsx("span", {
2232
+ "data-domainkit-part": "record-status",
2233
+ "data-status": evidence.status,
2234
+ children: messages.hostEvidenceStatus(evidence.status)
2235
+ }),
2236
+ evidence.detail === null ? null : /* @__PURE__ */ jsx("p", {
2237
+ "data-domainkit-part": "observation-note",
2238
+ children: evidence.detail
2239
+ })
2240
+ ]
2241
+ }, `${evidence.source}-${index}`)) })]
2242
+ })]
2243
+ });
2244
+ }
2245
+ function Outcome({ controller, ...props }) {
2246
+ const { messages } = useDomainKit();
1654
2247
  const state = controller.state;
1655
- const observation = state._tag === "Observation" ? state : state._tag === "Observing" || state._tag === "Failed" ? state.previous : void 0;
2248
+ const element = usePart("p", props, { status: state._tag }, {
2249
+ children: state._tag === "Failure" ? /* @__PURE__ */ jsxs(Fragment, { children: [
2250
+ failure(state.error, messages),
2251
+ " ",
2252
+ /* @__PURE__ */ jsx("button", {
2253
+ "data-domainkit-part": "verification-retry",
2254
+ onClick: controller.retry,
2255
+ type: "button",
2256
+ children: messages.retry
2257
+ })
2258
+ ] }) : null,
2259
+ "data-domainkit-part": "flow-outcome",
2260
+ "data-tone": "danger",
2261
+ role: "alert"
2262
+ });
2263
+ return state._tag === "Failure" ? element : null;
2264
+ }
2265
+ /** The default verification slot: a button that observes, with the evidence behind it. */
2266
+ function Status({ controller, render, ...props }) {
2267
+ const { colorScheme, messages, portalContainer, themeStyle } = useDomainKit();
2268
+ const readiness = controller.readiness;
2269
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [
2270
+ controller.state._tag === "Observing" ? /* @__PURE__ */ jsx("p", {
2271
+ "data-domainkit-part": "verification-progress",
2272
+ role: "status",
2273
+ children: messages.observing
2274
+ }) : null,
2275
+ readiness === null ? /* @__PURE__ */ jsx("p", {
2276
+ "data-domainkit-part": "verification-progress",
2277
+ children: messages.noEvidence
2278
+ }) : /* @__PURE__ */ jsx(Evidence, { readiness }),
2279
+ /* @__PURE__ */ jsx(Outcome, { controller })
2280
+ ] });
1656
2281
  return /* @__PURE__ */ jsx(Root, {
1657
- status: state._tag,
2282
+ controller,
1658
2283
  ...props,
1659
- children: /* @__PURE__ */ jsxs(Popover.Root, { children: [/* @__PURE__ */ jsx(Popover.Trigger, { render: /* @__PURE__ */ jsx(ObserveAction, { controller }) }), /* @__PURE__ */ jsx(Popover.Portal, {
2284
+ children: render === void 0 ? /* @__PURE__ */ jsxs(Popover.Root, { children: [/* @__PURE__ */ jsx(Popover.Trigger, { render: /* @__PURE__ */ jsx(ObserveAction, { controller }) }), /* @__PURE__ */ jsx(Popover.Portal, {
1660
2285
  container: portalContainer,
1661
2286
  children: /* @__PURE__ */ jsx(Popover.Positioner, {
1662
2287
  align: "end",
1663
2288
  "data-domainkit-part": "verification-positioner",
1664
2289
  sideOffset: 6,
1665
2290
  children: /* @__PURE__ */ jsxs(Popover.Popup, {
2291
+ "aria-busy": controller.state._tag === "Observing",
1666
2292
  "aria-label": messages.checkDns,
1667
- "aria-busy": state._tag === "Observing",
1668
2293
  "data-color-scheme": colorScheme,
1669
2294
  "data-domainkit-part": "verification-popover",
1670
2295
  "data-domainkit-root": "",
1671
2296
  style: themeStyle,
1672
- children: [
1673
- /* @__PURE__ */ jsx(Popover.Arrow, { "data-domainkit-part": "verification-arrow" }),
1674
- state._tag === "Observing" ? /* @__PURE__ */ jsx("p", {
1675
- "data-domainkit-part": "verification-progress",
1676
- role: "status",
1677
- children: messages.checkingDns
1678
- }) : null,
1679
- observation === void 0 ? null : /* @__PURE__ */ jsx(Evidence, { observation }),
1680
- /* @__PURE__ */ jsx(Outcome, { state })
1681
- ]
2297
+ children: [/* @__PURE__ */ jsx(Popover.Arrow, { "data-domainkit-part": "verification-arrow" }), body]
1682
2298
  })
1683
2299
  })
2300
+ })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ObserveAction, { controller }), render({
2301
+ children: body,
2302
+ open: controller.state._tag !== "Idle"
1684
2303
  })] })
1685
2304
  });
1686
2305
  }
1687
2306
  //#endregion
1688
2307
  //#region src/domain.tsx
1689
2308
  var domain_exports = /* @__PURE__ */ __exportAll({ Flow: () => Flow });
1690
- function Flow({ domain, receiptId, records }) {
1691
- const controller = useController$2(domain);
1692
- const state = controller.state;
1693
- const connected = state._tag === "Connected" ? state : state._tag === "Detaching" ? state.snapshot : void 0;
1694
- return /* @__PURE__ */ jsxs(Root$3, {
1695
- status: state._tag,
1696
- children: [
1697
- state._tag === "Connected" || state._tag === "Detaching" ? /* @__PURE__ */ jsx(ConnectedCard, {
1698
- connection: state._tag === "Connected" ? state : state.snapshot,
1699
- controller,
1700
- ...receiptId === void 0 ? {} : { initialReceiptId: receiptId },
1701
- records,
1702
- status: state._tag
1703
- }) : /* @__PURE__ */ jsx(AvailableCard, {
1704
- controller,
1705
- state
1706
- }),
1707
- /* @__PURE__ */ jsx(Table, { records }),
1708
- /* @__PURE__ */ jsx(Status, { config: {
1709
- ...connected === void 0 ? {} : { connection: connected },
1710
- domain,
1711
- records
1712
- } })
1713
- ]
1714
- });
2309
+ function DefaultConnection({ controller }) {
2310
+ return controller.state._tag === "Connected" ? /* @__PURE__ */ jsx(Card, { controller }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2311
+ /* @__PURE__ */ jsx(Status$2, { controller }),
2312
+ /* @__PURE__ */ jsx(Dialog$2, { controller }),
2313
+ /* @__PURE__ */ jsx(Outcome$2, { controller })
2314
+ ] });
1715
2315
  }
1716
- function AvailableCard({ controller, state }) {
1717
- const { messages } = useDomainKit();
1718
- const snapshot = state._tag === "Disconnected" ? state : state._tag === "Submitting" || state._tag === "Redirecting" ? state.snapshot : void 0;
1719
- return /* @__PURE__ */ jsxs(Card$1, {
1720
- status: state._tag,
1721
- children: [/* @__PURE__ */ jsxs(CardIdentity, {
1722
- status: state._tag,
1723
- children: [snapshot === void 0 ? null : /* @__PURE__ */ jsx(Mark, { provider: snapshot.provider }), /* @__PURE__ */ jsx(Status$2, { state })]
1724
- }), /* @__PURE__ */ jsxs(CardActions, {
1725
- status: state._tag,
1726
- children: [state._tag === "Failure" && state.retry !== "never" ? /* @__PURE__ */ jsx(RetryAction$1, {
1727
- controller,
1728
- kind: state.retry === "after-user-action" ? "after-user-action" : "retry"
1729
- }) : null, snapshot === void 0 ? null : /* @__PURE__ */ jsxs(Dialog.Root, {
1730
- onOpenChange: (open, eventDetails) => {
1731
- if (!open && (state._tag === "Submitting" || state._tag === "Redirecting")) eventDetails.cancel();
1732
- },
1733
- children: [/* @__PURE__ */ jsx(ConnectTrigger, {
1734
- disabled: state._tag === "Submitting" || state._tag === "Redirecting",
1735
- provider: snapshot.provider,
1736
- children: messages.connectProvider(snapshot.provider.name)
1737
- }), /* @__PURE__ */ jsx(Dialog$2, {
1738
- controller,
1739
- snapshot
1740
- })]
1741
- })]
1742
- })]
2316
+ function DefaultActions({ cleanup, connection, provisioning }) {
2317
+ const { capabilities, messages } = useDomainKit();
2318
+ const readOnly = useReadOnly();
2319
+ const icons = useIcons();
2320
+ const connected = connection.state._tag === "Connected";
2321
+ if (readOnly) return null;
2322
+ const hasReceipt = connection.snapshot?.lastReceiptId != null;
2323
+ return /* @__PURE__ */ jsxs(Fragment, { children: [capabilities.includes("provisioning") && connected ? /* @__PURE__ */ jsxs(Fragment, { children: [
2324
+ /* @__PURE__ */ jsx("button", {
2325
+ "data-domainkit-part": "plan-trigger",
2326
+ disabled: provisioning.state._tag === "Planning",
2327
+ onClick: provisioning.plan,
2328
+ type: "button",
2329
+ children: messages.reviewChanges
2330
+ }),
2331
+ /* @__PURE__ */ jsx(Status$1, { controller: provisioning }),
2332
+ /* @__PURE__ */ jsx(Actions, { controller: provisioning }),
2333
+ /* @__PURE__ */ jsx(Outcome$1, { controller: provisioning })
2334
+ ] }) : null, capabilities.includes("cleanup") && connected && hasReceipt ? /* @__PURE__ */ jsxs(Fragment, { children: [
2335
+ /* @__PURE__ */ jsxs("button", {
2336
+ "data-domainkit-part": "cleanup-trigger",
2337
+ disabled: cleanup.state._tag === "Planning",
2338
+ onClick: cleanup.plan,
2339
+ type: "button",
2340
+ children: [/* @__PURE__ */ jsx("span", {
2341
+ "aria-hidden": "true",
2342
+ "data-icon": "inline-start",
2343
+ children: icons.close
2344
+ }), messages.cleanUp]
2345
+ }),
2346
+ /* @__PURE__ */ jsx(Status$3, { controller: cleanup }),
2347
+ /* @__PURE__ */ jsx(Actions$1, { controller: cleanup }),
2348
+ /* @__PURE__ */ jsx(Outcome$3, { controller: cleanup })
2349
+ ] }) : null] });
2350
+ }
2351
+ /**
2352
+ * The whole lifecycle for one domain: connect, review, apply, observe, clean up. The flow renders
2353
+ * only what the transport declares, and every part of it is a slot with a default. It adds no
2354
+ * layout containers of its own, so a host's grid can place the slot output directly.
2355
+ */
2356
+ function Flow({ domain, onApplied, onCleaned, readOnly, requirements, returnTo, slots = {}, ...props }) {
2357
+ const { capabilities } = useDomainKit();
2358
+ const inherited = useReadOnly();
2359
+ const connection = useController$2({
2360
+ domain,
2361
+ ...returnTo === void 0 ? {} : { returnTo }
1743
2362
  });
1744
- }
1745
- function ConnectedCard({ connection, controller, initialReceiptId, records, status }) {
1746
- const [receipt, setReceipt] = useState({
1747
- host: initialReceiptId,
1748
- value: initialReceiptId
2363
+ const refresh = connection.refresh;
2364
+ const provisioning = useController$1({
2365
+ domain,
2366
+ onApplied: useCallback((receipt) => {
2367
+ refresh();
2368
+ onApplied?.(receipt);
2369
+ }, [onApplied, refresh]),
2370
+ requirements
2371
+ });
2372
+ const cleanup = useController$3({
2373
+ domain,
2374
+ onCleaned: useCallback((receipt) => {
2375
+ refresh();
2376
+ onCleaned?.(receipt);
2377
+ }, [onCleaned, refresh]),
2378
+ ...connection.snapshot?.lastReceiptId == null ? {} : { receiptId: Receipt.ReceiptId.make(connection.snapshot.lastReceiptId) }
1749
2379
  });
1750
- if (receipt.host !== initialReceiptId) setReceipt({
1751
- host: initialReceiptId,
1752
- value: initialReceiptId
2380
+ const verification = useController({
2381
+ domain,
2382
+ requirements
1753
2383
  });
1754
- const receiptId = receipt.host === initialReceiptId ? receipt.value : initialReceiptId;
1755
- const disabled = status === "Detaching";
1756
- const actionsTriggerRef = useRef(null);
1757
- return /* @__PURE__ */ jsxs(Card$1, {
1758
- status,
1759
- children: [/* @__PURE__ */ jsxs(CardIdentity, {
1760
- status,
1761
- children: [/* @__PURE__ */ jsx(Mark, { provider: connection.provider }), /* @__PURE__ */ jsx(Status$2, { state: status === "Connected" ? connection : controller.state })]
1762
- }), /* @__PURE__ */ jsxs(CardActions, {
1763
- status,
1764
- children: [/* @__PURE__ */ jsx(Flow$1, {
1765
- connection,
1766
- onApplied: (result) => setReceipt({
1767
- host: initialReceiptId,
1768
- value: result.receiptId
2384
+ const readiness = verification.readiness;
2385
+ return usePart("div", props, {
2386
+ connection: connection.state._tag,
2387
+ provisioning: provisioning.state._tag
2388
+ }, {
2389
+ children: /* @__PURE__ */ jsxs(ReadOnly, {
2390
+ value: readOnly ?? inherited,
2391
+ children: [
2392
+ !capabilities.includes("connection") ? null : slots.connection === void 0 ? /* @__PURE__ */ jsx(DefaultConnection, {
2393
+ controller: connection,
2394
+ domain
2395
+ }) : slots.connection({
2396
+ controller: connection,
2397
+ domain
1769
2398
  }),
1770
- records,
1771
- showRecords: false
1772
- }), receiptId === void 0 ? /* @__PURE__ */ jsx(ProviderActions, {
1773
- actionsTriggerRef,
1774
- connection,
1775
- controller,
1776
- disabled
1777
- }) : /* @__PURE__ */ jsx(ProviderActionsWithCleanup, {
1778
- actionsTriggerRef,
1779
- connection,
1780
- controller,
1781
- disabled,
1782
- onCleaned: () => setReceipt({
1783
- host: initialReceiptId,
1784
- value: void 0
2399
+ slots.records === void 0 ? /* @__PURE__ */ jsx(Table, {
2400
+ readiness,
2401
+ records: requirements
2402
+ }) : slots.records({
2403
+ controller: verification,
2404
+ domain,
2405
+ readiness,
2406
+ records: requirements
1785
2407
  }),
1786
- receiptId
1787
- }, receiptId)]
1788
- })]
1789
- });
1790
- }
1791
- function ProviderActionsWithCleanup({ actionsTriggerRef, connection, controller, disabled, onCleaned, receiptId }) {
1792
- const [cleaned, setCleaned] = useState(false);
1793
- const cleanup = useController$3(connection, receiptId, (result) => {
1794
- if (result._tag === "Cleaned") setCleaned(true);
1795
- });
1796
- return /* @__PURE__ */ jsx(ProviderActions, {
1797
- actionsTriggerRef,
1798
- cleanup,
1799
- connection,
1800
- controller,
1801
- disabled,
1802
- onCleanupCloseComplete: () => {
1803
- if (cleaned) onCleaned();
1804
- }
1805
- });
1806
- }
1807
- function ProviderActions({ actionsTriggerRef: actionsTriggerRefProp, cleanup, connection, controller, disabled, onCleanupCloseComplete }) {
1808
- const { colorScheme, messages, portalContainer, themeStyle } = useDomainKit();
1809
- const fallbackActionsTriggerRef = useRef(null);
1810
- const actionsTriggerRef = actionsTriggerRefProp ?? fallbackActionsTriggerRef;
1811
- const [menuOpen, setMenuOpen] = useState(false);
1812
- const [disconnectOpen, setDisconnectOpen] = useState(false);
1813
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1814
- /* @__PURE__ */ jsxs(Menu.Root, {
1815
- disabled,
1816
- modal: false,
1817
- onOpenChange: setMenuOpen,
1818
- open: menuOpen,
1819
- children: [/* @__PURE__ */ jsx(Menu.Trigger, {
1820
- "aria-label": messages.moreActions,
1821
- "data-domainkit-part": "actions-trigger",
1822
- ref: actionsTriggerRef,
1823
- children: /* @__PURE__ */ jsx("span", {
1824
- "aria-hidden": "true",
1825
- children: "⋯"
1826
- })
1827
- }), /* @__PURE__ */ jsx(Menu.Portal, {
1828
- container: portalContainer,
1829
- keepMounted: true,
1830
- children: /* @__PURE__ */ jsx(Menu.Positioner, {
1831
- align: "end",
1832
- sideOffset: 6,
1833
- children: /* @__PURE__ */ jsxs(Menu.Popup, {
1834
- "data-color-scheme": colorScheme,
1835
- "data-domainkit-part": "actions-menu",
1836
- "data-domainkit-root": "",
1837
- style: themeStyle,
1838
- children: [cleanup === void 0 ? null : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Menu.Item, {
1839
- closeOnClick: false,
1840
- "data-domainkit-part": "actions-item",
1841
- nativeButton: true,
1842
- onClickCapture: () => {
1843
- setMenuOpen(false);
1844
- queueMicrotask(() => cleanup.plan());
1845
- },
1846
- render: /* @__PURE__ */ jsx("button", { type: "button" }),
1847
- children: messages.reviewCleanup
1848
- }), /* @__PURE__ */ jsx(Menu.Separator, { "data-domainkit-part": "actions-separator" })] }), /* @__PURE__ */ jsx(Menu.Item, {
1849
- closeOnClick: false,
1850
- "data-domainkit-part": "actions-item",
1851
- "data-tone": "danger",
1852
- nativeButton: true,
1853
- onClickCapture: () => {
1854
- setMenuOpen(false);
1855
- queueMicrotask(() => setDisconnectOpen(true));
1856
- },
1857
- render: /* @__PURE__ */ jsx("button", { type: "button" }),
1858
- children: messages.disconnectDomain
1859
- })]
1860
- })
2408
+ !capabilities.includes("verification") ? null : slots.verification === void 0 ? /* @__PURE__ */ jsx(Status, { controller: verification }) : slots.verification({
2409
+ controller: verification,
2410
+ domain
2411
+ }),
2412
+ slots.actions === void 0 ? /* @__PURE__ */ jsx(DefaultActions, {
2413
+ cleanup,
2414
+ connection,
2415
+ domain,
2416
+ provisioning
2417
+ }) : slots.actions({
2418
+ cleanup,
2419
+ connection,
2420
+ domain,
2421
+ provisioning
1861
2422
  })
1862
- })]
1863
- }),
1864
- cleanup === void 0 ? null : /* @__PURE__ */ jsx(Dialog$3, {
1865
- controller: cleanup,
1866
- onOpenChangeComplete: (open) => {
1867
- if (!open) {
1868
- onCleanupCloseComplete?.();
1869
- queueMicrotask(() => actionsTriggerRef.current?.focus());
1870
- }
1871
- },
1872
- trigger: null
2423
+ ]
1873
2424
  }),
1874
- /* @__PURE__ */ jsx(DisconnectDialog, {
1875
- connection,
1876
- controller,
1877
- onOpenChange: (open) => {
1878
- setDisconnectOpen(open);
1879
- if (!open) queueMicrotask(() => actionsTriggerRef.current?.focus());
1880
- },
1881
- open: disconnectOpen,
1882
- trigger: null
1883
- })
1884
- ] });
2425
+ "data-domainkit-part": "domain-flow",
2426
+ "data-domain": domain
2427
+ });
1885
2428
  }
1886
2429
  //#endregion
1887
2430
  //#region src/testing.ts
1888
- var testing_exports = /* @__PURE__ */ __exportAll({
1889
- attachment: () => attachment,
1890
- connected: () => connected,
1891
- connection: () => connection,
1892
- makeFakeTransport: () => makeFakeTransport,
1893
- provider: () => provider,
1894
- target: () => target
1895
- });
1896
- const provider = (overrides = {}) => ({
1897
- authentication: [Transport.AuthenticationMethod.OAuth({ label: "Continue with OAuth" }), Transport.AuthenticationMethod.Token({
1898
- label: "Connect with token",
1899
- placeholder: "Paste API token"
1900
- })],
1901
- id: "cloudflare",
1902
- name: "Cloudflare",
1903
- ...overrides
1904
- });
1905
- const target = (overrides = {}) => ({
1906
- accountId: "account-1",
1907
- accountKind: "account",
1908
- zoneId: "zone-1",
1909
- zoneName: DomainName.parse("example.com"),
1910
- ...overrides
1911
- });
1912
- const connection = (overrides = {}) => ({
1913
- createdAt: /* @__PURE__ */ new Date("2026-08-30T00:00:00.000Z"),
1914
- id: "connection-1",
1915
- method: "oauth2",
1916
- ownerId: "organization-1",
1917
- providerId: "cloudflare",
1918
- status: "active",
1919
- ...overrides
1920
- });
1921
- const attachment = (overrides = {}) => ({
1922
- connectionId: "connection-1",
1923
- createdAt: /* @__PURE__ */ new Date("2026-08-30T00:00:00.000Z"),
1924
- domain: DomainName.parse("mail.example.com"),
1925
- id: "attachment-1",
1926
- target: target(),
1927
- ...overrides
1928
- });
1929
- const connected = (overrides = {}) => Transport.Connected({
1930
- attachment: attachment(),
1931
- connection: connection(),
1932
- provider: provider(),
1933
- ...overrides
1934
- });
1935
- function makeFakeTransport(options) {
1936
- const calls = {
1937
- apply: [],
1938
- attach: [],
1939
- cleanupApply: [],
1940
- cleanupPlan: [],
1941
- connect: [],
1942
- detach: [],
1943
- inspect: [],
1944
- observe: [],
1945
- plan: []
1946
- };
1947
- const inspections = Array.isArray(options.inspect) ? [...options.inspect] : [options.inspect];
1948
- const transport = {
1949
- cleanup: {
1950
- apply: async (input) => {
1951
- calls.cleanupApply.push(input);
1952
- return options.cleanupApply ?? Transport.CleanupResult.Cleaned({
1953
- operationId: "cleanup-operation-1",
1954
- results: []
1955
- });
1956
- },
1957
- plan: async (input) => {
1958
- calls.cleanupPlan.push(input);
1959
- return options.cleanupPlan ?? Transport.CleanupPlan({
1960
- digest: "cleanup-digest-1",
1961
- expiresAt: "2099-01-01T00:00:00.000Z",
1962
- operations: []
1963
- });
1964
- }
1965
- },
1966
- connection: {
1967
- connect: async (input) => {
1968
- calls.connect.push(input);
1969
- return options.connect ?? Transport.Connected({
1970
- attachment: attachment({ domain: DomainName.parse(input.domain) }),
1971
- connection: connection(),
1972
- provider: provider()
1973
- });
1974
- },
1975
- inspect: async (input) => {
1976
- calls.inspect.push(input);
1977
- const snapshot = inspections.shift() ?? inspections.at(-1) ?? options.inspect;
1978
- if (Array.isArray(snapshot)) throw new Error("Fake inspection sequence is empty");
1979
- if (snapshot._tag === "Failure") throw new Transport.Failure({
1980
- message: snapshot.message,
1981
- operation: snapshot.operation ?? "connection.inspect",
1982
- retry: snapshot.retry
1983
- });
1984
- return snapshot;
1985
- },
1986
- attach: async (input) => {
1987
- calls.attach.push(input);
1988
- return options.attach ?? Transport.Connected({
1989
- attachment: attachment({
1990
- connectionId: input.connectionId,
1991
- domain: DomainName.parse(input.domain),
1992
- target: input.target
1993
- }),
1994
- connection: connection({ id: input.connectionId }),
1995
- provider: provider()
1996
- });
1997
- },
1998
- detach: async (input) => {
1999
- calls.detach.push(input);
2000
- return options.detach ?? Transport.DetachResult({
2001
- attachment: attachment(),
2002
- connection: connection(),
2003
- remainingAttachments: 0
2004
- });
2005
- }
2006
- },
2007
- provisioning: {
2008
- apply: async (input) => {
2009
- calls.apply.push(input);
2010
- return options.apply ?? Transport.ApplyResult.Applied({
2011
- operationId: "apply-operation-1",
2012
- receiptId: "receipt-1",
2013
- results: []
2014
- });
2015
- },
2016
- plan: async (input) => {
2017
- calls.plan.push(input);
2018
- return options.plan ?? Transport.ProvisioningPlan({
2019
- digest: "plan-digest-1",
2020
- expiresAt: "2099-01-01T00:00:00.000Z",
2021
- operations: input.records.map((record) => Transport.PlanOperation.Create({
2022
- id: `create-${record.id}`,
2023
- record
2024
- }))
2025
- });
2026
- }
2027
- },
2028
- verification: { observe: async (input) => {
2029
- calls.observe.push(input);
2030
- return options.observe ?? Transport.Observation({
2031
- provider: input.sources.provider ? input.records.map((record) => Transport.ObservationEvidence.Found({ recordId: record.id })) : [],
2032
- publicDns: input.sources.publicDns ? input.records.map((record) => Transport.ObservationEvidence.Found({ recordId: record.id })) : []
2033
- });
2034
- } }
2035
- };
2036
- return Object.assign(Transport.layerFromAsync(transport), transport, { calls });
2037
- }
2431
+ /**
2432
+ * The fake transport `@domainkit/react` tests render against: an in-memory `domainkit/server`
2433
+ * with memory storage and a fake provider, recording every call.
2434
+ */
2435
+ var testing_exports = /* @__PURE__ */ __exportAll({ transport: () => transport });
2436
+ const transport = Testing.transport;
2437
+ //#endregion
2438
+ //#region src/failure.ts
2439
+ const Failure = Data.taggedEnum();
2038
2440
  //#endregion
2039
- export { cleanup_exports as Cleanup, connection_exports as Connection, domain_exports as Domain, domain_kit_exports as DomainKit, lifecycle_exports as Lifecycle, messages_exports as Messages, operations_exports as Operations, provider_exports as Provider, provisioning_exports as Provisioning, records_exports as Records, testing_exports as Testing, theme_exports as Theme, verification_exports as Verification };
2441
+ export { cleanup_exports as Cleanup, connect_exports as Connect, domain_exports as Domain, domain_kit_exports as DomainKit, Event, Failure, messages_exports as Messages, operations_exports as Operations, provider_exports as Provider, provision_exports as Provision, records_exports as Records, testing_exports as Testing, theme_exports as Theme, verify_exports as Verify, defaultIcons, useIcons };
2040
2442
 
2041
2443
  //# sourceMappingURL=index.mjs.map