@mason-data/cli 0.7.0 → 0.9.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 +101 -73
- package/dist/src/api/error.js +3 -0
- package/dist/src/api/postgres/catalog-worker.js +49 -2
- package/dist/src/api/postgres/catalog.js +368 -25
- package/dist/src/api/postgres/encode.js +3 -1
- package/dist/src/api/postgres/explain.js +3 -2
- package/dist/src/api/postgres/listener.js +11 -3
- package/dist/src/api/postgres/protocol.js +39 -23
- package/dist/src/api/postgres/proxy.js +283 -0
- package/dist/src/api/postgres/schema.js +6 -0
- package/dist/src/api/postgres/server.js +1617 -245
- package/dist/src/api/postgres/translate.js +127 -10
- package/dist/src/api/request-log.js +18 -5
- package/dist/src/cdc/poll.js +5 -5
- package/dist/src/cli/background.js +47 -25
- package/dist/src/cli/bridge-stdin.js +101 -0
- package/dist/src/cli/cancel.js +75 -0
- package/dist/src/cli/command.js +170 -11
- package/dist/src/cli/commands.js +804 -209
- package/dist/src/cli/control.js +59 -0
- package/dist/src/cli/credentials.js +182 -0
- package/dist/src/cli/demo.js +20 -11
- package/dist/src/cli/deploy.js +574 -0
- package/dist/src/cli/dev.js +491 -130
- package/dist/src/cli/doctor.js +131 -38
- package/dist/src/cli/generated-files.js +143 -0
- package/dist/src/cli/guides.js +175 -36
- package/dist/src/cli/hosted.js +850 -0
- package/dist/src/cli/init.js +162 -65
- package/dist/src/cli/login.js +289 -0
- package/dist/src/cli/machine.js +13 -12
- package/dist/src/cli/main.js +11 -5
- package/dist/src/cli/mason.cjs +17 -0
- package/dist/src/cli/mcp.js +31 -22
- package/dist/src/cli/metrics.js +79 -0
- package/dist/src/cli/output-gate.js +176 -0
- package/dist/src/cli/page-share.js +96 -0
- package/dist/src/cli/pg-tls.js +14 -4
- package/dist/src/cli/pg-url.js +4 -0
- package/dist/src/cli/pin.js +19 -14
- package/dist/src/cli/pool-size.cjs +85 -0
- package/dist/src/cli/ports.js +2 -0
- package/dist/src/cli/preflight.js +138 -46
- package/dist/src/cli/project.js +142 -4
- package/dist/src/cli/readiness.js +76 -6
- package/dist/src/cli/registry.js +65 -38
- package/dist/src/cli/replica-hosted.js +665 -0
- package/dist/src/cli/replica.js +76 -0
- package/dist/src/cli/replication-checks.js +7 -5
- package/dist/src/cli/replication-mode.js +105 -26
- package/dist/src/cli/request-filter.js +134 -0
- package/dist/src/cli/roles.js +585 -0
- package/dist/src/cli/run-server.js +637 -48
- package/dist/src/cli/runs.js +1 -1
- package/dist/src/cli/security.js +67 -18
- package/dist/src/cli/serve.js +204 -46
- package/dist/src/cli/share-run.js +329 -0
- package/dist/src/cli/share.js +286 -0
- package/dist/src/cli/source-edit.js +75 -17
- package/dist/src/cli/source-role.js +344 -0
- package/dist/src/cli/sources.js +306 -47
- package/dist/src/cli/style.js +2 -1
- package/dist/src/cli/supervise.js +27 -24
- package/dist/src/cli/sync.js +55 -25
- package/dist/src/cli/token-hosted.js +337 -0
- package/dist/src/cli/tunnel.js +200 -0
- package/dist/src/cli/ui-ssh.js +460 -0
- package/dist/src/cli/ui.js +124 -17
- package/dist/src/cli/usage.js +128 -0
- package/dist/src/cli/user-hosted.js +299 -0
- package/dist/src/cli/user.js +573 -65
- package/dist/src/cli/version.js +9 -7
- package/dist/src/demo/seed.js +29 -0
- package/dist/src/duck/analyze.js +36 -20
- package/dist/src/duck/cancel.js +10 -1
- package/dist/src/duck/collation.js +266 -0
- package/dist/src/duck/constant-key.js +94 -0
- package/dist/src/duck/distinct-key.js +283 -26
- package/dist/src/duck/events.js +165 -0
- package/dist/src/duck/executor-worker.js +20 -11
- package/dist/src/duck/executor.js +47 -33
- package/dist/src/duck/explain.js +60 -46
- package/dist/src/duck/extensions.js +160 -0
- package/dist/src/duck/functions.js +461 -63
- package/dist/src/duck/gaps.js +48 -21
- package/dist/src/duck/generated-plan.js +1070 -0
- package/dist/src/duck/generated.js +906 -0
- package/dist/src/duck/grants.js +176 -0
- package/dist/src/duck/held-types.js +64 -0
- package/dist/src/duck/history.js +127 -0
- package/dist/src/duck/jsonb-order.js +9 -12
- package/dist/src/duck/kept-route.js +6 -5
- package/dist/src/duck/kept.js +15 -13
- package/dist/src/duck/late.js +1469 -40
- package/dist/src/duck/libc-bound.js +341 -0
- package/dist/src/duck/libc-order.js +892 -126
- package/dist/src/duck/pg-eval-worker.js +7 -1
- package/dist/src/duck/pg-eval.js +6 -4
- package/dist/src/duck/pgtree.js +774 -23
- package/dist/src/duck/plan.js +369 -47
- package/dist/src/duck/publication.js +20 -6
- package/dist/src/duck/requests.js +269 -93
- package/dist/src/duck/resolved.js +117 -2
- package/dist/src/duck/rewrite.js +4584 -580
- package/dist/src/duck/row-filter.js +655 -0
- package/dist/src/duck/statement.js +19 -0
- package/dist/src/duck/store.js +439 -177
- package/dist/src/duck/stream.js +19 -10
- package/dist/src/duck/sync.js +1411 -368
- package/dist/src/duck/token.js +4 -1
- package/dist/src/duck/types.js +69 -12
- package/dist/src/duck/usage.js +326 -0
- package/dist/src/duck/users.js +69 -15
- package/dist/src/duck/verified.js +161 -1
- package/dist/src/duck/views.js +5 -5
- package/dist/src/lib/database-name.js +62 -0
- package/dist/src/lib/database-url.js +55 -10
- package/dist/src/lib/dotenv.js +17 -4
- package/dist/src/lib/instant.js +50 -0
- package/dist/src/lib/log-buffer.js +77 -0
- package/dist/src/lib/log-read.js +196 -0
- package/dist/src/lib/pg-numeric.js +390 -49
- package/dist/src/lib/project-version.js +23 -23
- package/dist/src/lib/secret-mask.js +142 -0
- package/dist/src/lib/table-sync.js +282 -0
- package/dist/src/web/app.css +706 -66
- package/dist/src/web/app.js +331 -409
- package/dist/src/web/charts.js +229 -0
- package/dist/src/web/index.html +82 -18
- package/dist/src/web/pages/queries.js +192 -0
- package/dist/src/web/pages/status.js +611 -0
- package/dist/src/web/pages/syncs.js +155 -0
- package/dist/src/web/pages/tables.js +439 -0
- package/dist/src/web/pages/usage.js +397 -0
- package/dist/src/web/pages/users.js +219 -0
- package/dist/src/web/shared.js +584 -0
- package/dist/src/web/vendor/README.md +25 -0
- package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
- package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
- package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
- package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
- package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
- package/package.json +12 -4
package/dist/src/duck/users.js
CHANGED
|
@@ -11,10 +11,14 @@
|
|
|
11
11
|
* statement of its session is attributed to that user, and rotating the API key removes every user at once (no MAC
|
|
12
12
|
* verifies under the new key). It never collides with a token: a token's MAC is 43 base64url characters.
|
|
13
13
|
*
|
|
14
|
-
* THE LIST OF
|
|
14
|
+
* THE LIST OF USERS is a FILE in the state folder (`.mason/users.json`), never a DuckDB table: `mason user …` runs
|
|
15
15
|
* as a second process beside the run, which holds DuckDB's file lock. It holds each user's name, id, when it was added
|
|
16
16
|
* and the API key's fingerprint at the time (`key`, a MAC of a constant: no secret, it says which key a user was added
|
|
17
|
-
* under) — never a password. A
|
|
17
|
+
* under) — never a password. A removed user STAYS in it with `removedAt` (so a list can show who was removed, and when):
|
|
18
|
+
* only the entries without one are live, and the listener reads only those. A DISABLED user stays too, with
|
|
19
|
+
* `disabledAt` (`mason user disable`): its password still verifies, but its login is refused as Postgres refuses a role
|
|
20
|
+
* without LOGIN (28000, "is not permitted to log in") and its sessions end at their next statement, until `mason user
|
|
21
|
+
* enable` clears it — the same id, so the same password connects again. A change is made under a lock file (`users.json.lock`, O_EXCL) and written beside and
|
|
18
22
|
* renamed, so a reader sees the old list or the new one, never half of either, and two `mason user add` at once cannot
|
|
19
23
|
* both win a name. The listener RE-READS it (`LiveUsers`: a stat at every connection and before every statement of a
|
|
20
24
|
* user's session, the file read again when its inode, size or mtime moved), so a removal refuses the next connection
|
|
@@ -24,13 +28,27 @@
|
|
|
24
28
|
import * as crypto from "node:crypto";
|
|
25
29
|
import * as fs from "node:fs";
|
|
26
30
|
import * as path from "node:path";
|
|
27
|
-
import { ApiError } from "../api/error.js";
|
|
31
|
+
import { ApiError, USER_DISABLED } from "../api/error.js";
|
|
28
32
|
import { tokenProblem } from "./token.js";
|
|
33
|
+
import { ATTRIBUTE_NAME_RE } from "./row-filter.js";
|
|
29
34
|
export const USERS_FILE = "users.json";
|
|
30
35
|
/** A user's name: what a BI tool's form calls the user (Postgres's NAMEDATALEN, 63), lower case so a form that
|
|
31
36
|
* capitalises it is told so, never another user. */
|
|
32
37
|
export const USER_NAME_RE = /^[a-z0-9][a-z0-9_.-]{0,62}$/;
|
|
33
38
|
export const USER_NAME_RULE = "1 to 63 lower case letters, digits, \"_\", \".\" or \"-\", starting with a letter or digit (e.g. metabase)";
|
|
39
|
+
/** a user's attributes as the file keeps them: a name each, a string or a list of strings */
|
|
40
|
+
function attributesValid(v) {
|
|
41
|
+
// a NUL ends the statement it is bound into, wherever it is: every statement of the user would fail as a syntax error
|
|
42
|
+
const value = (x) => typeof x === "string" && !x.includes("\0");
|
|
43
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) &&
|
|
44
|
+
Object.entries(v).every(([k, x]) => ATTRIBUTE_NAME_RE.test(k) && (value(x) || (Array.isArray(x) && x.every(value))));
|
|
45
|
+
}
|
|
46
|
+
/** Whether a user has not been removed (it may still not connect: see `underKey`). */
|
|
47
|
+
export const isActive = (u) => u.removedAt === undefined;
|
|
48
|
+
/** Whether a user is disabled (`mason user disable`): kept, its password unchanged, its logins refused until enabled. */
|
|
49
|
+
export const isDisabled = (u) => u.disabledAt !== undefined;
|
|
50
|
+
/** A role's name as the users file may carry it (src/cli/roles.ts ROLE_NAME_RE, which the roles file holds it to). */
|
|
51
|
+
const ROLE_RE = /^[a-z0-9][a-z0-9_-]{0,62}$/;
|
|
34
52
|
const PASSWORD_RE = /^mason_([0-9a-f]{16})_([0-9a-f]{32})$/;
|
|
35
53
|
const mac = (apiKey, name, id) => crypto.createHmac("sha256", apiKey).update(`mason-user:${name}:${id}`).digest("hex").slice(0, 32);
|
|
36
54
|
/** Which API key a user was added under, without the key: a MAC of a constant, 16 hex digits. */
|
|
@@ -41,6 +59,8 @@ export const newUserId = () => crypto.randomBytes(8).toString("hex");
|
|
|
41
59
|
export const userPassword = (apiKey, name, id) => `mason_${id}_${mac(apiKey, name, id)}`;
|
|
42
60
|
/** Whether a password is a user's by its shape (else it is read as a token, src/duck/token.ts). */
|
|
43
61
|
export const isUserPassword = (password) => PASSWORD_RE.test(password);
|
|
62
|
+
/** What every `mason share` password begins with (src/cli/share-run.ts `newSharePassword`): no user's has it, `share` being no hex. */
|
|
63
|
+
export const SHARE_PASSWORD_PREFIX = "mason_share_";
|
|
44
64
|
/** Whether a user was added under this API key (one rotated since is no longer live). */
|
|
45
65
|
export const underKey = (u, apiKey) => u.key === keyFingerprint(apiKey);
|
|
46
66
|
/** The users the file lists; none when there is no file; why not, when it cannot be read — never an empty list then. */
|
|
@@ -71,10 +91,19 @@ function parseUsers(file, text) {
|
|
|
71
91
|
for (const [i, u] of list.entries()) {
|
|
72
92
|
const o = u;
|
|
73
93
|
if (typeof o !== "object" || o === null || typeof o.name !== "string" || !USER_NAME_RE.test(o.name) || typeof o.id !== "string" || !/^[0-9a-f]{16}$/.test(o.id) ||
|
|
74
|
-
typeof o.addedAt !== "string" || typeof o.key !== "string")
|
|
75
|
-
|
|
94
|
+
typeof o.addedAt !== "string" || typeof o.key !== "string" || (o.role !== undefined && (typeof o.role !== "string" || !ROLE_RE.test(o.role))) ||
|
|
95
|
+
(o.attributes !== undefined && !attributesValid(o.attributes))) {
|
|
96
|
+
return { problem: `${file}: users[${i}] is not { name, id, addedAt, key, role?, attributes?[, removedAt][, disabledAt] } as mason user add writes it` };
|
|
97
|
+
}
|
|
98
|
+
if (o.removedAt !== undefined && typeof o.removedAt !== "string") {
|
|
99
|
+
return { problem: `${file}: users[${i}].removedAt is not a timestamp as mason user remove writes it` };
|
|
100
|
+
}
|
|
101
|
+
if (o.disabledAt !== undefined && typeof o.disabledAt !== "string") {
|
|
102
|
+
return { problem: `${file}: users[${i}].disabledAt is not a timestamp as mason user disable writes it` };
|
|
76
103
|
}
|
|
77
|
-
users.push({ name: o.name, id: o.id, addedAt: o.addedAt, key: o.key })
|
|
104
|
+
users.push({ name: o.name, id: o.id, addedAt: o.addedAt, key: o.key, ...(typeof o.role === "string" ? { role: o.role } : {}),
|
|
105
|
+
...(attributesValid(o.attributes) ? { attributes: o.attributes } : {}),
|
|
106
|
+
...(o.removedAt !== undefined ? { removedAt: o.removedAt } : {}), ...(o.disabledAt !== undefined ? { disabledAt: o.disabledAt } : {}) });
|
|
78
107
|
}
|
|
79
108
|
return { users };
|
|
80
109
|
}
|
|
@@ -140,8 +169,8 @@ export function changeUsers(file, change) {
|
|
|
140
169
|
}
|
|
141
170
|
// ------------------------------------------------------------------ the listener's view
|
|
142
171
|
/** The ApiError a refused user login carries: the user it asked for, so the request log names who still tries. */
|
|
143
|
-
function refusal(status, message, user) {
|
|
144
|
-
const e = new ApiError(status, message,
|
|
172
|
+
function refusal(status, message, user, code = status === 503 ? "users_unreadable" : "invalid_user") {
|
|
173
|
+
const e = new ApiError(status, message, code);
|
|
145
174
|
e.userName = user;
|
|
146
175
|
return e;
|
|
147
176
|
}
|
|
@@ -156,7 +185,7 @@ export class LiveUsers {
|
|
|
156
185
|
seen = null;
|
|
157
186
|
read = { users: [] };
|
|
158
187
|
constructor(file, apiKey) { this.file = file; this.apiKey = apiKey; }
|
|
159
|
-
/** the users now; why not, when the file cannot be read */
|
|
188
|
+
/** the users not removed now (disabled ones included: their login is refused by name); why not, when the file cannot be read */
|
|
160
189
|
now() {
|
|
161
190
|
let stamp;
|
|
162
191
|
try {
|
|
@@ -169,7 +198,9 @@ export class LiveUsers {
|
|
|
169
198
|
stamp = "none";
|
|
170
199
|
}
|
|
171
200
|
if (stamp !== this.seen || "problem" in this.read) {
|
|
172
|
-
|
|
201
|
+
const r = readUsers(this.file);
|
|
202
|
+
// a removed user is kept in the file for the list: it is no user to the listener
|
|
203
|
+
this.read = "problem" in r ? r : { users: r.users.filter(isActive) };
|
|
173
204
|
this.seen = stamp;
|
|
174
205
|
}
|
|
175
206
|
return this.read;
|
|
@@ -197,35 +228,58 @@ export class LiveUsers {
|
|
|
197
228
|
// is the login — to anyone holding only the password (found on the test server)
|
|
198
229
|
if (found.name !== user)
|
|
199
230
|
throw refusal(401, "not a password of this project's users: mason user list shows them", user);
|
|
200
|
-
|
|
231
|
+
// disabled: refused only once its password verified (so the state is told to the holder of the login alone), in
|
|
232
|
+
// Postgres's words for a role without LOGIN, 28000 (src/api/postgres/server.ts reads the code)
|
|
233
|
+
if (isDisabled(found))
|
|
234
|
+
throw refusal(401, disabledMessage(found.name), user, USER_DISABLED);
|
|
235
|
+
return { filters: [], subject: undefined, user: found.name, jti: found.id, ...(found.role !== undefined ? { role: found.role } : {}), ...attributesOf(found) };
|
|
201
236
|
}
|
|
202
|
-
/**
|
|
237
|
+
/**
|
|
238
|
+
* A held session's user, asked again before a statement: throws as `login` does when it is no longer live; else the
|
|
239
|
+
* token as the file has the user NOW — its role as `mason user add --role` wrote it, so a role given or taken
|
|
240
|
+
* applies at the user's next statement, without a restart.
|
|
241
|
+
*/
|
|
203
242
|
recheck(token) {
|
|
204
243
|
if (token.user === undefined)
|
|
205
|
-
return;
|
|
244
|
+
return token;
|
|
206
245
|
const now = this.now();
|
|
207
246
|
if ("problem" in now)
|
|
208
247
|
throw refusal(503, `the list of users cannot be read: ${now.problem}`, token.user);
|
|
209
|
-
|
|
248
|
+
const found = now.users.find((u) => u.id === token.jti && u.name === token.user);
|
|
249
|
+
if (found === undefined)
|
|
210
250
|
throw refusal(401, `the user "${token.user}" was removed`, token.user);
|
|
251
|
+
if (isDisabled(found))
|
|
252
|
+
throw refusal(401, `the user "${token.user}" was disabled`, token.user, USER_DISABLED);
|
|
253
|
+
if (found.role === token.role && JSON.stringify(found.attributes ?? null) === JSON.stringify(token.attributes ?? null))
|
|
254
|
+
return token;
|
|
255
|
+
const { role: _was, attributes: _had, ...rest } = token;
|
|
256
|
+
return { ...rest, ...(found.role !== undefined ? { role: found.role } : {}), ...attributesOf(found) };
|
|
211
257
|
}
|
|
212
258
|
}
|
|
259
|
+
/** a user's attributes as its session's token carries them (src/api/postgres/server.ts PgToken) */
|
|
260
|
+
const attributesOf = (u) => u.attributes !== undefined && Object.keys(u.attributes).length > 0 ? { attributes: u.attributes } : {};
|
|
261
|
+
/** A disabled user's refused login: Postgres's sentence for a role without LOGIN, and it alone — why is its DETAIL,
|
|
262
|
+
* the way out its HINT (src/api/postgres/server.ts), as Postgres splits them. */
|
|
263
|
+
export const disabledMessage = (name) => `role "${name}" is not permitted to log in`;
|
|
213
264
|
/**
|
|
214
265
|
* HOW THE DUCKDB RUN'S LISTENER TAKES A PASSWORD (src/cli/dev.ts; the tests wire the same): a user's password (its shape)
|
|
215
266
|
* is the users file's to judge, and its session is asked again before every statement (a stat); anything else is read as a
|
|
216
267
|
* token, the kind `mason connect` minted before `mason user` (src/duck/token.ts: the API key used as a password is refused by name).
|
|
268
|
+
* A `mason share` password is refused by name too: it was never this port's, and making a user is not the way out.
|
|
217
269
|
*/
|
|
218
270
|
export function listenerLogins(users, apiKey) {
|
|
219
271
|
return {
|
|
220
272
|
authenticate: (password, user) => {
|
|
221
273
|
if (isUserPassword(password))
|
|
222
274
|
return users.login(user, password);
|
|
275
|
+
if (password.startsWith(SHARE_PASSWORD_PREFIX))
|
|
276
|
+
throw new Error("a mason share password logs in only at the address mason share printed, never on this port");
|
|
223
277
|
const problem = tokenProblem(apiKey, password);
|
|
224
278
|
if (problem !== null)
|
|
225
279
|
throw new Error(problem);
|
|
226
280
|
return { filters: [], subject: undefined };
|
|
227
281
|
},
|
|
228
|
-
recheck: async (t) =>
|
|
282
|
+
recheck: async (t) => users.recheck(t),
|
|
229
283
|
recheckMs: 0,
|
|
230
284
|
};
|
|
231
285
|
}
|