@mtndev/cli 1.1.0 → 2.0.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.
Files changed (260) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/cjs/__chunks/VH7oDXx4.cjs +28 -0
  3. package/cjs/commands/aws/ec2/allow-ip/handler.cjs +261 -0
  4. package/cjs/commands/aws/ec2/allow-ip/handler.d.ts +6 -0
  5. package/cjs/commands/aws/ec2/allow-ip/index.cjs +25 -0
  6. package/cjs/commands/aws/ec2/allow-ip/index.d.ts +3 -0
  7. package/cjs/commands/aws/ec2/allow-ip/schema.cjs +33 -0
  8. package/cjs/commands/aws/ec2/allow-ip/schema.d.ts +33 -0
  9. package/cjs/commands/aws/ec2/allow-ip/utils.cjs +379 -0
  10. package/cjs/commands/aws/ec2/allow-ip/utils.d.ts +189 -0
  11. package/cjs/commands/aws/iam/admin-group.cjs +111 -0
  12. package/cjs/commands/aws/iam/admin-group.d.ts +55 -0
  13. package/cjs/commands/aws/iam/break-glass/handler.cjs +127 -0
  14. package/cjs/commands/aws/iam/break-glass/handler.d.ts +7 -0
  15. package/cjs/commands/aws/iam/break-glass/index.cjs +7 -0
  16. package/cjs/commands/aws/iam/break-glass/index.d.ts +2 -0
  17. package/cjs/commands/aws/iam/break-glass/schema.cjs +21 -0
  18. package/cjs/commands/aws/iam/break-glass/schema.d.ts +25 -0
  19. package/cjs/commands/aws/iam/check-admin/handler.cjs +89 -0
  20. package/cjs/commands/aws/iam/check-admin/handler.d.ts +5 -0
  21. package/cjs/commands/aws/iam/check-admin/index.cjs +7 -0
  22. package/cjs/commands/aws/iam/check-admin/index.d.ts +2 -0
  23. package/cjs/commands/aws/iam/check-admin/schema.cjs +19 -0
  24. package/cjs/commands/aws/iam/check-admin/schema.d.ts +19 -0
  25. package/cjs/commands/aws/iam/policies/s3-restrictions-deny-non-s3-services.json +20 -0
  26. package/cjs/commands/aws/iam/policies/sts-federation.json +11 -0
  27. package/cjs/commands/aws/iam/policies/user-admins-deny-list.json +175 -0
  28. package/cjs/commands/aws/iam/policies/user-admins-deny-regions-list.json +41 -0
  29. package/cjs/commands/aws/iam/policies/user-admins-self-service-iam.json +56 -0
  30. package/cjs/commands/aws/iam/policies/user-admins-write-list.json +117 -0
  31. package/cjs/commands/aws/iam/remove-admin/handler.cjs +120 -0
  32. package/cjs/commands/aws/iam/remove-admin/handler.d.ts +6 -0
  33. package/cjs/commands/aws/iam/remove-admin/index.cjs +7 -0
  34. package/cjs/commands/aws/iam/remove-admin/index.d.ts +2 -0
  35. package/cjs/commands/aws/iam/remove-admin/schema.cjs +22 -0
  36. package/cjs/commands/aws/iam/remove-admin/schema.d.ts +28 -0
  37. package/cjs/commands/aws/iam/setup-acl/handler.cjs +227 -0
  38. package/cjs/commands/aws/iam/setup-acl/handler.d.ts +6 -0
  39. package/cjs/commands/aws/iam/setup-acl/index.cjs +13 -0
  40. package/cjs/commands/aws/iam/setup-acl/index.d.ts +3 -0
  41. package/cjs/commands/aws/iam/setup-acl/schema.cjs +19 -0
  42. package/cjs/commands/aws/iam/setup-acl/schema.d.ts +24 -0
  43. package/cjs/commands/aws/iam/setup-acl/utils.cjs +305 -0
  44. package/cjs/commands/aws/iam/setup-acl/utils.d.ts +61 -0
  45. package/cjs/commands/aws/iam/users/handler.cjs +59 -0
  46. package/cjs/commands/aws/iam/users/handler.d.ts +5 -0
  47. package/cjs/commands/aws/iam/users/index.cjs +18 -0
  48. package/cjs/commands/aws/iam/users/index.d.ts +3 -0
  49. package/cjs/commands/aws/iam/users/schema.cjs +17 -0
  50. package/cjs/commands/aws/iam/users/schema.d.ts +18 -0
  51. package/cjs/commands/aws/iam/users/utils.cjs +454 -0
  52. package/cjs/commands/aws/iam/users/utils.d.ts +119 -0
  53. package/cjs/commands/aws/index.cjs +223 -0
  54. package/cjs/commands/aws/index.d.ts +14 -0
  55. package/cjs/commands/aws/route53/add-record/handler.cjs +264 -0
  56. package/cjs/commands/aws/route53/add-record/handler.d.ts +36 -0
  57. package/cjs/commands/aws/route53/add-record/index.cjs +26 -0
  58. package/cjs/commands/aws/route53/add-record/index.d.ts +3 -0
  59. package/cjs/commands/aws/route53/add-record/schema.cjs +43 -0
  60. package/cjs/commands/aws/route53/add-record/schema.d.ts +45 -0
  61. package/cjs/commands/aws/route53/add-record/utils.cjs +404 -0
  62. package/cjs/commands/aws/route53/add-record/utils.d.ts +216 -0
  63. package/cjs/commands/aws/route53/remove-record/handler.cjs +114 -0
  64. package/cjs/commands/aws/route53/remove-record/handler.d.ts +6 -0
  65. package/cjs/commands/aws/route53/remove-record/index.cjs +5 -0
  66. package/cjs/commands/aws/route53/remove-record/index.d.ts +2 -0
  67. package/cjs/commands/aws/route53/remove-record/schema.cjs +22 -0
  68. package/cjs/commands/aws/route53/remove-record/schema.d.ts +30 -0
  69. package/cjs/commands/aws/schema.cjs +19 -0
  70. package/cjs/commands/aws/schema.d.ts +13 -0
  71. package/cjs/commands/aws/ses/send-email/handler.cjs +73 -0
  72. package/cjs/commands/aws/ses/send-email/handler.d.ts +5 -0
  73. package/cjs/commands/aws/ses/send-email/index.cjs +9 -0
  74. package/cjs/commands/aws/ses/send-email/index.d.ts +3 -0
  75. package/cjs/commands/aws/ses/send-email/schema.cjs +24 -0
  76. package/cjs/commands/aws/ses/send-email/schema.d.ts +39 -0
  77. package/cjs/commands/aws/ses/send-email/utils.cjs +87 -0
  78. package/cjs/commands/aws/ses/send-email/utils.d.ts +47 -0
  79. package/cjs/commands/aws/ses/smtp-password.cjs +72 -0
  80. package/cjs/commands/aws/ses/smtp-password.d.ts +21 -0
  81. package/cjs/commands/aws/utils/exec-aws.cjs +145 -0
  82. package/cjs/commands/aws/utils/exec-aws.d.ts +79 -0
  83. package/cjs/commands/aws/utils/resolve-policy-path.cjs +15 -0
  84. package/cjs/commands/aws/utils/resolve-policy-path.d.ts +8 -0
  85. package/cjs/commands/aws/utils/resolve-profile.cjs +93 -0
  86. package/cjs/commands/aws/utils/resolve-profile.d.ts +28 -0
  87. package/cjs/commands/aws/utils/tty.cjs +54 -0
  88. package/cjs/commands/aws/utils/tty.d.ts +24 -0
  89. package/cjs/commands/db/create-readonly-user/handler.cjs +254 -0
  90. package/cjs/commands/db/create-readonly-user/handler.d.ts +5 -0
  91. package/cjs/commands/db/create-readonly-user/index.cjs +18 -0
  92. package/cjs/commands/db/create-readonly-user/index.d.ts +3 -0
  93. package/cjs/commands/db/create-readonly-user/mysql.cjs +102 -0
  94. package/cjs/commands/db/create-readonly-user/mysql.d.ts +33 -0
  95. package/cjs/commands/db/create-readonly-user/postgres.cjs +209 -0
  96. package/cjs/commands/db/create-readonly-user/postgres.d.ts +31 -0
  97. package/cjs/commands/db/create-readonly-user/schema.cjs +39 -0
  98. package/cjs/commands/db/create-readonly-user/schema.d.ts +55 -0
  99. package/cjs/commands/db/create-readonly-user/utils.cjs +160 -0
  100. package/cjs/commands/db/create-readonly-user/utils.d.ts +112 -0
  101. package/cjs/commands/db/index.cjs +46 -0
  102. package/cjs/commands/db/index.d.ts +6 -0
  103. package/cjs/commands/doppler/handler.cjs +95 -9
  104. package/cjs/commands/doppler/index.cjs +33 -41
  105. package/cjs/commands/doppler/index.d.ts +1 -0
  106. package/cjs/commands/doppler/schema.cjs +130 -6
  107. package/cjs/commands/doppler/schema.d.ts +233 -17
  108. package/cjs/commands/doppler/set-secret.cjs +378 -0
  109. package/cjs/commands/doppler/set-secret.d.ts +113 -0
  110. package/cjs/commands/doppler/utils/exec.cjs +106 -12
  111. package/cjs/commands/doppler/utils.cjs +412 -30
  112. package/cjs/commands/doppler/utils.d.ts +32 -8
  113. package/cjs/commands/icons/handler.cjs +82 -8
  114. package/cjs/commands/icons/index.cjs +15 -31
  115. package/cjs/commands/icons/schema.cjs +101 -13
  116. package/cjs/commands/icons/schema.d.ts +1 -1
  117. package/cjs/commands/icons/utils.cjs +131 -13
  118. package/cjs/commands/index.cjs +54 -82
  119. package/cjs/commands/schema.cjs +25 -8
  120. package/cjs/index.cjs +3 -4
  121. package/esm/commands/aws/ec2/allow-ip/handler.d.ts +6 -0
  122. package/esm/commands/aws/ec2/allow-ip/handler.js +258 -0
  123. package/esm/commands/aws/ec2/allow-ip/index.d.ts +3 -0
  124. package/esm/commands/aws/ec2/allow-ip/index.js +4 -0
  125. package/esm/commands/aws/ec2/allow-ip/schema.d.ts +33 -0
  126. package/esm/commands/aws/ec2/allow-ip/schema.js +29 -0
  127. package/esm/commands/aws/ec2/allow-ip/utils.d.ts +189 -0
  128. package/esm/commands/aws/ec2/allow-ip/utils.js +363 -0
  129. package/esm/commands/aws/iam/admin-group.d.ts +55 -0
  130. package/esm/commands/aws/iam/admin-group.js +104 -0
  131. package/esm/commands/aws/iam/break-glass/handler.d.ts +7 -0
  132. package/esm/commands/aws/iam/break-glass/handler.js +124 -0
  133. package/esm/commands/aws/iam/break-glass/index.d.ts +2 -0
  134. package/esm/commands/aws/iam/break-glass/index.js +4 -0
  135. package/esm/commands/aws/iam/break-glass/schema.d.ts +25 -0
  136. package/esm/commands/aws/iam/break-glass/schema.js +19 -0
  137. package/esm/commands/aws/iam/check-admin/handler.d.ts +5 -0
  138. package/esm/commands/aws/iam/check-admin/handler.js +86 -0
  139. package/esm/commands/aws/iam/check-admin/index.d.ts +2 -0
  140. package/esm/commands/aws/iam/check-admin/index.js +4 -0
  141. package/esm/commands/aws/iam/check-admin/schema.d.ts +19 -0
  142. package/esm/commands/aws/iam/check-admin/schema.js +17 -0
  143. package/esm/commands/aws/iam/policies/s3-restrictions-deny-non-s3-services.json +20 -0
  144. package/esm/commands/aws/iam/policies/sts-federation.json +11 -0
  145. package/esm/commands/aws/iam/policies/user-admins-deny-list.json +175 -0
  146. package/esm/commands/aws/iam/policies/user-admins-deny-regions-list.json +41 -0
  147. package/esm/commands/aws/iam/policies/user-admins-self-service-iam.json +56 -0
  148. package/esm/commands/aws/iam/policies/user-admins-write-list.json +117 -0
  149. package/esm/commands/aws/iam/remove-admin/handler.d.ts +6 -0
  150. package/esm/commands/aws/iam/remove-admin/handler.js +117 -0
  151. package/esm/commands/aws/iam/remove-admin/index.d.ts +2 -0
  152. package/esm/commands/aws/iam/remove-admin/index.js +4 -0
  153. package/esm/commands/aws/iam/remove-admin/schema.d.ts +28 -0
  154. package/esm/commands/aws/iam/remove-admin/schema.js +20 -0
  155. package/esm/commands/aws/iam/setup-acl/handler.d.ts +6 -0
  156. package/esm/commands/aws/iam/setup-acl/handler.js +224 -0
  157. package/esm/commands/aws/iam/setup-acl/index.d.ts +3 -0
  158. package/esm/commands/aws/iam/setup-acl/index.js +4 -0
  159. package/esm/commands/aws/iam/setup-acl/schema.d.ts +24 -0
  160. package/esm/commands/aws/iam/setup-acl/schema.js +18 -0
  161. package/esm/commands/aws/iam/setup-acl/utils.d.ts +61 -0
  162. package/esm/commands/aws/iam/setup-acl/utils.js +298 -0
  163. package/esm/commands/aws/iam/users/handler.d.ts +5 -0
  164. package/esm/commands/aws/iam/users/handler.js +56 -0
  165. package/esm/commands/aws/iam/users/index.d.ts +3 -0
  166. package/esm/commands/aws/iam/users/index.js +4 -0
  167. package/esm/commands/aws/iam/users/schema.d.ts +18 -0
  168. package/esm/commands/aws/iam/users/schema.js +16 -0
  169. package/esm/commands/aws/iam/users/utils.d.ts +119 -0
  170. package/esm/commands/aws/iam/users/utils.js +440 -0
  171. package/esm/commands/aws/index.d.ts +14 -0
  172. package/esm/commands/aws/index.js +142 -0
  173. package/esm/commands/aws/route53/add-record/handler.d.ts +36 -0
  174. package/esm/commands/aws/route53/add-record/handler.js +258 -0
  175. package/esm/commands/aws/route53/add-record/index.d.ts +3 -0
  176. package/esm/commands/aws/route53/add-record/index.js +4 -0
  177. package/esm/commands/aws/route53/add-record/schema.d.ts +45 -0
  178. package/esm/commands/aws/route53/add-record/schema.js +40 -0
  179. package/esm/commands/aws/route53/add-record/utils.d.ts +216 -0
  180. package/esm/commands/aws/route53/add-record/utils.js +389 -0
  181. package/esm/commands/aws/route53/remove-record/handler.d.ts +6 -0
  182. package/esm/commands/aws/route53/remove-record/handler.js +111 -0
  183. package/esm/commands/aws/route53/remove-record/index.d.ts +2 -0
  184. package/esm/commands/aws/route53/remove-record/index.js +3 -0
  185. package/esm/commands/aws/route53/remove-record/schema.d.ts +30 -0
  186. package/esm/commands/aws/route53/remove-record/schema.js +21 -0
  187. package/esm/commands/aws/schema.d.ts +13 -0
  188. package/esm/commands/aws/schema.js +16 -0
  189. package/esm/commands/aws/ses/send-email/handler.d.ts +5 -0
  190. package/esm/commands/aws/ses/send-email/handler.js +70 -0
  191. package/esm/commands/aws/ses/send-email/index.d.ts +3 -0
  192. package/esm/commands/aws/ses/send-email/index.js +4 -0
  193. package/esm/commands/aws/ses/send-email/schema.d.ts +39 -0
  194. package/esm/commands/aws/ses/send-email/schema.js +23 -0
  195. package/esm/commands/aws/ses/send-email/utils.d.ts +47 -0
  196. package/esm/commands/aws/ses/send-email/utils.js +84 -0
  197. package/esm/commands/aws/ses/smtp-password.d.ts +21 -0
  198. package/esm/commands/aws/ses/smtp-password.js +69 -0
  199. package/esm/commands/aws/utils/exec-aws.d.ts +79 -0
  200. package/esm/commands/aws/utils/exec-aws.js +137 -0
  201. package/esm/commands/aws/utils/resolve-policy-path.d.ts +8 -0
  202. package/esm/commands/aws/utils/resolve-policy-path.js +14 -0
  203. package/esm/commands/aws/utils/resolve-profile.d.ts +28 -0
  204. package/esm/commands/aws/utils/resolve-profile.js +88 -0
  205. package/esm/commands/aws/utils/tty.d.ts +24 -0
  206. package/esm/commands/aws/utils/tty.js +51 -0
  207. package/esm/commands/db/create-readonly-user/handler.d.ts +5 -0
  208. package/esm/commands/db/create-readonly-user/handler.js +251 -0
  209. package/esm/commands/db/create-readonly-user/index.d.ts +3 -0
  210. package/esm/commands/db/create-readonly-user/index.js +4 -0
  211. package/esm/commands/db/create-readonly-user/mysql.d.ts +33 -0
  212. package/esm/commands/db/create-readonly-user/mysql.js +98 -0
  213. package/esm/commands/db/create-readonly-user/postgres.d.ts +31 -0
  214. package/esm/commands/db/create-readonly-user/postgres.js +205 -0
  215. package/esm/commands/db/create-readonly-user/schema.d.ts +55 -0
  216. package/esm/commands/db/create-readonly-user/schema.js +35 -0
  217. package/esm/commands/db/create-readonly-user/utils.d.ts +112 -0
  218. package/esm/commands/db/create-readonly-user/utils.js +151 -0
  219. package/esm/commands/db/index.d.ts +6 -0
  220. package/esm/commands/db/index.js +31 -0
  221. package/esm/commands/doppler/handler.js +92 -9
  222. package/esm/commands/doppler/index.d.ts +1 -0
  223. package/esm/commands/doppler/index.js +6 -41
  224. package/esm/commands/doppler/schema.d.ts +233 -17
  225. package/esm/commands/doppler/schema.js +125 -7
  226. package/esm/commands/doppler/set-secret.d.ts +113 -0
  227. package/esm/commands/doppler/set-secret.js +370 -0
  228. package/esm/commands/doppler/utils/exec.js +100 -12
  229. package/esm/commands/doppler/utils.d.ts +32 -8
  230. package/esm/commands/doppler/utils.js +396 -30
  231. package/esm/commands/icons/handler.js +79 -8
  232. package/esm/commands/icons/index.js +4 -31
  233. package/esm/commands/icons/schema.d.ts +1 -1
  234. package/esm/commands/icons/schema.js +97 -15
  235. package/esm/commands/icons/utils.js +125 -13
  236. package/esm/commands/index.js +49 -78
  237. package/esm/commands/schema.js +22 -7
  238. package/esm/index.js +3 -4
  239. package/package.json +12 -2
  240. package/vitest.config.ts +11 -0
  241. package/cjs/__chunks/4GFJUCSP.cjs +0 -52
  242. package/cjs/__chunks/4SFWHCB3.cjs +0 -133
  243. package/cjs/__chunks/B2KFW7T7.cjs +0 -1
  244. package/cjs/__chunks/EMMC33QA.cjs +0 -401
  245. package/cjs/__chunks/INFOPAHN.cjs +0 -117
  246. package/cjs/__chunks/J36P6UNE.cjs +0 -75
  247. package/cjs/__chunks/NIMXXHJQ.cjs +0 -92
  248. package/cjs/__chunks/NSRFG4DF.cjs +0 -100
  249. package/cjs/__chunks/WNJ3ZYLI.cjs +0 -1
  250. package/cjs/__chunks/ZMDIOZPT.cjs +0 -22
  251. package/esm/__chunks/46HGXFME.js +0 -0
  252. package/esm/__chunks/6G4PXXKF.js +0 -75
  253. package/esm/__chunks/6L5RNR7G.js +0 -100
  254. package/esm/__chunks/FL7AILGH.js +0 -133
  255. package/esm/__chunks/HJSXC6HP.js +0 -117
  256. package/esm/__chunks/O262GK4B.js +0 -401
  257. package/esm/__chunks/O3CCA76Q.js +0 -22
  258. package/esm/__chunks/SJXQJ7RS.js +0 -92
  259. package/esm/__chunks/SNCKIAOR.js +0 -0
  260. package/esm/__chunks/X3R5PSWH.js +0 -52
