@mason-data/cli 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +90 -71
  2. package/dist/src/api/error.js +3 -0
  3. package/dist/src/api/postgres/catalog.js +222 -13
  4. package/dist/src/api/postgres/encode.js +3 -1
  5. package/dist/src/api/postgres/explain.js +3 -2
  6. package/dist/src/api/postgres/protocol.js +39 -23
  7. package/dist/src/api/postgres/server.js +1173 -207
  8. package/dist/src/api/postgres/translate.js +81 -9
  9. package/dist/src/api/request-log.js +18 -5
  10. package/dist/src/cdc/poll.js +5 -5
  11. package/dist/src/cli/background.js +29 -6
  12. package/dist/src/cli/cancel.js +75 -0
  13. package/dist/src/cli/command.js +1 -0
  14. package/dist/src/cli/commands.js +622 -183
  15. package/dist/src/cli/control.js +59 -0
  16. package/dist/src/cli/demo.js +19 -10
  17. package/dist/src/cli/dev.js +351 -111
  18. package/dist/src/cli/doctor.js +20 -10
  19. package/dist/src/cli/guides.js +79 -31
  20. package/dist/src/cli/init.js +119 -58
  21. package/dist/src/cli/machine.js +12 -11
  22. package/dist/src/cli/main.js +3 -3
  23. package/dist/src/cli/mcp.js +29 -20
  24. package/dist/src/cli/metrics.js +79 -0
  25. package/dist/src/cli/pg-tls.js +13 -3
  26. package/dist/src/cli/pg-url.js +4 -0
  27. package/dist/src/cli/pin.js +13 -12
  28. package/dist/src/cli/preflight.js +8 -7
  29. package/dist/src/cli/project.js +43 -4
  30. package/dist/src/cli/readiness.js +38 -4
  31. package/dist/src/cli/registry.js +53 -34
  32. package/dist/src/cli/replication-checks.js +7 -5
  33. package/dist/src/cli/replication-mode.js +105 -26
  34. package/dist/src/cli/request-filter.js +134 -0
  35. package/dist/src/cli/roles.js +584 -0
  36. package/dist/src/cli/run-server.js +483 -41
  37. package/dist/src/cli/runs.js +1 -1
  38. package/dist/src/cli/security.js +10 -8
  39. package/dist/src/cli/serve.js +144 -42
  40. package/dist/src/cli/share-run.js +286 -0
  41. package/dist/src/cli/share.js +286 -0
  42. package/dist/src/cli/source-edit.js +65 -13
  43. package/dist/src/cli/sources.js +287 -38
  44. package/dist/src/cli/supervise.js +12 -1
  45. package/dist/src/cli/sync.js +55 -25
  46. package/dist/src/cli/tunnel.js +200 -0
  47. package/dist/src/cli/ui.js +9 -6
  48. package/dist/src/cli/usage.js +128 -0
  49. package/dist/src/cli/user.js +496 -40
  50. package/dist/src/cli/version.js +9 -7
  51. package/dist/src/demo/seed.js +3 -0
  52. package/dist/src/duck/cancel.js +10 -1
  53. package/dist/src/duck/events.js +134 -0
  54. package/dist/src/duck/executor-worker.js +18 -9
  55. package/dist/src/duck/executor.js +43 -30
  56. package/dist/src/duck/explain.js +56 -46
  57. package/dist/src/duck/extensions.js +160 -0
  58. package/dist/src/duck/functions.js +13 -14
  59. package/dist/src/duck/gaps.js +17 -8
  60. package/dist/src/duck/grants.js +176 -0
  61. package/dist/src/duck/history.js +123 -0
  62. package/dist/src/duck/kept.js +6 -6
  63. package/dist/src/duck/plan.js +102 -25
  64. package/dist/src/duck/publication.js +6 -6
  65. package/dist/src/duck/requests.js +265 -93
  66. package/dist/src/duck/rewrite.js +25 -5
  67. package/dist/src/duck/row-filter.js +649 -0
  68. package/dist/src/duck/store.js +156 -120
  69. package/dist/src/duck/stream.js +10 -5
  70. package/dist/src/duck/sync.js +835 -268
  71. package/dist/src/duck/types.js +6 -5
  72. package/dist/src/duck/usage.js +324 -0
  73. package/dist/src/duck/users.js +64 -15
  74. package/dist/src/duck/verified.js +3 -0
  75. package/dist/src/duck/views.js +2 -3
  76. package/dist/src/lib/database-name.js +62 -0
  77. package/dist/src/lib/dotenv.js +17 -4
  78. package/dist/src/lib/log-buffer.js +68 -0
  79. package/dist/src/lib/pg-numeric.js +5 -2
  80. package/dist/src/lib/project-version.js +23 -23
  81. package/dist/src/lib/table-sync.js +267 -0
  82. package/dist/src/web/app.css +706 -66
  83. package/dist/src/web/app.js +327 -410
  84. package/dist/src/web/charts.js +229 -0
  85. package/dist/src/web/index.html +82 -18
  86. package/dist/src/web/pages/queries.js +192 -0
  87. package/dist/src/web/pages/status.js +446 -0
  88. package/dist/src/web/pages/syncs.js +155 -0
  89. package/dist/src/web/pages/tables.js +434 -0
  90. package/dist/src/web/pages/usage.js +397 -0
  91. package/dist/src/web/pages/users.js +218 -0
  92. package/dist/src/web/shared.js +568 -0
  93. package/dist/src/web/vendor/README.md +25 -0
  94. package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
  95. package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
  96. package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
  97. package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
  98. package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
  99. package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
  100. package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
  101. package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
  102. package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
  103. package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
  104. package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
  105. package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
  106. package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
  107. package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
  108. package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
  109. package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
  110. package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
  111. package/package.json +10 -3
