@mason-data/cli 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/src/api/error.js +4 -0
- package/dist/src/api/postgres/proxy.js +6 -160
- package/dist/src/api/postgres/server.js +22 -4
- package/dist/src/cli/command.js +169 -5
- package/dist/src/cli/commands.js +290 -157
- package/dist/src/cli/deploy.js +134 -28
- package/dist/src/cli/doctor.js +14 -11
- package/dist/src/cli/guides.js +20 -11
- package/dist/src/cli/hosted.js +1 -1
- package/dist/src/cli/init.js +110 -25
- package/dist/src/cli/login.js +20 -17
- package/dist/src/cli/machine.js +125 -40
- package/dist/src/cli/main.js +51 -8
- package/dist/src/cli/mcp.js +15 -12
- package/dist/src/cli/project.js +1 -1
- package/dist/src/cli/registry.js +80 -35
- package/dist/src/cli/replica-hosted.js +761 -102
- package/dist/src/cli/replica.js +43 -5
- package/dist/src/cli/replication-mode.js +5 -0
- package/dist/src/cli/request-filter.js +16 -6
- package/dist/src/cli/run-server.js +25 -2
- package/dist/src/cli/secret-input.js +193 -0
- package/dist/src/cli/serve.js +2 -2
- package/dist/src/cli/share.js +20 -15
- package/dist/src/cli/source-role.js +14 -15
- package/dist/src/cli/sources.js +20 -3
- package/dist/src/cli/style.js +14 -0
- package/dist/src/cli/sync.js +17 -8
- package/dist/src/cli/token-hosted.js +30 -24
- package/dist/src/cli/tunnel.js +1 -1
- package/dist/src/cli/ui-hosted.js +163 -0
- package/dist/src/cli/ui-ssh.js +3 -3
- package/dist/src/cli/ui.js +67 -26
- package/dist/src/cli/usage.js +19 -15
- package/dist/src/cli/user-hosted.js +78 -13
- package/dist/src/cli/user.js +168 -50
- package/dist/src/duck/users.js +105 -9
- package/dist/src/lib/cidr.js +177 -0
- package/dist/src/lib/table-sync.js +69 -19
- package/package.json +6 -6
package/dist/src/cli/usage.js
CHANGED
|
@@ -12,9 +12,10 @@ import { fileSize, guarded, runFailure } from "./commands.js";
|
|
|
12
12
|
import { timeOf } from "./request-filter.js";
|
|
13
13
|
import { duration, msText } from "./readiness.js";
|
|
14
14
|
import { SIZE_EVERY_MS } from "../duck/usage.js";
|
|
15
|
+
import { parseDuration } from "../lib/table-sync.js";
|
|
15
16
|
const out = (m) => { process.stdout.write(`${m}\n`); };
|
|
16
17
|
const n = (x) => x.toLocaleString("en-US");
|
|
17
|
-
export const USAGE_USAGE = `usage: mason usage [--since <when>] [--until <when>] [--step <
|
|
18
|
+
export const USAGE_USAGE = `usage: mason usage [--since <when>] [--until <when>] [--step <duration>] [--json]
|
|
18
19
|
|
|
19
20
|
What Mason was and did over time, from a sample it takes every minute and keeps MASON_HISTORY_DAYS days
|
|
20
21
|
(default 7): a line each span — the statements of the Postgres API (how many, how many were refused, failed or
|
|
@@ -29,15 +30,18 @@ export const USAGE_USAGE = `usage: mason usage [--since <when>] [--until <when>]
|
|
|
29
30
|
|
|
30
31
|
--since <when> from when: an age (90s, 30m, 6h, 7d) or an ISO 8601 date or time with its zone; default 1h
|
|
31
32
|
--until <when> until when, the same way; default now
|
|
32
|
-
--step <
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
--step <duration> a line per span (1m to the days kept; default 5m; a bare number is seconds), or 0 for every
|
|
34
|
+
sample as it was taken
|
|
35
|
+
--json as data: its shape is at the end
|
|
36
|
+
|
|
37
|
+
The --json answer: { since, until, stepSeconds (null: every sample), sampleSeconds, keptDays, samples: [{ at
|
|
38
|
+
(with a step, the span's start), seconds, copyBytes, copyRows, tablesAdded, tablesCopied,
|
|
39
|
+
tablesFailed, oldestSyncSeconds, statements, statementsRefused, statementsFailed, statementsCancelled,
|
|
40
|
+
rowsSent, bytesSent, durationP50Ms, durationP95Ms, streamLagBytes, streamLagSeconds, rssBytes,
|
|
41
|
+
heapUsedBytes, copyMemoryBytes, copyMemoryLimitBytes, slotWalHeldBytes (replication: the WAL the
|
|
42
|
+
slot holds on the database), diskFreeBytes (free where the synced data is; with a step, the lowest),
|
|
43
|
+
sessionsOpen (the Postgres API's sessions logged in) }],
|
|
44
|
+
copySizes: [{ table, bytes, at }], next }
|
|
41
45
|
`;
|
|
42
46
|
export const USAGE = {
|
|
43
47
|
needs: "run",
|
|
@@ -47,13 +51,13 @@ export const USAGE = {
|
|
|
47
51
|
options: { since: { type: "string" }, until: { type: "string" }, step: { type: "string" }, json: { type: "boolean" } },
|
|
48
52
|
json: ["since", "until", "stepSeconds", "sampleSeconds", "keptDays", "samples", "copySizes", "next"],
|
|
49
53
|
};
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
/** A span as the options write it (the command line's one duration grammar, src/lib/table-sync.ts `parseDuration`:
|
|
55
|
+
* 90s, 30m, 6h, 7d, a bare number of seconds; 0 for every sample), in whole seconds, or null when it is not one. */
|
|
52
56
|
export function spanSeconds(text) {
|
|
53
57
|
if (text.trim() === "0")
|
|
54
58
|
return 0;
|
|
55
|
-
const
|
|
56
|
-
return
|
|
59
|
+
const d = parseDuration(text, { zero: true });
|
|
60
|
+
return "problem" in d || !Number.isInteger(d.seconds) ? null : d.seconds;
|
|
57
61
|
}
|
|
58
62
|
/** One sample on one line: the time, the statements, those refused, failed or cancelled, p95, rows sent, the copy, its rows, the stalest copy, the lag, the memory. */
|
|
59
63
|
export function sampleLine(s, withLag) {
|
|
@@ -87,7 +91,7 @@ export async function usage(argv, now = Date.now()) {
|
|
|
87
91
|
return failed("usage", { ...t, usage: USAGE_USAGE }, { json });
|
|
88
92
|
const step = spanSeconds(v.step ?? "5m");
|
|
89
93
|
if (step === null)
|
|
90
|
-
return failed("usage", { message: `--step is a
|
|
94
|
+
return failed("usage", { message: `--step is a duration (300, 1m, 5m, 1h, 1d: a bare number is seconds) or 0 for every sample, not ${JSON.stringify(v.step)}`, code: "usage", usage: USAGE_USAGE }, { json });
|
|
91
95
|
const w = await reach(USAGE, json);
|
|
92
96
|
if (w === null || !("run" in w))
|
|
93
97
|
return 1;
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* server takes for it (src/cli/run-server.ts `/api/users`), sent to the service under `/v1/replicas/{org}/{name}/users`:
|
|
5
5
|
*
|
|
6
6
|
* list GET …/users?lastUsed=1 mason user list --json's shape, beside its next
|
|
7
|
-
* add POST …/users { name, role?, set? }
|
|
7
|
+
* add POST …/users { name, role?, set?, from? } mason user add --json's shape (the run is up: its url)
|
|
8
|
+
* from POST …/users/{user}/from { from } { changed, already }: its ranges replaced (null: any address)
|
|
8
9
|
* disable POST …/users/{user}/disable { disabled, already }
|
|
9
10
|
* enable POST …/users/{user}/enable { enabled, already }
|
|
10
11
|
* remove DELETE …/users/{user} { removed }
|
|
@@ -36,6 +37,10 @@
|
|
|
36
37
|
* role, roleFrom, readsNothing, rows, list's lines, --json `ROLE_NAME_RE`; closed sets; booleans; each consistent with the others
|
|
37
38
|
* roleKnown, live
|
|
38
39
|
* attributes, missingAttributes rows, list's lines, --json `ATTRIBUTE_NAME_RE` names; each value one line (`serviceText`), bounded
|
|
40
|
+
* from (a user's address ranges) rows, list's lines, add's row, --json null, or 1 to MAX_RANGES ranges each in the one form
|
|
41
|
+
* src/duck/users.ts `userRanges` writes back (`heldRanges`);
|
|
42
|
+
* on add and from, exactly the ranges sent, else the answer
|
|
43
|
+
* is refused whole (`sameRanges`, `unconfirmed`: fail closed)
|
|
39
44
|
* lastUsedNote, urlNote list's tip "Last used: …", --json one line (`serviceText`), bounded
|
|
40
45
|
* requestLogDays list's lines, --json a whole number, 1 to 36500
|
|
41
46
|
* already the headline chosen, --json a boolean
|
|
@@ -57,12 +62,12 @@ import { replicaText, tokenFor } from "./replica.js";
|
|
|
57
62
|
import { holdOutput } from "./output-gate.js";
|
|
58
63
|
import { err as styleErr, out as styleOut } from "./style.js";
|
|
59
64
|
import { connectionUrl, psqlLine } from "./pg-url.js";
|
|
60
|
-
import { USER_NAME_RE } from "../duck/users.js";
|
|
65
|
+
import { MAX_RANGES, USER_NAME_RE, userRanges } from "../duck/users.js";
|
|
61
66
|
import { ROLE_NAME_RE } from "./roles.js";
|
|
62
67
|
import { ATTRIBUTE_NAME_RE } from "../duck/row-filter.js";
|
|
63
68
|
import { databaseNameProblem } from "../lib/database-name.js";
|
|
64
69
|
import { NO_ROLES } from "../duck/grants.js";
|
|
65
|
-
import { addedNext, addedReport, disabledNext, disabledReport, enabledNext, enabledReport, listNext, listText, REMOVED_NEXT, removedReport, USER } from "./user.js";
|
|
70
|
+
import { addedNext, addedReport, disabledNext, disabledReport, enabledNext, enabledReport, fromNext, fromReport, listNext, listText, REMOVED_NEXT, removedReport, USER } from "./user.js";
|
|
66
71
|
/** The bound of a list's answer (a replica's users, a row each), and of the number of users in it; every other answer
|
|
67
72
|
* is `call`'s own 64 KiB. */
|
|
68
73
|
export const MAX_LIST_BYTES = 1024 * 1024;
|
|
@@ -83,7 +88,9 @@ export function requestOf(r, o) {
|
|
|
83
88
|
// a user's name is USER_NAME_RE's (checked before this): letters, digits, `_`, `.` and `-`, safe in a path as it is
|
|
84
89
|
switch (o.verb) {
|
|
85
90
|
case "list": return { method: "GET", path: `${base}?lastUsed=1` };
|
|
86
|
-
case "add": return { method: "POST", path: base, json: { name: o.name, ...(o.role !== undefined ? { role: o.role } : {}), ...(o.sets.length ? { set: [...o.sets] } : {})
|
|
91
|
+
case "add": return { method: "POST", path: base, json: { name: o.name, ...(o.role !== undefined ? { role: o.role } : {}), ...(o.sets.length ? { set: [...o.sets] } : {}),
|
|
92
|
+
...(o.from !== undefined && o.from !== null ? { from: [...o.from] } : {}) } };
|
|
93
|
+
case "from": return { method: "POST", path: `${base}/${o.name}/from`, json: { from: o.from === undefined || o.from === null ? null : [...o.from] } };
|
|
87
94
|
case "disable":
|
|
88
95
|
case "enable": return { method: "POST", path: `${base}/${o.name}/${o.verb}` };
|
|
89
96
|
case "remove": return { method: "DELETE", path: `${base}/${o.name}` };
|
|
@@ -125,6 +132,16 @@ function attributesOf(v) {
|
|
|
125
132
|
}
|
|
126
133
|
return outv;
|
|
127
134
|
}
|
|
135
|
+
/** A user's address ranges as the service sends them: null (any address), or 1 to MAX_RANGES ranges, each in the one form
|
|
136
|
+
* the one grammar writes back (src/duck/users.ts `userRanges`); undefined when they are not that. */
|
|
137
|
+
export function heldRanges(v) {
|
|
138
|
+
if (v === null)
|
|
139
|
+
return null;
|
|
140
|
+
if (!Array.isArray(v) || v.length < 1 || v.length > MAX_RANGES || !v.every((x) => typeof x === "string" && x.length <= 64))
|
|
141
|
+
return undefined;
|
|
142
|
+
const r = userRanges(v);
|
|
143
|
+
return "problem" in r || r.from.length !== v.length || r.from.some((x, i) => x !== v[i]) ? undefined : r.from;
|
|
144
|
+
}
|
|
128
145
|
const names = (v) => Array.isArray(v) && v.length >= 1 && v.length <= 64 && v.every((x) => typeof x === "string" && ATTRIBUTE_NAME_RE.test(x));
|
|
129
146
|
const timeOrNull = (v) => v === null || isServiceTime(v);
|
|
130
147
|
/** One row of a list, as `listUsers` makes it (src/cli/user.ts `UserRow`); null when it is not that shape. */
|
|
@@ -142,6 +159,10 @@ function rowOf(v) {
|
|
|
142
159
|
const role = roleOf(u);
|
|
143
160
|
if (role === null)
|
|
144
161
|
return null;
|
|
162
|
+
// a replica's Mason before address ranges sends none: it has none, so any address
|
|
163
|
+
const from = u.from === undefined ? null : heldRanges(u.from);
|
|
164
|
+
if (from === undefined)
|
|
165
|
+
return null;
|
|
145
166
|
const attributes = u.attributes === undefined ? undefined : attributesOf(u.attributes);
|
|
146
167
|
if (attributes === null)
|
|
147
168
|
return null;
|
|
@@ -150,7 +171,7 @@ function rowOf(v) {
|
|
|
150
171
|
if (u.roleKnown !== undefined && (typeof u.roleKnown !== "boolean" || role.roleFrom !== "file"))
|
|
151
172
|
return null;
|
|
152
173
|
return { name: u.name, id: u.id, addedAt: u.addedAt, removedAt: u.removedAt, disabledAt: u.disabledAt, live: u.live, lastUsedAt: u.lastUsedAt,
|
|
153
|
-
url: u.url, ...role, ...(attributes !== undefined ? { attributes } : {}),
|
|
174
|
+
url: u.url, from, ...role, ...(attributes !== undefined ? { attributes } : {}),
|
|
154
175
|
...(u.missingAttributes !== undefined ? { missingAttributes: u.missingAttributes } : {}),
|
|
155
176
|
...(typeof u.roleKnown === "boolean" ? { roleKnown: u.roleKnown } : {}) };
|
|
156
177
|
}
|
|
@@ -195,11 +216,15 @@ export function heldAdded(json, asked, secrets) {
|
|
|
195
216
|
const role = roleOf(a);
|
|
196
217
|
if (role === null)
|
|
197
218
|
return null;
|
|
219
|
+
const from = a.from === undefined ? undefined : heldRanges(a.from);
|
|
220
|
+
// null (any address) is read as no ranges, as a list's row reads it: an add that sent ranges then refuses it unconfirmed
|
|
221
|
+
if (from === undefined && a.from !== undefined)
|
|
222
|
+
return null;
|
|
198
223
|
const attributes = a.attributes === undefined ? undefined : attributesOf(a.attributes);
|
|
199
224
|
if (attributes === null)
|
|
200
225
|
return null;
|
|
201
226
|
return { user: asked, id: a.id, addedAt: a.addedAt, password: a.password, database: a.database, url, psql: psqlLine(url), host: a.host, port: a.port, sslmode: a.sslmode,
|
|
202
|
-
...role, ...(attributes !== undefined ? { attributes } : {}) };
|
|
227
|
+
...role, ...(attributes !== undefined ? { attributes } : {}), ...(from !== undefined && from !== null ? { from } : {}) };
|
|
203
228
|
}
|
|
204
229
|
/** A user as disable, enable and remove name it: the name asked, its id, when it was added, and `more` times. */
|
|
205
230
|
function userOf(v, asked, more) {
|
|
@@ -219,6 +244,14 @@ export function heldEnabled(json, asked, secrets) {
|
|
|
219
244
|
const e = userOf(json.enabled, asked, []);
|
|
220
245
|
return e === null ? null : { enabled: e, already: json.already };
|
|
221
246
|
}
|
|
247
|
+
/** `POST …/users/{user}/from`'s answer: the user asked, its ranges now (null: any address), and `already`. */
|
|
248
|
+
export function heldFrom(json, asked, secrets) {
|
|
249
|
+
if (!obj(json) || typeof json.already !== "boolean" || !carriesNoSecret(json, secrets))
|
|
250
|
+
return null;
|
|
251
|
+
const c = userOf(json.changed, asked, []);
|
|
252
|
+
const from = obj(json.changed) ? heldRanges(json.changed.from) : undefined;
|
|
253
|
+
return c === null || from === undefined ? null : { changed: { ...c, from }, already: json.already };
|
|
254
|
+
}
|
|
222
255
|
export function heldRemoved(json, asked, secrets) {
|
|
223
256
|
if (!obj(json) || !carriesNoSecret(json, secrets))
|
|
224
257
|
return null;
|
|
@@ -226,8 +259,25 @@ export function heldRemoved(json, asked, secrets) {
|
|
|
226
259
|
return r === null ? null : { removed: r };
|
|
227
260
|
}
|
|
228
261
|
// ------------------------------------------------------------------ the command
|
|
262
|
+
/** Whether the ranges an answer carries are exactly those sent (each in its one form, in order; null: any address). */
|
|
263
|
+
export const sameRanges = (got, sent) => got !== undefined && JSON.stringify(got) === JSON.stringify(sent);
|
|
264
|
+
/**
|
|
265
|
+
* AN ADDRESS LIMIT THE SERVICE DID NOT CONFIRM: its 200 does not carry the ranges sent (none, null, or others). The
|
|
266
|
+
* answer is refused whole, never printed (an add's password included): the user may now connect from more addresses
|
|
267
|
+
* than asked, which only the replica's list can say, and the way out removes it or sets the ranges again.
|
|
268
|
+
*/
|
|
269
|
+
export function unconfirmed(r, name, sent, verb) {
|
|
270
|
+
const on = `--replica ${replicaText(r)}`;
|
|
271
|
+
const asked = sent === null ? "any address" : sent.join(", ");
|
|
272
|
+
return { message: `the service did not confirm ${name}'s address limit: its answer does not carry the ranges sent (${asked})`, code: "failed",
|
|
273
|
+
why: verb === "add" ? `the replica may have added ${name} with no limit, so it would connect from any address, and its password was not shown`
|
|
274
|
+
: `the replica may have kept ${name}'s ranges as they were, or set others`,
|
|
275
|
+
fix: verb === "add" ? `mason user list ${on} says what ${name} has; if it was made without the limit, mason user remove ${name} ${on}`
|
|
276
|
+
: `mason user list ${on} says what ${name} has now; mason user from ${name} ${sent === null ? "--any" : sent.join(" ")} ${on} sets it again, or mason user remove ${name} ${on}`,
|
|
277
|
+
next: [[`mason user list ${on}`, "the replica's users and the addresses each connects from"], [`mason user remove ${name} ${on}`, "if it was made without the limit"]] };
|
|
278
|
+
}
|
|
229
279
|
/** What each subcommand asked, in the words of a failure outside the contract (`unexpected`). */
|
|
230
|
-
const STEP = { list: "the replica's users were asked for", add: "a user was added", disable: "a user was disabled", enable: "a user was enabled", remove: "a user was removed" };
|
|
280
|
+
const STEP = { list: "the replica's users were asked for", add: "a user was added", from: "a user's address ranges were set", disable: "a user was disabled", enable: "a user was enabled", remove: "a user was removed" };
|
|
231
281
|
/** `mason user <verb> … --replica <org>/<name>`: one request, its answer held and printed as the local command prints it. */
|
|
232
282
|
export async function hostedUser(r, o) {
|
|
233
283
|
const outStyle = serviceStyle(styleOut());
|
|
@@ -250,8 +300,10 @@ export async function hostedUser(r, o) {
|
|
|
250
300
|
if (a.status !== 200)
|
|
251
301
|
return fail(refused(a));
|
|
252
302
|
const outside = () => fail(unexpected(service, a, STEP[o.verb]));
|
|
303
|
+
// the target in every answer: the replica's name in --json and in the headline
|
|
304
|
+
const on = replicaText(r);
|
|
253
305
|
const print = (value, next, text) => {
|
|
254
|
-
out(o.json ? jsonText(USER, value, next) : text());
|
|
306
|
+
out(o.json ? jsonText(USER, { replica: on, ...value }, next) : text());
|
|
255
307
|
return 0;
|
|
256
308
|
};
|
|
257
309
|
switch (o.verb) {
|
|
@@ -260,7 +312,7 @@ export async function hostedUser(r, o) {
|
|
|
260
312
|
if (l === null)
|
|
261
313
|
return outside();
|
|
262
314
|
const next = onReplica(listNext(l.users), r);
|
|
263
|
-
return print({ ...l }, next, () => listText(
|
|
315
|
+
return print({ ...l }, next, () => listText(on, l, NO_ROLES, next, outStyle));
|
|
264
316
|
}
|
|
265
317
|
case "add": {
|
|
266
318
|
const added = heldAdded(a.json, o.name, secrets);
|
|
@@ -271,29 +323,42 @@ export async function hostedUser(r, o) {
|
|
|
271
323
|
fix: `mason user list --replica ${replicaText(r)} says whether it did; if so, mason user remove ${o.name} --replica ${replicaText(r)}, then add it again`,
|
|
272
324
|
next: [[`mason user list --replica ${replicaText(r)}`, "the replica's users"]] });
|
|
273
325
|
}
|
|
326
|
+
// FAIL CLOSED: ranges sent must come back as sent, or the user may have been made with none (a service that does
|
|
327
|
+
// not know `from` would make it connect from any address), and its password is not shown
|
|
328
|
+
if (o.from !== undefined && o.from !== null && !sameRanges(added.from, o.from))
|
|
329
|
+
return fail(unconfirmed(r, o.name, o.from, "add"));
|
|
274
330
|
const next = onReplica(addedNext(added.user, true), r);
|
|
275
|
-
return print({ ...added }, next, () => addedReport(added, { roles: NO_ROLES, listener: null, next }, outStyle));
|
|
331
|
+
return print({ ...added }, next, () => addedReport(added, { roles: NO_ROLES, listener: null, next, on }, outStyle));
|
|
332
|
+
}
|
|
333
|
+
case "from": {
|
|
334
|
+
const c = heldFrom(a.json, o.name, secrets);
|
|
335
|
+
if (c === null)
|
|
336
|
+
return outside();
|
|
337
|
+
if (!sameRanges(c.changed.from, o.from ?? null))
|
|
338
|
+
return fail(unconfirmed(r, o.name, o.from ?? null, "from"));
|
|
339
|
+
const next = onReplica(fromNext(c.changed.name, c.changed.from), r);
|
|
340
|
+
return print({ ...c }, next, () => fromReport(c, true, next, outStyle));
|
|
276
341
|
}
|
|
277
342
|
case "disable": {
|
|
278
343
|
const d = heldDisabled(a.json, o.name, secrets);
|
|
279
344
|
if (d === null)
|
|
280
345
|
return outside();
|
|
281
346
|
const next = onReplica(disabledNext(d.disabled.name), r);
|
|
282
|
-
return print({ ...d }, next, () => disabledReport(d, true, next, outStyle));
|
|
347
|
+
return print({ ...d }, next, () => disabledReport(d, true, next, outStyle, on));
|
|
283
348
|
}
|
|
284
349
|
case "enable": {
|
|
285
350
|
const e = heldEnabled(a.json, o.name, secrets);
|
|
286
351
|
if (e === null)
|
|
287
352
|
return outside();
|
|
288
353
|
const next = onReplica(enabledNext(e.enabled.name), r);
|
|
289
|
-
return print({ ...e }, next, () => enabledReport(e, next, outStyle));
|
|
354
|
+
return print({ ...e }, next, () => enabledReport(e, next, outStyle, on));
|
|
290
355
|
}
|
|
291
356
|
case "remove": {
|
|
292
357
|
const x = heldRemoved(a.json, o.name, secrets);
|
|
293
358
|
if (x === null)
|
|
294
359
|
return outside();
|
|
295
360
|
const next = onReplica(REMOVED_NEXT, r);
|
|
296
|
-
return print({ ...x }, next, () => removedReport(x, true, next, outStyle));
|
|
361
|
+
return print({ removedUser: x.removed, ...x }, next, () => removedReport(x, true, next, outStyle, on));
|
|
297
362
|
}
|
|
298
363
|
}
|
|
299
364
|
}
|