@@ -0,0 +1,51 @@
1
+ import { createInterface } from "readline";
2
+ //#region src/commands/aws/utils/tty.js
3
+ /**
4
+ * Prompt for a single line of input.
5
+ * @param {string} message - Prompt text shown to the user
6
+ * @returns {Promise<string>}
7
+ */
8
+ function prompt(message) {
9
+ return new Promise((resolve) => {
10
+ const rl = createInterface({
11
+ input: process.stdin,
12
+ output: process.stdout
13
+ });
14
+ rl.question(message, (answer) => {
15
+ rl.close();
16
+ resolve(answer.trim());
17
+ });
18
+ });
19
+ }
20
+ /**
21
+ * Prompt the user to pick one option from a numbered list.
22
+ * @param {{ message: string; options: string[] }} params
23
+ * @returns {Promise<number>} Zero-based index of the chosen option
24
+ */
25
+ async function promptChoice(params) {
26
+ const { message, options } = params;
27
+ if (options.length === 0) throw new Error("promptChoice: no options to choose from");
28
+ console.log(message);
29
+ options.forEach((option, index) => {
30
+ console.log(` ${index + 1}. ${option}`);
31
+ });
32
+ for (;;) {
33
+ const raw = await prompt("Enter number: ");
34
+ const choice = Number.parseInt(raw, 10);
35
+ if (Number.isInteger(choice) && choice >= 1 && choice <= options.length) return choice - 1;
36
+ console.log(`Please enter a number between 1 and ${options.length}.`);
37
+ }
38
+ }
39
+ /**
40
+ * Prompt a yes/no question.
41
+ * @param {{ message: string; defaultYes?: boolean }} params
42
+ * @returns {Promise<boolean>}
43
+ */
44
+ async function promptYesNo(params) {
45
+ const { message, defaultYes = false } = params;
46
+ const answer = (await prompt(`${message}${defaultYes ? " [Y/n]: " : " [y/N]: "}`)).toLowerCase();
47
+ if (answer === "") return defaultYes;
48
+ return answer === "y" || answer === "yes";
49
+ }
50
+ //#endregion
51
+ export { prompt, promptChoice, promptYesNo };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * `db create-readonly-user` handler.
3
+ * @param {unknown} options
4
+ */
5
+ export function createReadonlyUserCommand(options: unknown): Promise<void>;
@@ -0,0 +1,251 @@
1
+ import { prompt, promptChoice, promptYesNo } from "../../aws/utils/tty.js";
2
+ import { buildReadonlyUrl, detectEngineFromUrl, generatePassword, parseConnectionUrl, resolveAdminUrl } from "./utils.js";
3
+ import { applyMysqlReadonly, listMysqlDatabases } from "./mysql.js";
4
+ import { applyPostgresReadonly, listPostgresDatabases } from "./postgres.js";
5
+ import { ENGINES, createReadonlyUserOptionsSchema } from "./schema.js";
6
+ import chalk from "chalk";
7
+ import { ZodError } from "zod";
8
+ //#region src/commands/db/create-readonly-user/handler.js
9
+ /**
10
+ * @param {"postgres" | "mysql" | undefined} engineOption
11
+ * @param {string | null} url
12
+ * @returns {Promise<"postgres" | "mysql">}
13
+ */
14
+ async function resolveEngine(engineOption, url) {
15
+ if (engineOption) return engineOption;
16
+ if (url) {
17
+ const detected = detectEngineFromUrl(url);
18
+ if (detected) return detected;
19
+ }
20
+ const index = await promptChoice({
21
+ message: "Choose a database engine:",
22
+ options: ["postgres", "mysql"]
23
+ });
24
+ const chosen = ENGINES[index];
25
+ if (!chosen) throw new Error("No engine selected");
26
+ return chosen;
27
+ }
28
+ /**
29
+ * @param {{ urlOption?: string; engine: "postgres" | "mysql" }} params
30
+ * @returns {Promise<string>}
31
+ */
32
+ async function resolveUrl(params) {
33
+ const { urlOption, engine } = params;
34
+ const fromEnv = resolveAdminUrl({
35
+ url: urlOption,
36
+ engine
37
+ });
38
+ if (fromEnv) return fromEnv;
39
+ const raw = await prompt(` Admin connection URL (e.g. ${engine === "postgres" ? "postgresql://admin:pass@host:5432/postgres" : "mysql://admin:pass@host:3306/"}): `);
40
+ if (!raw) throw new Error("Admin connection URL is required (pass --url or set DATABASE_URL)");
41
+ return raw;
42
+ }
43
+ /**
44
+ * @param {{
45
+ * databaseOption?: string;
46
+ * databases: string[];
47
+ * }} params
48
+ * @returns {Promise<{ mode: "all" | "one"; databases: string[] }>}
49
+ */
50
+ async function resolveScope(params) {
51
+ const { databaseOption, databases } = params;
52
+ if (databaseOption) {
53
+ const normalized = databaseOption.trim().toLowerCase();
54
+ if (normalized === "all" || normalized === "*") return {
55
+ mode: "all",
56
+ databases
57
+ };
58
+ if (!databases.includes(databaseOption)) throw new Error(`Database "${databaseOption}" not found. Available: ${databases.join(", ") || "(none)"}`);
59
+ return {
60
+ mode: "one",
61
+ databases: [databaseOption]
62
+ };
63
+ }
64
+ if (databases.length === 0) throw new Error("No user databases found to grant access on.");
65
+ const options = [...databases.map((name) => name), `all (${databases.length} databases)`];
66
+ const index = await promptChoice({
67
+ message: "Grant read-only on which database?",
68
+ options
69
+ });
70
+ if (index === options.length - 1) return {
71
+ mode: "all",
72
+ databases
73
+ };
74
+ const chosen = databases[index];
75
+ if (!chosen) throw new Error("No database selected");
76
+ return {
77
+ mode: "one",
78
+ databases: [chosen]
79
+ };
80
+ }
81
+ /**
82
+ * @param {string | undefined} usernameOption
83
+ * @returns {Promise<string>}
84
+ */
85
+ async function resolveUsername(usernameOption) {
86
+ if (usernameOption) return usernameOption;
87
+ const username = await prompt(` Read-only username [default: readonly]: `) || "readonly";
88
+ if (!/^[A-Za-z0-9_]+$/.test(username)) throw new Error("Username must contain only letters, numbers, and underscores");
89
+ return username;
90
+ }
91
+ /**
92
+ * @param {string | undefined} passwordOption
93
+ * @returns {Promise<{ password: string; generated: boolean }>}
94
+ */
95
+ async function resolvePassword(passwordOption) {
96
+ if (passwordOption) return {
97
+ password: passwordOption,
98
+ generated: false
99
+ };
100
+ if (await promptYesNo({
101
+ message: " Generate a random password?",
102
+ defaultYes: true
103
+ })) return {
104
+ password: generatePassword(),
105
+ generated: true
106
+ };
107
+ const password = await prompt(" Password: ");
108
+ if (!password) throw new Error("Password is required");
109
+ return {
110
+ password,
111
+ generated: false
112
+ };
113
+ }
114
+ /**
115
+ * Redact password in a URL for display.
116
+ * @param {string} url
117
+ * @returns {string}
118
+ */
119
+ function redactUrl(url) {
120
+ try {
121
+ const parsed = new URL(url);
122
+ if (parsed.password) parsed.password = "***";
123
+ return parsed.toString();
124
+ } catch {
125
+ return url;
126
+ }
127
+ }
128
+ /**
129
+ * `db create-readonly-user` handler.
130
+ * @param {unknown} options
131
+ */
132
+ async function createReadonlyUserCommand(options) {
133
+ try {
134
+ const { engine: engineOption, url: urlOption, database: databaseOption, username: usernameOption, password: passwordOption, hostPattern: hostPatternOption, ssl: sslOption, yes, dryRun, verbose } = createReadonlyUserOptionsSchema.parse(options);
135
+ console.log(chalk.blue("\nCreate read-only database user"));
136
+ console.log("");
137
+ const engine = await resolveEngine(engineOption, resolveAdminUrl({
138
+ url: urlOption,
139
+ engine: engineOption
140
+ }));
141
+ console.log(`Engine: ${engine}`);
142
+ const url = await resolveUrl({
143
+ urlOption,
144
+ engine
145
+ });
146
+ const config = parseConnectionUrl({
147
+ url,
148
+ engine,
149
+ sslOption
150
+ });
151
+ console.log(`Admin: ${config.user}@${config.host}:${config.port}` + (config.database ? `/${config.database}` : ""));
152
+ console.log("");
153
+ const listed = engine === "postgres" ? await listPostgresDatabases(config) : await listMysqlDatabases(config);
154
+ if (verbose) {
155
+ console.log(`Found databases: ${listed.join(", ") || "(none)"}`);
156
+ console.log("");
157
+ }
158
+ const scope = await resolveScope({
159
+ databaseOption,
160
+ databases: listed
161
+ });
162
+ console.log(scope.mode === "all" ? `Scope: all (${scope.databases.length} databases)` : `Scope: ${scope.databases[0]}`);
163
+ console.log("");
164
+ const username = await resolveUsername(usernameOption);
165
+ const { password, generated } = await resolvePassword(passwordOption);
166
+ const hostPattern = hostPatternOption ?? "%";
167
+ console.log("");
168
+ console.log("Plan:");
169
+ console.log(` User: ${username}`);
170
+ console.log(` Password: ${generated ? "(generated)" : "(provided)"}${dryRun ? " [dry-run]" : ""}`);
171
+ if (engine === "mysql") console.log(` Host: ${hostPattern}`);
172
+ console.log(scope.mode === "all" ? ` Grants: SELECT on ALL databases (${scope.databases.length})` : ` Grants: SELECT on ${scope.databases[0]}`);
173
+ if (scope.mode === "all" && engine === "postgres") console.log(chalk.yellow(" Note: Databases created later will NOT get these grants automatically — re-run this command."));
174
+ console.log("");
175
+ if (!yes && !dryRun) {
176
+ if (!await promptYesNo({
177
+ message: "Apply these changes?",
178
+ defaultYes: false
179
+ })) {
180
+ console.log("Aborted.");
181
+ return;
182
+ }
183
+ console.log("");
184
+ }
185
+ const result = engine === "postgres" ? await applyPostgresReadonly({
186
+ config,
187
+ username,
188
+ password,
189
+ databases: scope.databases,
190
+ dryRun,
191
+ verbose
192
+ }) : await applyMysqlReadonly({
193
+ config,
194
+ username,
195
+ password,
196
+ databases: scope.mode === "all" ? "all" : scope.databases,
197
+ hostPattern,
198
+ dryRun,
199
+ verbose
200
+ });
201
+ if (dryRun) {
202
+ console.log(chalk.cyan("Dry run — SQL that would run:"));
203
+ for (const statement of result.statements) console.log(` ${statement}`);
204
+ console.log("");
205
+ console.log(chalk.cyan("No changes made."));
206
+ return;
207
+ }
208
+ console.log(chalk.green(result.created ? `Created user "${username}" and granted read-only access.` : `Updated user "${username}" password and refreshed read-only grants.`));
209
+ console.log("");
210
+ const sampleDb = scope.mode === "one" ? scope.databases[0] ?? "" : scope.databases[0] ?? "";
211
+ const readonlyUrl = buildReadonlyUrl({
212
+ engine,
213
+ host: config.host,
214
+ port: config.port,
215
+ username,
216
+ password,
217
+ database: sampleDb
218
+ });
219
+ console.log("Connection (secret shown once — copy now):");
220
+ console.log("────────────────────────────────────────────────────────────────");
221
+ console.log(readonlyUrl);
222
+ if (scope.mode === "all") {
223
+ console.log("");
224
+ console.log(chalk.gray(` (URL uses database "${sampleDb}" as an example; user can connect to any granted database.)`));
225
+ }
226
+ console.log("────────────────────────────────────────────────────────────────");
227
+ console.log("");
228
+ console.log(chalk.gray(`Admin URL used: ${redactUrl(url)}`));
229
+ } catch (error) {
230
+ if (error instanceof ZodError) {
231
+ console.error(chalk.red("\n❌ Invalid command options:"));
232
+ error.errors.forEach((err) => {
233
+ console.error(chalk.red(` ${err.path.join(".")}: ${err.message}`));
234
+ });
235
+ process.exit(1);
236
+ }
237
+ const verbose = options?.verbose || false;
238
+ console.error(chalk.red("\n❌ create-readonly-user failed:"));
239
+ if (error instanceof Error) {
240
+ console.error(chalk.red(` ${error.message}`));
241
+ if (verbose && error.stack) {
242
+ console.error(chalk.gray("\nStack trace:"));
243
+ console.error(chalk.gray(error.stack));
244
+ }
245
+ } else console.error(chalk.red(` ${String(error)}`));
246
+ console.error(chalk.yellow("\n💡 Pass --url or set DATABASE_URL. Use --verbose for details.\n"));
247
+ process.exit(1);
248
+ }
249
+ }
250
+ //#endregion
251
+ export { createReadonlyUserCommand };
@@ -0,0 +1,3 @@
1
+ export * from "./handler.js";
2
+ export * from "./schema.js";
3
+ export * from "./utils.js";
@@ -0,0 +1,4 @@
1
+ import { buildReadonlyUrl, detectEngineFromUrl, generatePassword, isSystemDatabase, parseConnectionUrl, quoteIdent, quoteLiteral, resolveAdminUrl, resolveSsl } from "./utils.js";
2
+ import { DEFAULT_MYSQL_HOST_PATTERN, DEFAULT_READONLY_USERNAME, ENGINES, createReadonlyUserOptionsSchema } from "./schema.js";
3
+ import { createReadonlyUserCommand } from "./handler.js";
4
+ export { DEFAULT_MYSQL_HOST_PATTERN, DEFAULT_READONLY_USERNAME, ENGINES, buildReadonlyUrl, createReadonlyUserCommand, createReadonlyUserOptionsSchema, detectEngineFromUrl, generatePassword, isSystemDatabase, parseConnectionUrl, quoteIdent, quoteLiteral, resolveAdminUrl, resolveSsl };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * List schemas/databases visible to the admin user (excluding system schemas).
3
+ * @param {ConnectionConfig} config
4
+ * @returns {Promise<string[]>}
5
+ */
6
+ export function listMysqlDatabases(config: ConnectionConfig): Promise<string[]>;
7
+ /**
8
+ * Create/update a MySQL user and grant SELECT on one database or *.* .
9
+ * @param {{
10
+ * config: ConnectionConfig;
11
+ * username: string;
12
+ * password: string;
13
+ * databases: string[] | "all";
14
+ * hostPattern: string;
15
+ * dryRun?: boolean;
16
+ * verbose?: boolean;
17
+ * }} params
18
+ * @returns {Promise<{ created: boolean; statements: string[]; databases: string[] | "all" }>}
19
+ */
20
+ export function applyMysqlReadonly(params: {
21
+ config: ConnectionConfig;
22
+ username: string;
23
+ password: string;
24
+ databases: string[] | "all";
25
+ hostPattern: string;
26
+ dryRun?: boolean;
27
+ verbose?: boolean;
28
+ }): Promise<{
29
+ created: boolean;
30
+ statements: string[];
31
+ databases: string[] | "all";
32
+ }>;
33
+ export type ConnectionConfig = import("./utils.js").ConnectionConfig;
@@ -0,0 +1,98 @@
1
+ import { isSystemDatabase, quoteIdent, quoteLiteral } from "./utils.js";
2
+ import mysql from "mysql2/promise";
3
+ //#region src/commands/db/create-readonly-user/mysql.js
4
+ /**
5
+ * @typedef {import("./utils.js").ConnectionConfig} ConnectionConfig
6
+ */
7
+ /**
8
+ * @param {ConnectionConfig} config
9
+ * @returns {Promise<import("mysql2/promise").Connection>}
10
+ */
11
+ async function createConnection(config) {
12
+ return mysql.createConnection({
13
+ host: config.host,
14
+ port: config.port,
15
+ user: config.user,
16
+ password: config.password,
17
+ database: config.database || void 0,
18
+ ssl: config.ssl && typeof config.ssl === "object" ? config.ssl : void 0,
19
+ multipleStatements: false
20
+ });
21
+ }
22
+ /**
23
+ * List schemas/databases visible to the admin user (excluding system schemas).
24
+ * @param {ConnectionConfig} config
25
+ * @returns {Promise<string[]>}
26
+ */
27
+ async function listMysqlDatabases(config) {
28
+ const connection = await createConnection(config);
29
+ try {
30
+ const [rows] = await connection.query("SHOW DATABASES");
31
+ return rows.map((row) => row.Database ?? "").filter((name) => Boolean(name)).filter((name) => !isSystemDatabase({
32
+ engine: "mysql",
33
+ name
34
+ })).sort((a, b) => a.localeCompare(b));
35
+ } finally {
36
+ await connection.end();
37
+ }
38
+ }
39
+ /**
40
+ * Create/update a MySQL user and grant SELECT on one database or *.* .
41
+ * @param {{
42
+ * config: ConnectionConfig;
43
+ * username: string;
44
+ * password: string;
45
+ * databases: string[] | "all";
46
+ * hostPattern: string;
47
+ * dryRun?: boolean;
48
+ * verbose?: boolean;
49
+ * }} params
50
+ * @returns {Promise<{ created: boolean; statements: string[]; databases: string[] | "all" }>}
51
+ */
52
+ async function applyMysqlReadonly(params) {
53
+ const { config, username, password, databases, hostPattern, dryRun = false, verbose = false } = params;
54
+ const userLiteral = quoteLiteral(username);
55
+ const hostLiteral = quoteLiteral(hostPattern);
56
+ const passwordLiteral = quoteLiteral(password);
57
+ const account = `${userLiteral}@${hostLiteral}`;
58
+ /** @type {string[]} */
59
+ const statements = [];
60
+ const createSql = `CREATE USER IF NOT EXISTS ${account} IDENTIFIED BY ${passwordLiteral}`;
61
+ const alterSql = `ALTER USER ${account} IDENTIFIED BY ${passwordLiteral}`;
62
+ /** @type {string[]} */
63
+ const grantSqls = [];
64
+ if (databases === "all") grantSqls.push(`GRANT SELECT, SHOW VIEW, SHOW DATABASES ON *.* TO ${account}`);
65
+ else for (const database of databases) {
66
+ const dbIdent = quoteIdent({
67
+ engine: "mysql",
68
+ name: database
69
+ });
70
+ grantSqls.push(`GRANT SELECT, SHOW VIEW ON ${dbIdent}.* TO ${account}`);
71
+ }
72
+ const flushSql = "FLUSH PRIVILEGES";
73
+ statements.push(createSql, alterSql, ...grantSqls, flushSql);
74
+ if (dryRun) return {
75
+ created: true,
76
+ statements: statements.map((sql) => sql.includes("IDENTIFIED BY") ? sql.replace(/IDENTIFIED BY '.*'/, "IDENTIFIED BY ***") : sql),
77
+ databases
78
+ };
79
+ const connection = await createConnection(config);
80
+ try {
81
+ if (verbose) console.log(` Connected to MySQL as ${config.user}@${config.host}`);
82
+ const [existingRows] = await connection.query("SELECT 1 AS ok FROM mysql.user WHERE User = ? AND Host = ? LIMIT 1", [username, hostPattern]);
83
+ const existed = Array.isArray(existingRows) && existingRows.length > 0;
84
+ await connection.query(createSql);
85
+ await connection.query(alterSql);
86
+ for (const grantSql of grantSqls) await connection.query(grantSql);
87
+ await connection.query(flushSql);
88
+ return {
89
+ created: !existed,
90
+ statements: statements.map((sql) => sql.includes("IDENTIFIED BY") ? sql.replace(/IDENTIFIED BY '.*'/, "IDENTIFIED BY ***") : sql),
91
+ databases
92
+ };
93
+ } finally {
94
+ await connection.end();
95
+ }
96
+ }
97
+ //#endregion
98
+ export { applyMysqlReadonly, listMysqlDatabases };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * List non-template databases visible to the admin user.
3
+ * @param {ConnectionConfig} config
4
+ * @returns {Promise<string[]>}
5
+ */
6
+ export function listPostgresDatabases(config: ConnectionConfig): Promise<string[]>;
7
+ /**
8
+ * Create/update a Postgres role and grant read-only on one or all databases.
9
+ * @param {{
10
+ * config: ConnectionConfig;
11
+ * username: string;
12
+ * password: string;
13
+ * databases: string[];
14
+ * dryRun?: boolean;
15
+ * verbose?: boolean;
16
+ * }} params
17
+ * @returns {Promise<{ created: boolean; statements: string[]; databases: string[] }>}
18
+ */
19
+ export function applyPostgresReadonly(params: {
20
+ config: ConnectionConfig;
21
+ username: string;
22
+ password: string;
23
+ databases: string[];
24
+ dryRun?: boolean;
25
+ verbose?: boolean;
26
+ }): Promise<{
27
+ created: boolean;
28
+ statements: string[];
29
+ databases: string[];
30
+ }>;
31
+ export type ConnectionConfig = import("./utils.js").ConnectionConfig;
@@ -0,0 +1,205 @@
1
+ import { isSystemDatabase, quoteIdent, quoteLiteral } from "./utils.js";
2
+ import pg from "pg";
3
+ //#region src/commands/db/create-readonly-user/postgres.js
4
+ const { Client } = pg;
5
+ /**
6
+ * @typedef {import("./utils.js").ConnectionConfig} ConnectionConfig
7
+ */
8
+ /**
9
+ * @param {ConnectionConfig} config
10
+ * @param {string} [database]
11
+ * @returns {import("pg").Client}
12
+ */
13
+ function createClient(config, database) {
14
+ return new Client({
15
+ host: config.host,
16
+ port: config.port,
17
+ user: config.user,
18
+ password: config.password,
19
+ database: database ?? (config.database || "postgres"),
20
+ ssl: config.ssl
21
+ });
22
+ }
23
+ /**
24
+ * @param {import("pg").Client} client
25
+ * @param {string} sql
26
+ * @param {unknown[]} [params]
27
+ */
28
+ async function query(client, sql, params) {
29
+ return client.query(sql, params);
30
+ }
31
+ /**
32
+ * List non-template databases visible to the admin user.
33
+ * @param {ConnectionConfig} config
34
+ * @returns {Promise<string[]>}
35
+ */
36
+ async function listPostgresDatabases(config) {
37
+ const client = createClient(config, config.database || "postgres");
38
+ await client.connect();
39
+ try {
40
+ return (await query(client, `SELECT datname
41
+ FROM pg_database
42
+ WHERE datistemplate = false
43
+ ORDER BY datname`)).rows.map((row) => row.datname).filter((name) => !isSystemDatabase({
44
+ engine: "postgres",
45
+ name
46
+ }));
47
+ } finally {
48
+ await client.end();
49
+ }
50
+ }
51
+ /**
52
+ * Ensure a login role exists (create or update password).
53
+ * @param {import("pg").Client} client
54
+ * @param {{ username: string; password: string }} params
55
+ * @returns {Promise<{ created: boolean; statements: string[] }>}
56
+ */
57
+ async function ensureRole(client, params) {
58
+ const { username, password } = params;
59
+ const ident = quoteIdent({
60
+ engine: "postgres",
61
+ name: username
62
+ });
63
+ const passwordLiteral = quoteLiteral(password);
64
+ const existing = await query(client, "SELECT 1 FROM pg_roles WHERE rolname = $1", [username]);
65
+ /** @type {string[]} */
66
+ const statements = [];
67
+ if (existing.rowCount && existing.rowCount > 0) {
68
+ const sql = `ALTER ROLE ${ident} WITH LOGIN PASSWORD ${passwordLiteral}`;
69
+ statements.push(sql);
70
+ await query(client, sql);
71
+ return {
72
+ created: false,
73
+ statements
74
+ };
75
+ }
76
+ const sql = `CREATE ROLE ${ident} WITH LOGIN PASSWORD ${passwordLiteral}`;
77
+ statements.push(sql);
78
+ await query(client, sql);
79
+ return {
80
+ created: true,
81
+ statements
82
+ };
83
+ }
84
+ /**
85
+ * Grant read-only privileges inside one database (schemas, tables, sequences, defaults).
86
+ * @param {import("pg").Client} client
87
+ * @param {{ username: string }} params
88
+ * @returns {Promise<string[]>}
89
+ */
90
+ async function grantReadonlyInDatabase(client, params) {
91
+ const { username } = params;
92
+ const ident = quoteIdent({
93
+ engine: "postgres",
94
+ name: username
95
+ });
96
+ /** @type {string[]} */
97
+ const statements = [];
98
+ const schemas = (await query(client, `SELECT nspname
99
+ FROM pg_namespace
100
+ WHERE nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
101
+ AND nspname NOT LIKE 'pg_temp_%'
102
+ AND nspname NOT LIKE 'pg_toast_temp_%'
103
+ ORDER BY nspname`)).rows.map((row) => row.nspname);
104
+ if (schemas.length === 0) schemas.push("public");
105
+ for (const schema of schemas) {
106
+ const schemaIdent = quoteIdent({
107
+ engine: "postgres",
108
+ name: schema
109
+ });
110
+ const grants = [
111
+ `GRANT USAGE ON SCHEMA ${schemaIdent} TO ${ident}`,
112
+ `GRANT SELECT ON ALL TABLES IN SCHEMA ${schemaIdent} TO ${ident}`,
113
+ `GRANT SELECT ON ALL SEQUENCES IN SCHEMA ${schemaIdent} TO ${ident}`,
114
+ `ALTER DEFAULT PRIVILEGES IN SCHEMA ${schemaIdent} GRANT SELECT ON TABLES TO ${ident}`,
115
+ `ALTER DEFAULT PRIVILEGES IN SCHEMA ${schemaIdent} GRANT SELECT ON SEQUENCES TO ${ident}`
116
+ ];
117
+ for (const sql of grants) {
118
+ statements.push(sql);
119
+ await query(client, sql);
120
+ }
121
+ }
122
+ return statements;
123
+ }
124
+ /**
125
+ * Create/update a Postgres role and grant read-only on one or all databases.
126
+ * @param {{
127
+ * config: ConnectionConfig;
128
+ * username: string;
129
+ * password: string;
130
+ * databases: string[];
131
+ * dryRun?: boolean;
132
+ * verbose?: boolean;
133
+ * }} params
134
+ * @returns {Promise<{ created: boolean; statements: string[]; databases: string[] }>}
135
+ */
136
+ async function applyPostgresReadonly(params) {
137
+ const { config, username, password, databases, dryRun = false, verbose = false } = params;
138
+ /** @type {string[]} */
139
+ const statements = [];
140
+ let created = false;
141
+ if (dryRun) {
142
+ const ident = quoteIdent({
143
+ engine: "postgres",
144
+ name: username
145
+ });
146
+ statements.push(`-- Would CREATE/ALTER ROLE ${ident} WITH LOGIN PASSWORD ***`);
147
+ for (const database of databases) {
148
+ const dbIdent = quoteIdent({
149
+ engine: "postgres",
150
+ name: database
151
+ });
152
+ statements.push(`GRANT CONNECT ON DATABASE ${dbIdent} TO ${ident}`);
153
+ statements.push(`-- In database ${database}: GRANT USAGE/SELECT on all user schemas + ALTER DEFAULT PRIVILEGES`);
154
+ }
155
+ return {
156
+ created: true,
157
+ statements,
158
+ databases
159
+ };
160
+ }
161
+ const adminDb = config.database || "postgres";
162
+ const adminClient = createClient(config, adminDb);
163
+ await adminClient.connect();
164
+ try {
165
+ if (verbose) console.log(` Connected to Postgres as ${config.user}@${config.host}/${adminDb}`);
166
+ const roleResult = await ensureRole(adminClient, {
167
+ username,
168
+ password
169
+ });
170
+ created = roleResult.created;
171
+ statements.push(...roleResult.statements);
172
+ const ident = quoteIdent({
173
+ engine: "postgres",
174
+ name: username
175
+ });
176
+ for (const database of databases) {
177
+ const connectSql = `GRANT CONNECT ON DATABASE ${quoteIdent({
178
+ engine: "postgres",
179
+ name: database
180
+ })} TO ${ident}`;
181
+ statements.push(connectSql);
182
+ await query(adminClient, connectSql);
183
+ }
184
+ } finally {
185
+ await adminClient.end();
186
+ }
187
+ for (const database of databases) {
188
+ const dbClient = createClient(config, database);
189
+ await dbClient.connect();
190
+ try {
191
+ if (verbose) console.log(` Granting read-only in database ${database}`);
192
+ const dbStatements = await grantReadonlyInDatabase(dbClient, { username });
193
+ statements.push(...dbStatements);
194
+ } finally {
195
+ await dbClient.end();
196
+ }
197
+ }
198
+ return {
199
+ created,
200
+ statements,
201
+ databases
202
+ };
203
+ }
204
+ //#endregion
205
+ export { applyPostgresReadonly, listPostgresDatabases };