@@ -0,0 +1,218 @@
1
+ /**
2
+ * THE USERS PAGE (#users): what `mason user list|add|disable|enable|remove` does — GET /api/users, POST /api/users,
3
+ * POST /api/users/<name>/disable and /enable, DELETE /api/users/<name>. An added user's password lives in this page's memory until the person dismisses it, never in
4
+ * storage, a URL or a log line.
5
+ */
6
+ import { actId, badge, button, code, copyable, copyClick, duration, empty, esc, json, panel, pop, renderFailure, ROOMS, status, utcMinute } from "../shared.js";
7
+
8
+ /** The users as the page reads them: with each one's last use (`mason user list` asks the same), then every 30 s. */
9
+ const USERS = "/api/users?lastUsed=1";
10
+ const USERS_POST = "/api/users";
11
+ const USERS_EVERY_MS = 30_000;
12
+ /** The run's state: where a tool connects (`postgres`, src/cli/pg-url.ts postgresFacts), the Connect panel's; and the
13
+ * roles file's state (`roles`, src/cli/run-server.ts rolesState), what each user's role is held to. */
14
+ const STATE = "/api/state";
15
+
16
+ /** A time as `mason user list` stamps it: `2026-10-27 09:10`, UTC. */
17
+ const stamp = utcMinute;
18
+
19
+ /** When a user was last used, as `mason user list` says it (src/cli/user.ts `lastUsedText`, held equal by the tests): the
20
+ * time, or why not — unknown, never (added within the log's days, and no statement since), or not within the log's days. */
21
+ export function lastUsedText(u, lastUsedNote, requestLogDays, now = Date.now()) {
22
+ if (u.lastUsedAt != null) return `${stamp(u.lastUsedAt)} UTC`;
23
+ if (lastUsedNote != null) return "unknown";
24
+ if (requestLogDays != null && u.addedAt != null && now - Date.parse(u.addedAt) < requestLogDays * 86_400_000) return "never";
25
+ return `not used in the last ${requestLogDays ?? "?"} days`;
26
+ }
27
+
28
+ /** What a Disable does, as `mason user disable` says it (src/cli/user.ts `disableEffect`, held equal by the tests). */
29
+ export const disableEffect = (name) =>
30
+ `It connects no more until mason user enable ${name}, with the same password, and the sessions it holds end at their next statement.`;
31
+
32
+ /** A disabled user's state, as `mason user list` says it (src/cli/user.ts `disabledText`, held equal by the tests). */
33
+ export const disabledText = (disabledAt) => `Disabled ${stamp(disabledAt)} UTC`;
34
+
35
+ /** The question being asked, from a model's `confirming`: a name alone is Remove's (as before), else `{ name, verb }`. */
36
+ const askOf = (confirming) => confirming === null || confirming === undefined ? null : typeof confirming === "string" ? { name: confirming, verb: "remove" } : confirming;
37
+
38
+ /** a user's attributes as `mason user list` says them (src/cli/user.ts attributesText, held equal by tests/web-page.test.ts) */
39
+ export const attributesText = (a) => Object.entries(a).map(([k, v]) => `${k}=${typeof v === "string" ? v : `[${v.join(", ")}]`}`).join(", ");
40
+
41
+ /**
42
+ * THE USERS from `/api/users?lastUsed=1` (`mason user list --json`'s shape), in a table in a card: a row each — its name
43
+ * in mono with its id muted, its status as a dot and the word (Active, Disabled and when, or Removed and when), when it
44
+ * was added, when it was last used (an age, `lastUsedText`'s UTC time in a title; or the note why it is not known), then
45
+ * Queries, Disable — Enable for a disabled one, in Disable's room — and Remove. The users
46
+ * not removed first; a removed user is listed after them, muted, without actions. A Remove or a Disable asks first
47
+ * (`confirming`: the name, Remove's; or { name, verb: "disable" }), in a popover beside the row's actions; an Enable
48
+ * asks nothing. A refused action (`refused`: { name, failure }) is said there too, until dismissed. `now` is the
49
+ * page's clock, in ms. `roles`: `/api/state`'s (`mason status --json`'s): while the roles file cannot be read every
50
+ * user held to it is refused (57P03), said above the table as `mason user list`'s tip says it (src/cli/roles.ts
51
+ * rolesProblemText, held equal by tests/web-page.test.ts) and in each user's role; beside the role, the user's
52
+ * attributes (its row filters' values) and, where it lacks one its role's filters read, that its statements are refused.
53
+ */
54
+ export function renderUsers(list, confirming = null, now = Date.now(), refused = null, roles = null) {
55
+ const users = [...list.users.filter((u) => u.removedAt == null), ...list.users.filter((u) => u.removedAt != null)];
56
+ if (!users.length) return empty("No users yet", `A user is the name and password psql, a BI tool or your app connects with. Add one above, as ${code("mason user add <name>")} does: its URL, password included, is shown once.`);
57
+ const note = (what, text) => text ? `<p class="dim">${esc(what)}: ${esc(text)}.</p>` : "";
58
+ const broken = roles && roles.problem != null ? roles : null;
59
+ const brokenNote = broken ? `<p class="warn">${esc(`${broken.file ?? "mason/roles.yml"} cannot be read as it is now, so every statement of a user held to it is refused: ${broken.problem}`)}</p>` : "";
60
+ // a Remove's or a Disable's question is a popover beside the user's actions (`pop`), never a row: no row below it moves
61
+ const pops = [];
62
+ const ask = askOf(confirming);
63
+ return brokenNote + panel(`<table class="users"><thead><tr><th scope="col">User</th><th scope="col" class="role">Role</th><th scope="col" class="status">Status</th><th scope="col" class="added">Added</th><th scope="col" class="num last">Last used</th><th scope="col" class="act"></th></tr></thead><tbody>${users.map((u) => {
64
+ const active = u.removedAt == null;
65
+ const last = lastUsedText(u, list.lastUsedNote, list.requestLogDays, now);
66
+ const lastCell = !active ? "" : u.lastUsedAt != null
67
+ ? `<span title="${esc(last)}">${esc(duration(Math.max(0, (now - Date.parse(u.lastUsedAt)) / 1000)))} ago</span>` : esc(last);
68
+ const disabled = active && u.disabledAt != null;
69
+ const asking = active && ask?.name === u.name ? ask.verb : null;
70
+ const anchor = actId("users", u.name);
71
+ if (asking === "remove") pops.push(pop(anchor, `<p class="confirm">Remove ${esc(u.name)}? It connects no more, and the sessions it holds end at their next statement.</p>` +
72
+ `<p class="pop-actions">${button(`Remove ${u.name}`, { confirmRemove: u.name }, "danger small")} ${button("Cancel", { cancelRemove: "", popClose: "" }, "small")}</p>`, { kind: "dialog", label: `Remove ${u.name}?` }));
73
+ else if (asking === "disable") pops.push(pop(anchor, `<p class="confirm">Disable ${esc(u.name)}? ${esc(disableEffect(u.name))}</p>` +
74
+ `<p class="pop-actions">${button(`Disable ${u.name}`, { confirmDisable: u.name }, "danger small")} ${button("Cancel", { cancelRemove: "", popClose: "" }, "small")}</p>`, { kind: "dialog", label: `Disable ${u.name}?` }));
75
+ else if (refused?.name === u.name) pops.push(pop(anchor, `${renderFailure(refused.failure)}<p class="pop-actions">${button("Dismiss", { dismissRefused: "", popClose: "" }, "small")}</p>`));
76
+ return `<tr${active ? "" : ` class="removed"`}><th scope="row"><code class="name" title="${esc(u.name)}">${esc(u.name)}</code> <span class="dim">id ${esc(u.id)}</span></th>` +
77
+ // the role it reads as (src/cli/roles.ts userRole): its own (mason user add --role) — one the roles file does not name,
78
+ // said — or the file's default; none under a file with no default reads nothing, said; no roles file, every table
79
+ `<td>${broken ? `<span class="warn">refused: the roles file cannot be read</span>` : u.readsNothing === true ? `<span class="warn">no role, and the roles file names no default: its statements are refused</span>`
80
+ : u.role === undefined ? `<span class="dim">every table</span>`
81
+ : `${code(u.role)}${u.roleFrom === "default" ? ` <span class="dim">(default)</span>` : ""}${u.attributes ? ` <span class="dim">${esc(attributesText(u.attributes))}</span>` : ""}` +
82
+ `${u.roleKnown === false ? ` <span class="warn">not in the roles file: its statements are refused</span>` : ""}` +
83
+ `${u.missingAttributes ? ` <span class="warn">${esc(`no value for ${u.missingAttributes.join(", ")}: its statements are refused`)}</span>` : ""}`}</td>` +
84
+ `<td>${!active ? status("unknown", `Removed ${stamp(u.removedAt)} UTC`) : disabled ? status("warn", disabledText(u.disabledAt)) : status("ok", "Active")}` +
85
+ `${active && !disabled && !u.live ? ` <span class="warn">does not connect: the API key was rotated since it was added</span>` : ""}</td>` +
86
+ `<td class="mono">${esc(stamp(u.addedAt))} UTC</td><td class="num">${lastCell}</td>` +
87
+ // Queries: its statements of the last 24 hours on Observability › Queries (a removed user's too: its past)
88
+ `<td class="act" id="${esc(anchor)}"><a class="btn small" href="${esc(userQueriesHref(u.name))}" title="the statements ${esc(u.name)} sent in the last 24 hours">Queries</a>${!active ? "" : ` ${toggle(u, disabled, asking === "disable")} ${asking === "remove" ? button("Remove", {}, "danger small", { disabled: true, expanded: true })
89
+ : button("Remove", { remove: u.name }, "danger small")}`}</td></tr>`;
90
+ }).join("")}</tbody></table>`) + pops.join("") + note("Last used", list.lastUsedNote);
91
+ }
92
+
93
+ /**
94
+ * HOW A TOOL CONNECTS, once for the run (`/api/state` `postgres`: src/cli/pg-url.ts postgresFacts, what `mason status`
95
+ * prints): the host, port, database and sslmode — what a BI tool's form asks for, each with Copy — then the URL and the
96
+ * psql line with `<user>:<password>` as placeholders. None of it is a user's: a row carries what is (its name, its
97
+ * state), and a user's password is copied only from the card shown once when it is added (`renderAdded`). "" when the
98
+ * run did not say.
99
+ */
100
+ export function renderUsersConnect(pg) {
101
+ if (!pg || typeof pg.shown !== "string") return "";
102
+ const rows = [["Host", pg.host, "users-connect-host"], ["Port", String(pg.port), "users-connect-port"], ["Database", pg.database, "users-connect-database"],
103
+ ["SSL mode", pg.sslmode ?? "disable", "users-connect-sslmode"], ["URL", pg.shown, "users-connect-url"], ["psql", pg.psql, "users-connect-psql"]];
104
+ return `<div class="connect"><p class="connect-title">Connect</p>` +
105
+ `<dl class="facts">${rows.map(([k, v, id]) => `<dt>${esc(k)}</dt><dd>${copyable(id, v)}</dd>`).join("")}</dl>` +
106
+ `<p class="tip">A user's name and password go in place of ${code("<user>")} and ${code("<password>")}. The password is shown once, when the user is added: lost, remove the user and add it again.</p></div>`;
107
+ }
108
+
109
+ /** A user's statements: Observability › Queries filtered to it, over the last 24 hours (Now's 15 minutes would mostly be empty). */
110
+ export const userQueriesHref = (name) => `#observability/queries?${new URLSearchParams({ user: name, range: "24h" })}`;
111
+
112
+ /** A user's Disable, or Enable once disabled: one room (ROOMS.disable), so the row's actions keep their width; held
113
+ * while its question is asked. */
114
+ function toggle(u, disabled, asking) {
115
+ if (disabled) return button("Enable", { enable: u.name }, "small", { room: ROOMS.disable });
116
+ return asking ? button("Disable", {}, "small", { room: ROOMS.disable, disabled: true, expanded: true })
117
+ : button("Disable", { disable: u.name }, "small", { room: ROOMS.disable });
118
+ }
119
+
120
+ /**
121
+ * A USER JUST ADDED (`mason user add --json`'s shape): a prominent card with its password, shown this once, beside the
122
+ * URL to paste and its psql line, each with a Copy button — the rows `mason user add` prints, their labels capitalised.
123
+ * Mason keeps no copy, so it is never shown again. Then where its statements will show: the Queries page.
124
+ */
125
+ export function renderAdded(a) {
126
+ const rows = [["URL", a.url, "added-url"], ["psql", a.psql, "added-psql"], ["User", a.user], ["Password", a.password, "added-password"], ["Database", a.database],
127
+ ...(a.host !== undefined ? [["Host", a.host], ["Port", String(a.port)]] : []), ...(a.sslmode && a.sslmode !== "disable" ? [["SSL mode", a.sslmode]] : [])]
128
+ .filter(([, v]) => v !== undefined);
129
+ return `<div class="added" role="status"><p class="headline">The user ${esc(a.user)} added: its password is shown this once ${badge("Shown once", "warn")}</p>` +
130
+ `<dl class="facts">${rows.map(([k, v, id]) => `<dt>${esc(k)}</dt><dd>${id ? copyable(id, v) : esc(v)}</dd>`).join("")}</dl>` +
131
+ `<p class="tip">Paste it into the tool now: Mason keeps no copy of the password, and this page will not show it again. Lost, remove the user and add it again.</p>` +
132
+ `<p class="tip">${a.psql ? "Run the psql line, or connect your tool: " : ""}each statement it sends shows on <a href="#observability/queries">Observability › Queries</a>.</p>` +
133
+ `${button("I have copied it", { dismissAdded: "" }, "primary")}</div>`;
134
+ }
135
+
136
+ export const page = {
137
+ id: "users",
138
+ title: "Users",
139
+ create(ctx) {
140
+ const m = { actionFailure: null, added: null, confirming: null, refused: null };
141
+ const users = () => ctx.read(USERS)?.body ?? null;
142
+ const actions = {
143
+ async addUser(name) {
144
+ m.actionFailure = null;
145
+ const r = await ctx.ask(USERS_POST, json("POST", { name }));
146
+ if ("failure" in r) { m.actionFailure = r.failure; return false; }
147
+ m.added = r.body;
148
+ await ctx.refresh([USERS]);
149
+ return true;
150
+ },
151
+ askRemove(name) { m.confirming = name; m.actionFailure = null; m.refused = null; },
152
+ askDisable(name) { m.confirming = { name, verb: "disable" }; m.actionFailure = null; m.refused = null; },
153
+ cancelRemove() { m.confirming = null; },
154
+ /** Disable once asked, Enable at once: POST /api/users/<name>/disable|enable (mason user's own functions); a
155
+ * refusal is said beside the user's actions, as a Remove's is */
156
+ async toggleUser(name, verb) {
157
+ m.actionFailure = null;
158
+ m.refused = null;
159
+ const r = await ctx.ask(`/api/users/${encodeURIComponent(name)}/${verb}`, json("POST"));
160
+ m.confirming = null;
161
+ if ("failure" in r) { m.refused = { name, failure: r.failure }; return false; }
162
+ await ctx.refresh([USERS]);
163
+ return true;
164
+ },
165
+ confirmDisable(name) { return actions.toggleUser(name, "disable"); },
166
+ enableUser(name) { return actions.toggleUser(name, "enable"); },
167
+ async confirmRemove(name) {
168
+ m.actionFailure = null;
169
+ const r = await ctx.ask(`/api/users/${encodeURIComponent(name)}`, json("DELETE"));
170
+ m.confirming = null;
171
+ // refused: said beside the user's Remove, as its question was (nothing above the table moves it)
172
+ if ("failure" in r) { m.refused = { name, failure: r.failure }; return false; }
173
+ await ctx.refresh([USERS]);
174
+ return true;
175
+ },
176
+ dismissAdded() { m.added = null; },
177
+ dismissRefused() { m.refused = null; },
178
+ };
179
+ return {
180
+ model: m,
181
+ // the users first (their read's failure is the page's banner), and the state for the Connect panel
182
+ reads: () => [{ path: USERS, everyMs: USERS_EVERY_MS }, { path: STATE, everyMs: USERS_EVERY_MS }],
183
+ afterRead() {
184
+ const u = users();
185
+ // the user asked about went (removed), or is disabled already (by mason user disable): the question goes
186
+ const ask = askOf(m.confirming);
187
+ if (ask !== null && u !== null && !u.users.some((x) => x.name === ask.name && x.removedAt == null && (ask.verb !== "disable" || x.disabledAt == null))) m.confirming = null;
188
+ },
189
+ actions,
190
+ view() {
191
+ const u = users();
192
+ return {
193
+ // the roles file's state is the run's (/api/state roles): a file that cannot be read refuses every user held to it
194
+ users: u ? renderUsers(u, m.confirming, ctx.now().getTime(), m.refused, ctx.read(STATE)?.body?.roles ?? null) : "",
195
+ "users-connect": renderUsersConnect(ctx.read(STATE)?.body?.postgres),
196
+ added: m.added ? renderAdded(m.added) : "",
197
+ action: m.actionFailure ? renderFailure(m.actionFailure) : "",
198
+ };
199
+ },
200
+ submit(form) {
201
+ if (form.id !== "add-user") return null;
202
+ const input = form.elements.name;
203
+ return actions.addUser(input.value.trim()).then((ok) => { if (ok) input.value = ""; });
204
+ },
205
+ click(d, button, { doc, win }) {
206
+ if (d.remove !== undefined) return actions.askRemove(d.remove) ?? true;
207
+ if (d.cancelRemove !== undefined) return actions.cancelRemove() ?? true;
208
+ if (d.confirmRemove !== undefined) return actions.confirmRemove(d.confirmRemove);
209
+ if (d.disable !== undefined) return actions.askDisable(d.disable) ?? true;
210
+ if (d.confirmDisable !== undefined) return actions.confirmDisable(d.confirmDisable);
211
+ if (d.enable !== undefined) return actions.enableUser(d.enable);
212
+ if (d.dismissAdded !== undefined) return actions.dismissAdded() ?? true;
213
+ if (d.dismissRefused !== undefined) return actions.dismissRefused() ?? true;
214
+ return copyClick(d, button, { doc, win });
215
+ },
216
+ };
217
+ },
218
+ };