@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.
Files changed (155) hide show
  1. package/README.md +101 -73
  2. package/dist/src/api/error.js +3 -0
  3. package/dist/src/api/postgres/catalog-worker.js +49 -2
  4. package/dist/src/api/postgres/catalog.js +368 -25
  5. package/dist/src/api/postgres/encode.js +3 -1
  6. package/dist/src/api/postgres/explain.js +3 -2
  7. package/dist/src/api/postgres/listener.js +11 -3
  8. package/dist/src/api/postgres/protocol.js +39 -23
  9. package/dist/src/api/postgres/proxy.js +283 -0
  10. package/dist/src/api/postgres/schema.js +6 -0
  11. package/dist/src/api/postgres/server.js +1617 -245
  12. package/dist/src/api/postgres/translate.js +127 -10
  13. package/dist/src/api/request-log.js +18 -5
  14. package/dist/src/cdc/poll.js +5 -5
  15. package/dist/src/cli/background.js +47 -25
  16. package/dist/src/cli/bridge-stdin.js +101 -0
  17. package/dist/src/cli/cancel.js +75 -0
  18. package/dist/src/cli/command.js +170 -11
  19. package/dist/src/cli/commands.js +804 -209
  20. package/dist/src/cli/control.js +59 -0
  21. package/dist/src/cli/credentials.js +182 -0
  22. package/dist/src/cli/demo.js +20 -11
  23. package/dist/src/cli/deploy.js +574 -0
  24. package/dist/src/cli/dev.js +491 -130
  25. package/dist/src/cli/doctor.js +131 -38
  26. package/dist/src/cli/generated-files.js +143 -0
  27. package/dist/src/cli/guides.js +175 -36
  28. package/dist/src/cli/hosted.js +850 -0
  29. package/dist/src/cli/init.js +162 -65
  30. package/dist/src/cli/login.js +289 -0
  31. package/dist/src/cli/machine.js +13 -12
  32. package/dist/src/cli/main.js +11 -5
  33. package/dist/src/cli/mason.cjs +17 -0
  34. package/dist/src/cli/mcp.js +31 -22
  35. package/dist/src/cli/metrics.js +79 -0
  36. package/dist/src/cli/output-gate.js +176 -0
  37. package/dist/src/cli/page-share.js +96 -0
  38. package/dist/src/cli/pg-tls.js +14 -4
  39. package/dist/src/cli/pg-url.js +4 -0
  40. package/dist/src/cli/pin.js +19 -14
  41. package/dist/src/cli/pool-size.cjs +85 -0
  42. package/dist/src/cli/ports.js +2 -0
  43. package/dist/src/cli/preflight.js +138 -46
  44. package/dist/src/cli/project.js +142 -4
  45. package/dist/src/cli/readiness.js +76 -6
  46. package/dist/src/cli/registry.js +65 -38
  47. package/dist/src/cli/replica-hosted.js +665 -0
  48. package/dist/src/cli/replica.js +76 -0
  49. package/dist/src/cli/replication-checks.js +7 -5
  50. package/dist/src/cli/replication-mode.js +105 -26
  51. package/dist/src/cli/request-filter.js +134 -0
  52. package/dist/src/cli/roles.js +585 -0
  53. package/dist/src/cli/run-server.js +637 -48
  54. package/dist/src/cli/runs.js +1 -1
  55. package/dist/src/cli/security.js +67 -18
  56. package/dist/src/cli/serve.js +204 -46
  57. package/dist/src/cli/share-run.js +329 -0
  58. package/dist/src/cli/share.js +286 -0
  59. package/dist/src/cli/source-edit.js +75 -17
  60. package/dist/src/cli/source-role.js +344 -0
  61. package/dist/src/cli/sources.js +306 -47
  62. package/dist/src/cli/style.js +2 -1
  63. package/dist/src/cli/supervise.js +27 -24
  64. package/dist/src/cli/sync.js +55 -25
  65. package/dist/src/cli/token-hosted.js +337 -0
  66. package/dist/src/cli/tunnel.js +200 -0
  67. package/dist/src/cli/ui-ssh.js +460 -0
  68. package/dist/src/cli/ui.js +124 -17
  69. package/dist/src/cli/usage.js +128 -0
  70. package/dist/src/cli/user-hosted.js +299 -0
  71. package/dist/src/cli/user.js +573 -65
  72. package/dist/src/cli/version.js +9 -7
  73. package/dist/src/demo/seed.js +29 -0
  74. package/dist/src/duck/analyze.js +36 -20
  75. package/dist/src/duck/cancel.js +10 -1
  76. package/dist/src/duck/collation.js +266 -0
  77. package/dist/src/duck/constant-key.js +94 -0
  78. package/dist/src/duck/distinct-key.js +283 -26
  79. package/dist/src/duck/events.js +165 -0
  80. package/dist/src/duck/executor-worker.js +20 -11
  81. package/dist/src/duck/executor.js +47 -33
  82. package/dist/src/duck/explain.js +60 -46
  83. package/dist/src/duck/extensions.js +160 -0
  84. package/dist/src/duck/functions.js +461 -63
  85. package/dist/src/duck/gaps.js +48 -21
  86. package/dist/src/duck/generated-plan.js +1070 -0
  87. package/dist/src/duck/generated.js +906 -0
  88. package/dist/src/duck/grants.js +176 -0
  89. package/dist/src/duck/held-types.js +64 -0
  90. package/dist/src/duck/history.js +127 -0
  91. package/dist/src/duck/jsonb-order.js +9 -12
  92. package/dist/src/duck/kept-route.js +6 -5
  93. package/dist/src/duck/kept.js +15 -13
  94. package/dist/src/duck/late.js +1469 -40
  95. package/dist/src/duck/libc-bound.js +341 -0
  96. package/dist/src/duck/libc-order.js +892 -126
  97. package/dist/src/duck/pg-eval-worker.js +7 -1
  98. package/dist/src/duck/pg-eval.js +6 -4
  99. package/dist/src/duck/pgtree.js +774 -23
  100. package/dist/src/duck/plan.js +369 -47
  101. package/dist/src/duck/publication.js +20 -6
  102. package/dist/src/duck/requests.js +269 -93
  103. package/dist/src/duck/resolved.js +117 -2
  104. package/dist/src/duck/rewrite.js +4584 -580
  105. package/dist/src/duck/row-filter.js +655 -0
  106. package/dist/src/duck/statement.js +19 -0
  107. package/dist/src/duck/store.js +439 -177
  108. package/dist/src/duck/stream.js +19 -10
  109. package/dist/src/duck/sync.js +1411 -368
  110. package/dist/src/duck/token.js +4 -1
  111. package/dist/src/duck/types.js +69 -12
  112. package/dist/src/duck/usage.js +326 -0
  113. package/dist/src/duck/users.js +69 -15
  114. package/dist/src/duck/verified.js +161 -1
  115. package/dist/src/duck/views.js +5 -5
  116. package/dist/src/lib/database-name.js +62 -0
  117. package/dist/src/lib/database-url.js +55 -10
  118. package/dist/src/lib/dotenv.js +17 -4
  119. package/dist/src/lib/instant.js +50 -0
  120. package/dist/src/lib/log-buffer.js +77 -0
  121. package/dist/src/lib/log-read.js +196 -0
  122. package/dist/src/lib/pg-numeric.js +390 -49
  123. package/dist/src/lib/project-version.js +23 -23
  124. package/dist/src/lib/secret-mask.js +142 -0
  125. package/dist/src/lib/table-sync.js +282 -0
  126. package/dist/src/web/app.css +706 -66
  127. package/dist/src/web/app.js +331 -409
  128. package/dist/src/web/charts.js +229 -0
  129. package/dist/src/web/index.html +82 -18
  130. package/dist/src/web/pages/queries.js +192 -0
  131. package/dist/src/web/pages/status.js +611 -0
  132. package/dist/src/web/pages/syncs.js +155 -0
  133. package/dist/src/web/pages/tables.js +439 -0
  134. package/dist/src/web/pages/usage.js +397 -0
  135. package/dist/src/web/pages/users.js +219 -0
  136. package/dist/src/web/shared.js +584 -0
  137. package/dist/src/web/vendor/README.md +25 -0
  138. package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
  139. package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
  140. package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
  141. package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
  142. package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
  143. package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
  144. package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
  145. package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
  146. package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
  147. package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
  148. package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
  149. package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
  150. package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
  151. package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
  152. package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
  153. package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
  154. package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
  155. package/package.json +12 -4
@@ -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 LIVE USERS is a FILE in the state folder (`.mason/users.json`), never a DuckDB table: `mason user …` runs
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 change is made under a lock file (`users.json.lock`, O_EXCL) and written beside and
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
- return { problem: `${file}: users[${i}] is not { name, id, addedAt, key } as mason user add writes it` };
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, status === 503 ? "users_unreadable" : "invalid_user");
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
- this.read = readUsers(this.file);
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
- return { filters: [], subject: undefined, user: found.name, jti: found.id };
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
- /** A held session's user, asked again before a statement: throws as `login` does when it is no longer live. */
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
- if (!now.users.some((u) => u.id === token.jti && u.name === token.user))
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) => { users.recheck(t); },
282
+ recheck: async (t) => users.recheck(t),
229
283
  recheckMs: 0,
230
284
  };
231
285
  }