@rebasepro/cli 0.23.0 → 0.24.1-canary.g19596ef

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 (186) hide show
  1. package/README.md +19 -3
  2. package/bin/rebase.js +4 -1
  3. package/dist/agent-setup-B31F00DC.js +324 -0
  4. package/dist/agent-setup-B31F00DC.js.map +1 -0
  5. package/dist/api-keys-Bl1I9q7M.js +525 -0
  6. package/dist/api-keys-Bl1I9q7M.js.map +1 -0
  7. package/dist/apps-CNgQJjg5.js +340 -0
  8. package/dist/apps-CNgQJjg5.js.map +1 -0
  9. package/dist/args-BnmsEXvu.js +109 -0
  10. package/dist/args-BnmsEXvu.js.map +1 -0
  11. package/dist/atlas-dev-database-D_LP_Woq.js +62 -0
  12. package/dist/atlas-dev-database-D_LP_Woq.js.map +1 -0
  13. package/dist/auth-aySHEF45.js +414 -0
  14. package/dist/auth-aySHEF45.js.map +1 -0
  15. package/dist/branch-pointer-DvSHBjjz.js +2 -0
  16. package/dist/build-DIHWIqO4.js +336 -0
  17. package/dist/build-DIHWIqO4.js.map +1 -0
  18. package/dist/bundle-CqURVN-K.js +1621 -0
  19. package/dist/bundle-CqURVN-K.js.map +1 -0
  20. package/dist/bundle.d.ts +12 -3
  21. package/dist/cli.d.ts +23 -2
  22. package/dist/cloud-DjByQlkW.js +9667 -0
  23. package/dist/cloud-DjByQlkW.js.map +1 -0
  24. package/dist/collection-drift-BKj_BoMq.js +106 -0
  25. package/dist/collection-drift-BKj_BoMq.js.map +1 -0
  26. package/dist/commands/__test_support__/parser-specs.d.ts +13 -0
  27. package/dist/commands/agent-setup.d.ts +116 -0
  28. package/dist/commands/api-keys.d.ts +86 -23
  29. package/dist/commands/apps.d.ts +17 -0
  30. package/dist/commands/auth.d.ts +7 -0
  31. package/dist/commands/build.d.ts +5 -1
  32. package/dist/commands/cloud/auth.d.ts +4 -3
  33. package/dist/commands/cloud/bundle-deploy.d.ts +45 -0
  34. package/dist/commands/cloud/context.d.ts +44 -5
  35. package/dist/commands/cloud/cron.d.ts +53 -0
  36. package/dist/commands/cloud/debug.d.ts +134 -0
  37. package/dist/commands/cloud/index.d.ts +89 -12
  38. package/dist/commands/cloud/token-capabilities.d.ts +76 -0
  39. package/dist/commands/cloud/tokens.d.ts +23 -0
  40. package/dist/commands/console.d.ts +12 -0
  41. package/dist/commands/db.d.ts +79 -25
  42. package/dist/commands/dev.d.ts +181 -13
  43. package/dist/commands/doctor.d.ts +5 -0
  44. package/dist/commands/generate_sdk.d.ts +29 -2
  45. package/dist/commands/init.d.ts +115 -14
  46. package/dist/commands/schema.d.ts +6 -0
  47. package/dist/commands/skills.d.ts +126 -7
  48. package/dist/commands/start.d.ts +44 -0
  49. package/dist/commands/storage.d.ts +46 -0
  50. package/dist/commands/upgrade.d.ts +1 -0
  51. package/dist/consent-ClRHmcxf.js +76 -0
  52. package/dist/consent-ClRHmcxf.js.map +1 -0
  53. package/dist/console-D6SJf8k_.js +102 -0
  54. package/dist/console-D6SJf8k_.js.map +1 -0
  55. package/dist/context-BqhRydBg.js +1234 -0
  56. package/dist/context-BqhRydBg.js.map +1 -0
  57. package/dist/{daemon-entry-LTFKtpHy.js → daemon-entry-Da-D3rmH.js} +3 -4
  58. package/dist/daemon-entry-Da-D3rmH.js.map +1 -0
  59. package/dist/daemon-pzsQwDHa.js +2 -0
  60. package/dist/db-B7RuNuaQ.js +1312 -0
  61. package/dist/db-B7RuNuaQ.js.map +1 -0
  62. package/dist/derive-Dl0z2OWi.js +484 -0
  63. package/dist/derive-Dl0z2OWi.js.map +1 -0
  64. package/dist/dev-CYr_WXan.js +1599 -0
  65. package/dist/dev-CYr_WXan.js.map +1 -0
  66. package/dist/dev-db/atlas-dev-database.d.ts +32 -0
  67. package/dist/dev-db/daemon-entry.d.ts +13 -0
  68. package/dist/dev-db/daemon.d.ts +10 -0
  69. package/dist/dev-db/last-dev-database.d.ts +41 -0
  70. package/dist/dev-db/prepare.d.ts +29 -3
  71. package/dist/dev-db/pull.d.ts +90 -5
  72. package/dist/dev-db/resolve.d.ts +26 -2
  73. package/dist/dev-preflight-B-LYyBf1.js +2 -0
  74. package/dist/dev-server-DG8Tt-uf.js +96 -0
  75. package/dist/dev-server-DG8Tt-uf.js.map +1 -0
  76. package/dist/doctor-BP8cZ-7F.js +771 -0
  77. package/dist/doctor-BP8cZ-7F.js.map +1 -0
  78. package/dist/eject-DizdoecV.js +335 -0
  79. package/dist/eject-DizdoecV.js.map +1 -0
  80. package/dist/fold-static-BC9CYicw.js +201 -0
  81. package/dist/fold-static-BC9CYicw.js.map +1 -0
  82. package/dist/fold-static.d.ts +26 -3
  83. package/dist/function-portability-BIvlvR9q.js +265 -0
  84. package/dist/function-portability-BIvlvR9q.js.map +1 -0
  85. package/dist/generate_sdk-BPqdJc3Q.js +489 -0
  86. package/dist/generate_sdk-BPqdJc3Q.js.map +1 -0
  87. package/dist/index.es.js +199 -22095
  88. package/dist/index.es.js.map +1 -1
  89. package/dist/init-Utstvh2Q.js +1149 -0
  90. package/dist/init-Utstvh2Q.js.map +1 -0
  91. package/dist/last-dev-database-B58g89uA.js +2 -0
  92. package/dist/manifest-DRp0UVTj.js +880 -0
  93. package/dist/manifest-DRp0UVTj.js.map +1 -0
  94. package/dist/manifest.d.ts +56 -4
  95. package/dist/normalize-imports-X0Tgze9y.js +107 -0
  96. package/dist/normalize-imports-X0Tgze9y.js.map +1 -0
  97. package/dist/package-manager-DSEOwTqM.js +193 -0
  98. package/dist/package-manager-DSEOwTqM.js.map +1 -0
  99. package/dist/path-flags-BFgG9V6y.js +91 -0
  100. package/dist/path-flags-BFgG9V6y.js.map +1 -0
  101. package/dist/precompress.d.ts +17 -0
  102. package/dist/prepare-CDN6lSpz.js +1236 -0
  103. package/dist/prepare-CDN6lSpz.js.map +1 -0
  104. package/dist/{project-3WCbrrfW.js → project-Wp2zoyeN.js} +34 -184
  105. package/dist/project-Wp2zoyeN.js.map +1 -0
  106. package/dist/pull-CrGYMEsy.js +458 -0
  107. package/dist/pull-CrGYMEsy.js.map +1 -0
  108. package/dist/resolve-B2O-wKKk.js +2 -0
  109. package/dist/resources-FD7AfZx8.js +183 -0
  110. package/dist/resources-FD7AfZx8.js.map +1 -0
  111. package/dist/schema-DMdr431S.js +125 -0
  112. package/dist/schema-DMdr431S.js.map +1 -0
  113. package/dist/skills-DTFibT1m.js +642 -0
  114. package/dist/skills-DTFibT1m.js.map +1 -0
  115. package/dist/start-DEa-hOBh.js +267 -0
  116. package/dist/start-DEa-hOBh.js.map +1 -0
  117. package/dist/{state-C59Elrnt.js → state-Do7ZJYkF.js} +49 -4
  118. package/dist/state-Do7ZJYkF.js.map +1 -0
  119. package/dist/status-DxnJTekv.js +352 -0
  120. package/dist/status-DxnJTekv.js.map +1 -0
  121. package/dist/storage-DyVq-f_F.js +303 -0
  122. package/dist/storage-DyVq-f_F.js.map +1 -0
  123. package/dist/telemetry/identity.d.ts +10 -1
  124. package/dist/telemetry-C8Uq8phb.js +129 -0
  125. package/dist/telemetry-C8Uq8phb.js.map +1 -0
  126. package/dist/telemetry-EMFRVRQi.js +351 -0
  127. package/dist/telemetry-EMFRVRQi.js.map +1 -0
  128. package/dist/unknown-command-DNwDcX3w.js +98 -0
  129. package/dist/unknown-command-DNwDcX3w.js.map +1 -0
  130. package/dist/upgrade-CtZtRkTE.js +1332 -0
  131. package/dist/upgrade-CtZtRkTE.js.map +1 -0
  132. package/dist/upgrade.d.ts +38 -5
  133. package/dist/utils/dev-preflight.d.ts +47 -5
  134. package/dist/utils/dev-server.d.ts +30 -0
  135. package/dist/utils/path-flags.d.ts +31 -0
  136. package/dist/utils/project.d.ts +16 -0
  137. package/dist/utils/temp-archive.d.ts +9 -0
  138. package/dist/version-Buyyo1ut.js +58 -0
  139. package/dist/version-Buyyo1ut.js.map +1 -0
  140. package/package.json +7 -7
  141. package/templates/eject/Dockerfile +4 -2
  142. package/templates/eject/backend/src/env.ts +1 -1
  143. package/templates/eject/backend/src/index.ts +42 -4
  144. package/templates/eject/docker-compose.custom.yml +8 -4
  145. package/templates/generated-schemas/blank.ts +40 -0
  146. package/templates/generated-schemas/blog.ts +127 -0
  147. package/templates/generated-schemas/ecommerce.ts +93 -0
  148. package/templates/overlays/baas/ai-instructions.md +1 -1
  149. package/templates/overlays/baas/config/package.json +1 -0
  150. package/templates/overlays/baas/config/storage.ts +5 -3
  151. package/templates/overlays/baas/package.json +1 -0
  152. package/templates/template/.cursorrules +1 -1
  153. package/templates/template/.env.example +20 -10
  154. package/templates/template/.github/copilot-instructions.md +1 -1
  155. package/templates/template/.mcp.json +2 -2
  156. package/templates/template/.windsurfrules +1 -1
  157. package/templates/template/AGENTS.md +1 -1
  158. package/templates/template/CLAUDE.md +1 -1
  159. package/templates/template/README.md +21 -15
  160. package/templates/template/ai-instructions.md +5 -4
  161. package/templates/template/backend/functions/hello.ts +8 -0
  162. package/templates/template/backend/package.json +1 -1
  163. package/templates/template/config/resources.ts +7 -6
  164. package/templates/template/config/storage.ts +5 -3
  165. package/templates/template/docker-compose.yml +15 -9
  166. package/templates/template/frontend/vite.config.ts +13 -3
  167. package/templates/template/gitignore +6 -1
  168. package/templates/template/package.json +5 -3
  169. package/templates/template/scripts/example.ts +6 -4
  170. package/templates/template/tsconfig.json +25 -0
  171. package/dist/branch-pointer-dBiClJ0p.js +0 -111
  172. package/dist/branch-pointer-dBiClJ0p.js.map +0 -1
  173. package/dist/constraints-DKGbNfUW.js +0 -48
  174. package/dist/constraints-DKGbNfUW.js.map +0 -1
  175. package/dist/daemon-DR9Tk0La.js +0 -355
  176. package/dist/daemon-DR9Tk0La.js.map +0 -1
  177. package/dist/daemon-entry-LTFKtpHy.js.map +0 -1
  178. package/dist/dev-preflight-CNLl4rdo.js +0 -367
  179. package/dist/dev-preflight-CNLl4rdo.js.map +0 -1
  180. package/dist/project-3WCbrrfW.js.map +0 -1
  181. package/dist/pull-Dywm5FWP.js +0 -232
  182. package/dist/pull-Dywm5FWP.js.map +0 -1
  183. package/dist/resolve-BAeRUBhg.js +0 -74
  184. package/dist/resolve-BAeRUBhg.js.map +0 -1
  185. package/dist/state-C59Elrnt.js.map +0 -1
  186. package/templates/template/backend/src/schema.generated.ts +0 -13
@@ -0,0 +1,414 @@
1
+ import { d as readEnvFile, f as requireBackendDir, g as resolveTsx, o as findEnvFile, p as requireProjectRoot, r as exitDependenciesNotInstalled } from "./project-Wp2zoyeN.js";
2
+ import { n as parseCommandArgs, r as wantsHelp } from "./args-BnmsEXvu.js";
3
+ import { t as unknownCommand } from "./unknown-command-DNwDcX3w.js";
4
+ import chalk from "chalk";
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { spawn } from "child_process";
8
+ import { randomBytes } from "node:crypto";
9
+ //#region src/commands/auth.ts
10
+ /**
11
+ * CLI command: rebase auth <action>
12
+ *
13
+ * Subcommands:
14
+ * reset-password — Reset a user's password
15
+ */
16
+ /** The backend was reached and refused: not a reason to fall back to the database. */
17
+ var ApiAnswered = class extends Error {};
18
+ /** Everything the switch below dispatches, for the did-you-mean. */
19
+ var AUTH_SUBCOMMANDS = ["reset-password"];
20
+ /**
21
+ * Pick the user with exactly this email out of a search response.
22
+ *
23
+ * `/api/admin/users?search=` is an `ILIKE '%…%'` over email **or display
24
+ * name**, ordered by role count descending. This used to take row `[0]` and
25
+ * reset it, then print the email it had been *given* as confirmation — so two
26
+ * ordinary situations ended in a successful-looking reset of somebody else's
27
+ * account:
28
+ *
29
+ * - a substring collision: `bob@example.com` also matches
30
+ * `robert.bob@example.com`;
31
+ * - a display name, which is user-controlled and accepted up to 255
32
+ * characters with no constraint on its content, containing an address
33
+ * belonging to someone else.
34
+ *
35
+ * The ordering makes it worse rather than better — `array_length(roles) DESC
36
+ * NULLS LAST` puts the most privileged match first, so the account most likely
37
+ * to be reset by mistake is an admin's.
38
+ *
39
+ * Returns `undefined` when nothing matched exactly, which the caller reports
40
+ * rather than falling through to a guess. The direct-database fallback below
41
+ * has always matched with `eq(usersTable.email, email)`; this is the same
42
+ * definition, so the command no longer resets different accounts depending on
43
+ * whether the backend happened to be running.
44
+ */
45
+ function selectUserForEmail(payload, email) {
46
+ const wanted = email.trim().toLowerCase();
47
+ if (!wanted) return void 0;
48
+ const rows = Array.isArray(payload) ? payload : payload && typeof payload === "object" && Array.isArray(payload.users) ? payload.users : [];
49
+ for (const row of rows) {
50
+ if (!row || typeof row !== "object") continue;
51
+ const record = row;
52
+ const rowEmail = typeof record.email === "string" ? record.email.trim().toLowerCase() : void 0;
53
+ if (!rowEmail || rowEmail !== wanted) continue;
54
+ const id = typeof record.id === "string" ? record.id : typeof record.uid === "string" ? record.uid : void 0;
55
+ if (!id) continue;
56
+ return {
57
+ id,
58
+ email: record.email
59
+ };
60
+ }
61
+ }
62
+ async function authCommand(subcommand, rawArgs) {
63
+ if (!subcommand || subcommand === "--help" || wantsHelp(rawArgs)) {
64
+ printAuthHelp();
65
+ return;
66
+ }
67
+ switch (subcommand) {
68
+ case "reset-password":
69
+ await resetPassword(rawArgs);
70
+ break;
71
+ default: unknownCommand(subcommand, AUTH_SUBCOMMANDS, "auth");
72
+ }
73
+ }
74
+ /**
75
+ * The flags `rebase auth reset-password` takes.
76
+ *
77
+ * `-p` was advertised in this command's own help and never declared here, so
78
+ * `arg` — running permissively — pushed it into the positionals and the value
79
+ * *after* it shifted out of reach: anyone following the help set the account's
80
+ * password to the two-character string `-p`. Declared now, and `auth.test.ts`
81
+ * asserts that the help and this spec list the same aliases.
82
+ */
83
+ var RESET_PASSWORD_FLAGS = {
84
+ "--email": String,
85
+ "--password": String,
86
+ "-e": "--email",
87
+ "-p": "--password"
88
+ };
89
+ /**
90
+ * Which account, and which password, this invocation names.
91
+ *
92
+ * Both may still be absent — the caller reports a missing email — but neither
93
+ * can be a flag. `parseCommandArgs` parses the whole line strictly, so an
94
+ * undeclared flag is an error rather than a positional. That is what stops
95
+ * `rebase auth reset-password bob@example.com --debug` from setting Bob's
96
+ * password to `--debug`, which is the flag the CLI itself prints after every
97
+ * failure as the thing to re-run with.
98
+ *
99
+ * Exported so its tests can drive the real parser rather than a copy of it.
100
+ */
101
+ function resolveResetPasswordArgs(rawArgs) {
102
+ const { flags, positionals } = parseCommandArgs({
103
+ spec: RESET_PASSWORD_FLAGS,
104
+ rawArgs,
105
+ commandWords: 2,
106
+ command: "auth reset-password",
107
+ maxPositionals: 2
108
+ });
109
+ return {
110
+ email: flags["--email"] || positionals[0],
111
+ password: flags["--password"] || positionals[1]
112
+ };
113
+ }
114
+ /**
115
+ * A password for a reset that was not given one.
116
+ *
117
+ * This used to be a constant, and `--help` printed it as the default. Reset is
118
+ * the documented way back into an account nobody can sign in to — an admin,
119
+ * usually — so the recovery path set every such account to a fixed string that
120
+ * ships inside a public repository and a published npm package, and left it
121
+ * there until somebody remembered to change it.
122
+ *
123
+ * base64url of 18 random bytes: 24 characters, ~144 bits, no shell-quoting
124
+ * hazard, and nothing that reads like a placeholder somebody might keep.
125
+ */
126
+ function generatePassword() {
127
+ return randomBytes(18).toString("base64url");
128
+ }
129
+ /**
130
+ * What a refusing backend said, from its `{ error: { message, code } }` body.
131
+ *
132
+ * The body used to be printed as it came, so a weak password read
133
+ * `{"error":{"message":"…","code":"…"}}`. A body that is not the envelope is
134
+ * still shown, since it is the only account of the refusal there is.
135
+ */
136
+ async function refusalReason(response) {
137
+ const text = await response.text().catch(() => "");
138
+ try {
139
+ const parsed = JSON.parse(text);
140
+ const error = parsed && typeof parsed === "object" && "error" in parsed ? parsed.error : void 0;
141
+ if (error && typeof error === "object" && "message" in error && typeof error.message === "string") {
142
+ const code = "code" in error && typeof error.code === "string" ? ` (${error.code})` : "";
143
+ return `${error.message}${code}`;
144
+ }
145
+ } catch {}
146
+ return text.trim() || `${response.status} ${response.statusText}`.trim();
147
+ }
148
+ async function resetPassword(rawArgs) {
149
+ const { email, password: providedPassword } = resolveResetPasswordArgs(rawArgs);
150
+ const wasGenerated = !providedPassword;
151
+ const newPassword = providedPassword || generatePassword();
152
+ if (!email) {
153
+ console.error(chalk.red("✗ Email is required."));
154
+ console.log("");
155
+ console.log(chalk.gray(" Usage: rebase auth reset-password <email> [new-password]"));
156
+ console.log(chalk.gray(" rebase auth reset-password --email user@example.com --password NewPass123!"));
157
+ process.exit(1);
158
+ }
159
+ const projectRoot = requireProjectRoot();
160
+ const envFile = findEnvFile(projectRoot);
161
+ const envServiceKey = readEnvFile(projectRoot).REBASE_SERVICE_KEY;
162
+ let baseUrl = process.env.REBASE_BASE_URL;
163
+ let serviceKey = process.env.REBASE_SERVICE_KEY || envServiceKey;
164
+ const statePath = path.join(projectRoot, ".rebase", "state.json");
165
+ if (fs.existsSync(statePath)) try {
166
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
167
+ if (state && typeof state === "object") {
168
+ if (typeof state.baseUrl === "string" && !baseUrl) baseUrl = state.baseUrl;
169
+ if (typeof state.serviceKey === "string" && !serviceKey) serviceKey = state.serviceKey;
170
+ }
171
+ } catch {}
172
+ const devUrlPath = path.join(projectRoot, ".rebase-dev-url");
173
+ if (fs.existsSync(devUrlPath) && !baseUrl) try {
174
+ baseUrl = fs.readFileSync(devUrlPath, "utf8").trim();
175
+ } catch {}
176
+ if (baseUrl && serviceKey) {
177
+ console.log("Trying API-first reset via running backend...");
178
+ try {
179
+ const finalPass = newPassword;
180
+ const cleanBaseUrl = baseUrl.replace(/\/+$/, "");
181
+ const searchUrl = `${cleanBaseUrl}/api/admin/users?search=${encodeURIComponent(email)}&limit=50`;
182
+ const searchRes = await fetch(searchUrl, { headers: {
183
+ "Authorization": `Bearer ${serviceKey}`,
184
+ "Accept": "application/json"
185
+ } });
186
+ if (!searchRes.ok) throw new ApiAnswered(`Failed to list users: ${searchRes.status} ${searchRes.statusText}`);
187
+ const searchData = await searchRes.json();
188
+ if (!searchData || typeof searchData !== "object") throw new Error("Invalid response format from user search API.");
189
+ const matched = selectUserForEmail(searchData, email);
190
+ if (!matched) throw new ApiAnswered(`No user has the email ${email}.`);
191
+ const resetUrl = `${cleanBaseUrl}/api/admin/users/${matched.id}/reset-password`;
192
+ const resetRes = await fetch(resetUrl, {
193
+ method: "POST",
194
+ headers: {
195
+ "Authorization": `Bearer ${serviceKey}`,
196
+ "Content-Type": "application/json",
197
+ "Accept": "application/json"
198
+ },
199
+ body: JSON.stringify({ password: finalPass })
200
+ });
201
+ if (!resetRes.ok) throw new ApiAnswered(`Password reset refused: ${await refusalReason(resetRes)}`);
202
+ console.log("API reset successful.");
203
+ console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password (via API)"));
204
+ console.log("");
205
+ console.log(` ${chalk.gray("Email:")} ${matched.email}`);
206
+ console.log(` ${chalk.gray("Password:")} ${wasGenerated ? finalPass : "*".repeat(finalPass.length)}`);
207
+ console.log("");
208
+ return;
209
+ } catch (err) {
210
+ const errMsg = err instanceof Error ? err.message : String(err);
211
+ if (err instanceof ApiAnswered) {
212
+ console.error(chalk.red(`✗ ${errMsg}`));
213
+ process.exitCode = 1;
214
+ return;
215
+ }
216
+ console.warn(chalk.yellow("API reset failed, falling back to direct database update..."));
217
+ console.warn(chalk.gray(` Details: ${errMsg}`));
218
+ }
219
+ }
220
+ const backendDir = requireBackendDir(projectRoot);
221
+ const tsxBin = resolveTsx(projectRoot);
222
+ if (!tsxBin) exitDependenciesNotInstalled(projectRoot);
223
+ try {
224
+ const env = { ...process.env };
225
+ if (envFile) env.DOTENV_CONFIG_PATH = envFile;
226
+ env.REBASE_RESET_EMAIL = email;
227
+ env.REBASE_RESET_PASSWORD = newPassword;
228
+ env.REBASE_ENV_FILE_PATH = envFile || path.join(projectRoot, ".env");
229
+ const { prepareDatabaseEnv } = await import("./prepare-CDN6lSpz.js");
230
+ const prepared = await prepareDatabaseEnv(projectRoot, { onProgress: (message) => console.log(chalk.gray(` ${message}`)) });
231
+ Object.assign(env, prepared.env);
232
+ const scriptContent = resetPasswordScript(wasGenerated);
233
+ const tmpScriptPath = path.join(backendDir, ".tmp-reset-password.ts");
234
+ fs.writeFileSync(tmpScriptPath, scriptContent, "utf-8");
235
+ console.log("");
236
+ console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password (Direct DB Fallback)"));
237
+ console.log("");
238
+ console.log(` ${chalk.gray("Email:")} ${email}`);
239
+ console.log(` ${chalk.gray("Database:")} ${prepared.description}`);
240
+ if (!wasGenerated) console.log(` ${chalk.gray("Password:")} ${"*".repeat(newPassword.length)}`);
241
+ console.log("");
242
+ const child = spawn(tsxBin, [tmpScriptPath], {
243
+ cwd: backendDir,
244
+ stdio: "inherit",
245
+ env
246
+ });
247
+ const cleanup = () => {
248
+ try {
249
+ fs.unlinkSync(tmpScriptPath);
250
+ } catch {}
251
+ };
252
+ return new Promise((resolve) => {
253
+ child.on("error", (err) => {
254
+ cleanup();
255
+ console.error(chalk.red("✗ Could not run the reset script."));
256
+ console.error(chalk.gray(` ${err.message}`));
257
+ process.exit(1);
258
+ });
259
+ child.on("close", (code) => {
260
+ cleanup();
261
+ if (code !== 0) process.exit(code ?? 1);
262
+ resolve();
263
+ });
264
+ });
265
+ } catch (err) {
266
+ console.error(chalk.red("✗ Direct database update failed."));
267
+ console.error(err instanceof Error ? err.message : String(err));
268
+ process.exit(1);
269
+ }
270
+ }
271
+ /**
272
+ * The script the direct-database fallback runs under the project's own tsx, so
273
+ * it resolves the project's `@rebasepro/server-postgres` and schema.
274
+ *
275
+ * It ends the account's sessions in the transaction that sets the password,
276
+ * as every door through the API does (`replaceUserPassword`): the refresh
277
+ * tokens and outstanding reset links go, and `tokens_valid_after` voids the
278
+ * access tokens already handed out. An operator resetting a phished account
279
+ * with the backend down is the case this exists for, and a reset that left the
280
+ * attacker's refresh token minting access tokens did not recover anything.
281
+ *
282
+ * It exits 0 only when a row was updated: a missing user and a thrown error are
283
+ * both exit 1. `echoPassword` prints the new password, for a generated one.
284
+ *
285
+ * Exported so its tests can run it.
286
+ */
287
+ function resetPasswordScript(echoPassword) {
288
+ return `
289
+ import { createPostgresDatabaseConnection } from "@rebasepro/server-postgres";
290
+ import { hashPassword } from "@rebasepro/server";
291
+ import { getTableConfig } from "drizzle-orm/pg-core";
292
+ import * as dotenv from "dotenv";
293
+ import path from "path";
294
+ import fs from "fs";
295
+
296
+ dotenv.config({ path: process.env.REBASE_ENV_FILE_PATH, quiet: true });
297
+
298
+ // Lowercased, as every address is stored (\`normalizeEmail\`): matched as
299
+ // typed, "Ops@Acme.test" found no row.
300
+ const email = process.env.REBASE_RESET_EMAIL!.trim().toLowerCase();
301
+ const newPassword = process.env.REBASE_RESET_PASSWORD!;
302
+
303
+ const quote = (identifier: string) => '"' + identifier.replace(/"/g, '""') + '"';
304
+
305
+ async function resetPassword() {
306
+ const { pool } = createPostgresDatabaseConnection(process.env.DATABASE_URL!);
307
+ const hash = await hashPassword(newPassword);
308
+
309
+ let usersTable;
310
+ try {
311
+ const schemaPath = path.resolve("./src/schema.generated.ts");
312
+ if (fs.existsSync(schemaPath)) {
313
+ const schema = await import("file://" + schemaPath);
314
+ usersTable = schema.users || schema.tables?.users;
315
+ }
316
+ } catch (e) {
317
+ // ignore and fallback
318
+ }
319
+
320
+ if (!usersTable) {
321
+ const pgServer = await import("@rebasepro/server-postgres");
322
+ usersTable = pgServer.users;
323
+ }
324
+
325
+ // The physical names, from the table object: the collection may map its
326
+ // properties to other columns, and the statements below are plain SQL.
327
+ const { name: tableName, schema: tableSchema } = getTableConfig(usersTable);
328
+ const usersSchema = tableSchema || "public";
329
+ const users = quote(usersSchema) + "." + quote(tableName);
330
+ const idColumn = quote(usersTable.id.name);
331
+ const emailColumn = quote(usersTable.email.name);
332
+ const passwordColumn = quote((usersTable.passwordHash ?? usersTable.password_hash).name);
333
+ // Where \`ensureAuthTablesExist\` puts the tables that hang off the users
334
+ // table: beside it, or in \`rebase\` when it lives in \`public\`.
335
+ const authSchema = usersSchema === "public" ? "rebase" : usersSchema;
336
+
337
+ const client = await pool.connect();
338
+ try {
339
+ await client.query("BEGIN");
340
+ const updated = await client.query(
341
+ "UPDATE " + users + " SET " + passwordColumn + " = $1 WHERE " + emailColumn + " = $2 " +
342
+ "RETURNING " + idColumn + " AS id, " + emailColumn + " AS email",
343
+ [hash, email]
344
+ );
345
+ if (updated.rows.length === 0) {
346
+ await client.query("ROLLBACK");
347
+ // Nothing was updated, so nothing was reset. Exiting 0 here reported
348
+ // success for a no-op, which is what a script would have believed.
349
+ console.error("✗ User not found: " + email);
350
+ process.exit(1);
351
+ }
352
+ const uid = updated.rows[0].id;
353
+
354
+ // A table or column the server has not created yet (it never booted
355
+ // against this database) holds no session to end.
356
+ const watermark = await client.query(
357
+ "SELECT 1 FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 AND column_name = 'tokens_valid_after'",
358
+ [usersSchema, tableName]
359
+ );
360
+ if (watermark.rows.length > 0) {
361
+ await client.query("UPDATE " + users + " SET tokens_valid_after = NOW() WHERE " + idColumn + " = $1", [uid]);
362
+ }
363
+ for (const table of ["refresh_tokens", "password_reset_tokens"]) {
364
+ const qualified = quote(authSchema) + "." + quote(table);
365
+ const present = await client.query("SELECT to_regclass($1) IS NOT NULL AS present", [qualified]);
366
+ if (present.rows[0]?.present) {
367
+ await client.query("DELETE FROM " + qualified + " WHERE uid = $1", [uid]);
368
+ }
369
+ }
370
+ await client.query("COMMIT");
371
+
372
+ console.log("✅ Password reset for: " + updated.rows[0].email);
373
+ console.log(" Every session of this account has ended.");
374
+ ${echoPassword ? "console.log(\" Password: \" + newPassword);" : ""}
375
+ process.exit(0);
376
+ } catch (error) {
377
+ await client.query("ROLLBACK").catch(() => undefined);
378
+ throw error;
379
+ } finally {
380
+ client.release();
381
+ }
382
+ }
383
+
384
+ // A thrown error is a failed reset, and exits like one — a refused connection
385
+ // that exits 0 reads as a reset to anything checking the exit code.
386
+ resetPassword().catch((error) => {
387
+ console.error(error);
388
+ process.exit(1);
389
+ });
390
+ `;
391
+ }
392
+ function printAuthHelp() {
393
+ console.log(`
394
+ ${chalk.bold("rebase auth")} — Authentication management commands
395
+
396
+ ${chalk.green.bold("Usage")}
397
+ rebase auth ${chalk.blue("<command>")} [options]
398
+
399
+ ${chalk.green.bold("Commands")}
400
+ ${chalk.blue.bold("reset-password")} Reset a user's password
401
+
402
+ ${chalk.green.bold("reset-password Options")}
403
+ ${chalk.blue("--email, -e")} User's email address
404
+ ${chalk.blue("--password, -p")} New password (default: one is generated and printed)
405
+
406
+ ${chalk.green.bold("Examples")}
407
+ rebase auth reset-password user@example.com
408
+ rebase auth reset-password --email user@example.com --password MyNewPass!
409
+ `);
410
+ }
411
+ //#endregion
412
+ export { AUTH_SUBCOMMANDS, authCommand };
413
+
414
+ //# sourceMappingURL=auth-aySHEF45.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-aySHEF45.js","names":[],"sources":["../src/commands/auth.ts"],"sourcesContent":["/**\n * CLI command: rebase auth <action>\n *\n * Subcommands:\n * reset-password — Reset a user's password\n */\nimport chalk from \"chalk\";\nimport path from \"path\";\nimport fs from \"fs\";\nimport { spawn } from \"child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport {\n requireProjectRoot,\n requireBackendDir,\n findEnvFile,\n readEnvFile,\n resolveTsx,\n exitDependenciesNotInstalled\n} from \"../utils/project\";\nimport { unknownCommand } from \"../utils/unknown-command\";\n\n/** The backend was reached and refused: not a reason to fall back to the database. */\nclass ApiAnswered extends Error {}\n\n/** Everything the switch below dispatches, for the did-you-mean. */\nexport const AUTH_SUBCOMMANDS = [\"reset-password\"] as const;\nimport { parseCommandArgs, wantsHelp } from \"../utils/args\";\n\n/** A user as the admin API returns it, reduced to what this command needs. */\nexport interface ResolvedUser {\n id: string;\n email: string;\n}\n\n/**\n * Pick the user with exactly this email out of a search response.\n *\n * `/api/admin/users?search=` is an `ILIKE '%…%'` over email **or display\n * name**, ordered by role count descending. This used to take row `[0]` and\n * reset it, then print the email it had been *given* as confirmation — so two\n * ordinary situations ended in a successful-looking reset of somebody else's\n * account:\n *\n * - a substring collision: `bob@example.com` also matches\n * `robert.bob@example.com`;\n * - a display name, which is user-controlled and accepted up to 255\n * characters with no constraint on its content, containing an address\n * belonging to someone else.\n *\n * The ordering makes it worse rather than better — `array_length(roles) DESC\n * NULLS LAST` puts the most privileged match first, so the account most likely\n * to be reset by mistake is an admin's.\n *\n * Returns `undefined` when nothing matched exactly, which the caller reports\n * rather than falling through to a guess. The direct-database fallback below\n * has always matched with `eq(usersTable.email, email)`; this is the same\n * definition, so the command no longer resets different accounts depending on\n * whether the backend happened to be running.\n */\nexport function selectUserForEmail(payload: unknown, email: string): ResolvedUser | undefined {\n const wanted = email.trim().toLowerCase();\n if (!wanted) return undefined;\n\n const rows: unknown[] = Array.isArray(payload)\n ? payload\n : (payload && typeof payload === \"object\" && Array.isArray((payload as { users?: unknown }).users)\n ? (payload as { users: unknown[] }).users\n : []);\n\n for (const row of rows) {\n if (!row || typeof row !== \"object\") continue;\n const record = row as Record<string, unknown>;\n const rowEmail = typeof record.email === \"string\" ? record.email.trim().toLowerCase() : undefined;\n if (!rowEmail || rowEmail !== wanted) continue;\n\n const id = typeof record.id === \"string\"\n ? record.id\n : (typeof record.uid === \"string\" ? record.uid : undefined);\n if (!id) continue;\n\n return { id, email: record.email as string };\n }\n\n return undefined;\n}\n\nexport async function authCommand(subcommand: string | undefined, rawArgs: string[]): Promise<void> {\n // `--help` is answered here and never reaches a handler. `cli.ts` only\n // rewrites the subcommand to `\"--help\"` when no subcommand was named, so\n // `rebase auth reset-password --help` used to *run the reset*: `--help`\n // became the email, the backend was contacted, `.tmp-reset-password.ts` was\n // written into the user's `backend/`, and a database UPDATE ran for a user\n // named `--help`. A flag whose whole job is to print text cannot be allowed\n // to reach code that writes.\n if (!subcommand || subcommand === \"--help\" || wantsHelp(rawArgs)) {\n printAuthHelp();\n return;\n }\n\n switch (subcommand) {\n case \"reset-password\":\n await resetPassword(rawArgs);\n break;\n default:\n unknownCommand(subcommand, AUTH_SUBCOMMANDS, \"auth\");\n }\n}\n\n/**\n * The flags `rebase auth reset-password` takes.\n *\n * `-p` was advertised in this command's own help and never declared here, so\n * `arg` — running permissively — pushed it into the positionals and the value\n * *after* it shifted out of reach: anyone following the help set the account's\n * password to the two-character string `-p`. Declared now, and `auth.test.ts`\n * asserts that the help and this spec list the same aliases.\n */\nexport const RESET_PASSWORD_FLAGS = {\n \"--email\": String,\n \"--password\": String,\n \"-e\": \"--email\",\n \"-p\": \"--password\"\n} as const;\n\n/**\n * Which account, and which password, this invocation names.\n *\n * Both may still be absent — the caller reports a missing email — but neither\n * can be a flag. `parseCommandArgs` parses the whole line strictly, so an\n * undeclared flag is an error rather than a positional. That is what stops\n * `rebase auth reset-password bob@example.com --debug` from setting Bob's\n * password to `--debug`, which is the flag the CLI itself prints after every\n * failure as the thing to re-run with.\n *\n * Exported so its tests can drive the real parser rather than a copy of it.\n */\nexport function resolveResetPasswordArgs(rawArgs: string[]): {\n email?: string;\n password?: string;\n} {\n const { flags, positionals } = parseCommandArgs({\n spec: RESET_PASSWORD_FLAGS,\n rawArgs,\n commandWords: 2,\n command: \"auth reset-password\",\n maxPositionals: 2\n });\n\n // Both spellings are supported: `<email> [password]` and `--email/--password`.\n return {\n email: flags[\"--email\"] || positionals[0],\n password: flags[\"--password\"] || positionals[1]\n };\n}\n\n/**\n * A password for a reset that was not given one.\n *\n * This used to be a constant, and `--help` printed it as the default. Reset is\n * the documented way back into an account nobody can sign in to — an admin,\n * usually — so the recovery path set every such account to a fixed string that\n * ships inside a public repository and a published npm package, and left it\n * there until somebody remembered to change it.\n *\n * base64url of 18 random bytes: 24 characters, ~144 bits, no shell-quoting\n * hazard, and nothing that reads like a placeholder somebody might keep.\n */\nexport function generatePassword(): string {\n return randomBytes(18).toString(\"base64url\");\n}\n\n/**\n * What a refusing backend said, from its `{ error: { message, code } }` body.\n *\n * The body used to be printed as it came, so a weak password read\n * `{\"error\":{\"message\":\"…\",\"code\":\"…\"}}`. A body that is not the envelope is\n * still shown, since it is the only account of the refusal there is.\n */\nasync function refusalReason(response: Response): Promise<string> {\n const text = await response.text().catch(() => \"\");\n try {\n const parsed: unknown = JSON.parse(text);\n const error = parsed && typeof parsed === \"object\" && \"error\" in parsed ? parsed.error : undefined;\n if (error && typeof error === \"object\" && \"message\" in error && typeof error.message === \"string\") {\n const code = \"code\" in error && typeof error.code === \"string\" ? ` (${error.code})` : \"\";\n return `${error.message}${code}`;\n }\n } catch {\n // Not JSON: the text is the answer.\n }\n return text.trim() || `${response.status} ${response.statusText}`.trim();\n}\n\nasync function resetPassword(rawArgs: string[]): Promise<void> {\n const { email, password: providedPassword } = resolveResetPasswordArgs(rawArgs);\n // Generated once, so both reset paths set and report the same thing.\n const wasGenerated = !providedPassword;\n const newPassword = providedPassword || generatePassword();\n\n if (!email) {\n console.error(chalk.red(\"✗ Email is required.\"));\n console.log(\"\");\n console.log(chalk.gray(\" Usage: rebase auth reset-password <email> [new-password]\"));\n console.log(chalk.gray(\" rebase auth reset-password --email user@example.com --password NewPass123!\"));\n process.exit(1);\n }\n\n const projectRoot = requireProjectRoot();\n\n // 1. Try API-first reset\n // Was a single-key regex, which did not match `export REBASE_SERVICE_KEY=…`\n // and kept a trailing `# comment` in the value. The command then fell\n // through to the direct-database path as though no key were configured.\n const envFile = findEnvFile(projectRoot);\n const envServiceKey: string | undefined = readEnvFile(projectRoot).REBASE_SERVICE_KEY;\n\n let baseUrl = process.env.REBASE_BASE_URL;\n let serviceKey = process.env.REBASE_SERVICE_KEY || envServiceKey;\n\n const statePath = path.join(projectRoot, \".rebase\", \"state.json\");\n if (fs.existsSync(statePath)) {\n try {\n const state = JSON.parse(fs.readFileSync(statePath, \"utf8\")) as Record<string, unknown>;\n if (state && typeof state === \"object\") {\n if (typeof state.baseUrl === \"string\" && !baseUrl) {\n baseUrl = state.baseUrl;\n }\n if (typeof state.serviceKey === \"string\" && !serviceKey) {\n serviceKey = state.serviceKey;\n }\n }\n } catch {\n // Ignore\n }\n }\n\n const devUrlPath = path.join(projectRoot, \".rebase-dev-url\");\n if (fs.existsSync(devUrlPath) && !baseUrl) {\n try {\n baseUrl = fs.readFileSync(devUrlPath, \"utf8\").trim();\n } catch {\n // Ignore\n }\n }\n\n if (baseUrl && serviceKey) {\n console.log(\"Trying API-first reset via running backend...\");\n try {\n const finalPass = newPassword;\n const cleanBaseUrl = baseUrl.replace(/\\/+$/, \"\");\n // Not `limit=1`: the search is fuzzy and ordered by role count, so\n // the exact match is not necessarily first — asking for one row can\n // make it unreachable. Ask for a page, then match exactly.\n const searchUrl = `${cleanBaseUrl}/api/admin/users?search=${encodeURIComponent(email)}&limit=50`;\n const searchRes = await fetch(searchUrl, {\n headers: {\n \"Authorization\": `Bearer ${serviceKey}`,\n \"Accept\": \"application/json\"\n }\n });\n if (!searchRes.ok) {\n throw new ApiAnswered(`Failed to list users: ${searchRes.status} ${searchRes.statusText}`);\n }\n const searchData = await searchRes.json() as unknown;\n if (!searchData || typeof searchData !== \"object\") {\n throw new Error(\"Invalid response format from user search API.\");\n }\n\n const matched = selectUserForEmail(searchData, email);\n if (!matched) {\n throw new ApiAnswered(`No user has the email ${email}.`);\n }\n\n const resetUrl = `${cleanBaseUrl}/api/admin/users/${matched.id}/reset-password`;\n const resetRes = await fetch(resetUrl, {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${serviceKey}`,\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/json\"\n },\n body: JSON.stringify({ password: finalPass })\n });\n\n if (!resetRes.ok) {\n throw new ApiAnswered(`Password reset refused: ${await refusalReason(resetRes)}`);\n }\n\n console.log(\"API reset successful.\");\n console.log(chalk.bold(\" 🔑 Rebase Auth — Reset Password (via API)\"));\n console.log(\"\");\n // The address as stored, not as typed — they differ in case often\n // enough that echoing the input hides which account was touched.\n console.log(` ${chalk.gray(\"Email:\")} ${matched.email}`);\n // Echoed only when we invented it. A password the operator typed\n // is already theirs; printing it again only adds scrollback.\n console.log(` ${chalk.gray(\"Password:\")} ${wasGenerated ? finalPass : \"*\".repeat(finalPass.length)}`);\n console.log(\"\");\n return;\n } catch (err) {\n const errMsg = err instanceof Error ? err.message : String(err);\n // The backend answered and said no — a password too weak, a user\n // it does not have. That answer stands: writing the hash straight\n // into the database instead skipped the strength rule it had just\n // applied, and ended no sessions. Only a backend that could not be\n // reached is a reason to go round it.\n if (err instanceof ApiAnswered) {\n console.error(chalk.red(`✗ ${errMsg}`));\n process.exitCode = 1;\n return;\n }\n console.warn(chalk.yellow(\"API reset failed, falling back to direct database update...\"));\n console.warn(chalk.gray(` Details: ${errMsg}`));\n }\n }\n\n // 2. Direct-DB Fallback\n const backendDir = requireBackendDir(projectRoot);\n const tsxBin = resolveTsx(projectRoot);\n\n if (!tsxBin) {\n exitDependenciesNotInstalled(projectRoot);\n }\n\n try {\n const env: Record<string, string> = { ...process.env as Record<string, string> };\n if (envFile) {\n env.DOTENV_CONFIG_PATH = envFile;\n }\n env.REBASE_RESET_EMAIL = email;\n env.REBASE_RESET_PASSWORD = newPassword;\n env.REBASE_ENV_FILE_PATH = envFile || path.join(projectRoot, \".env\");\n\n // The database the `db` family would use, resolved the same way. A\n // stock scaffold leaves DATABASE_URL commented out and runs on the\n // managed database, which only exists in a child's environment when\n // something puts it there — without this the script connected with\n // libpq's defaults, localhost:5432 as the OS user. An external\n // DATABASE_URL adds nothing here: the child already reads it.\n const { prepareDatabaseEnv } = await import(\"../dev-db/prepare\");\n const prepared = await prepareDatabaseEnv(projectRoot, {\n onProgress: (message) => console.log(chalk.gray(` ${message}`))\n });\n Object.assign(env, prepared.env);\n\n const scriptContent = resetPasswordScript(wasGenerated);\n\n const tmpScriptPath = path.join(backendDir, \".tmp-reset-password.ts\");\n fs.writeFileSync(tmpScriptPath, scriptContent, \"utf-8\");\n\n console.log(\"\");\n console.log(chalk.bold(\" 🔑 Rebase Auth — Reset Password (Direct DB Fallback)\"));\n console.log(\"\");\n console.log(` ${chalk.gray(\"Email:\")} ${email}`);\n console.log(` ${chalk.gray(\"Database:\")} ${prepared.description}`);\n if (!wasGenerated) {\n console.log(` ${chalk.gray(\"Password:\")} ${\"*\".repeat(newPassword.length)}`);\n }\n console.log(\"\");\n\n const child = spawn(tsxBin, [tmpScriptPath], {\n cwd: backendDir,\n stdio: \"inherit\",\n env\n });\n\n // The script is written into the user's backend directory, so every\n // exit has to remove it. Without an `error` handler a failed spawn\n // raises an unhandled event, the process dies before `close`, and\n // `.tmp-reset-password.ts` is left behind to be committed.\n const cleanup = () => {\n try { fs.unlinkSync(tmpScriptPath); } catch { /* already gone */ }\n };\n\n return new Promise((resolve) => {\n child.on(\"error\", (err) => {\n cleanup();\n console.error(chalk.red(\"✗ Could not run the reset script.\"));\n console.error(chalk.gray(` ${err.message}`));\n process.exit(1);\n });\n child.on(\"close\", (code) => {\n cleanup();\n if (code !== 0) {\n process.exit(code ?? 1);\n }\n resolve();\n });\n });\n } catch (err) {\n console.error(chalk.red(\"✗ Direct database update failed.\"));\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n}\n\n/**\n * The script the direct-database fallback runs under the project's own tsx, so\n * it resolves the project's `@rebasepro/server-postgres` and schema.\n *\n * It ends the account's sessions in the transaction that sets the password,\n * as every door through the API does (`replaceUserPassword`): the refresh\n * tokens and outstanding reset links go, and `tokens_valid_after` voids the\n * access tokens already handed out. An operator resetting a phished account\n * with the backend down is the case this exists for, and a reset that left the\n * attacker's refresh token minting access tokens did not recover anything.\n *\n * It exits 0 only when a row was updated: a missing user and a thrown error are\n * both exit 1. `echoPassword` prints the new password, for a generated one.\n *\n * Exported so its tests can run it.\n */\nexport function resetPasswordScript(echoPassword: boolean): string {\n return `\nimport { createPostgresDatabaseConnection } from \"@rebasepro/server-postgres\";\nimport { hashPassword } from \"@rebasepro/server\";\nimport { getTableConfig } from \"drizzle-orm/pg-core\";\nimport * as dotenv from \"dotenv\";\nimport path from \"path\";\nimport fs from \"fs\";\n\ndotenv.config({ path: process.env.REBASE_ENV_FILE_PATH, quiet: true });\n\n// Lowercased, as every address is stored (\\`normalizeEmail\\`): matched as\n// typed, \"Ops@Acme.test\" found no row.\nconst email = process.env.REBASE_RESET_EMAIL!.trim().toLowerCase();\nconst newPassword = process.env.REBASE_RESET_PASSWORD!;\n\nconst quote = (identifier: string) => '\"' + identifier.replace(/\"/g, '\"\"') + '\"';\n\nasync function resetPassword() {\n const { pool } = createPostgresDatabaseConnection(process.env.DATABASE_URL!);\n const hash = await hashPassword(newPassword);\n\n let usersTable;\n try {\n const schemaPath = path.resolve(\"./src/schema.generated.ts\");\n if (fs.existsSync(schemaPath)) {\n const schema = await import(\"file://\" + schemaPath);\n usersTable = schema.users || schema.tables?.users;\n }\n } catch (e) {\n // ignore and fallback\n }\n\n if (!usersTable) {\n const pgServer = await import(\"@rebasepro/server-postgres\");\n usersTable = pgServer.users;\n }\n\n // The physical names, from the table object: the collection may map its\n // properties to other columns, and the statements below are plain SQL.\n const { name: tableName, schema: tableSchema } = getTableConfig(usersTable);\n const usersSchema = tableSchema || \"public\";\n const users = quote(usersSchema) + \".\" + quote(tableName);\n const idColumn = quote(usersTable.id.name);\n const emailColumn = quote(usersTable.email.name);\n const passwordColumn = quote((usersTable.passwordHash ?? usersTable.password_hash).name);\n // Where \\`ensureAuthTablesExist\\` puts the tables that hang off the users\n // table: beside it, or in \\`rebase\\` when it lives in \\`public\\`.\n const authSchema = usersSchema === \"public\" ? \"rebase\" : usersSchema;\n\n const client = await pool.connect();\n try {\n await client.query(\"BEGIN\");\n const updated = await client.query(\n \"UPDATE \" + users + \" SET \" + passwordColumn + \" = $1 WHERE \" + emailColumn + \" = $2 \" +\n \"RETURNING \" + idColumn + \" AS id, \" + emailColumn + \" AS email\",\n [hash, email]\n );\n if (updated.rows.length === 0) {\n await client.query(\"ROLLBACK\");\n // Nothing was updated, so nothing was reset. Exiting 0 here reported\n // success for a no-op, which is what a script would have believed.\n console.error(\"✗ User not found: \" + email);\n process.exit(1);\n }\n const uid = updated.rows[0].id;\n\n // A table or column the server has not created yet (it never booted\n // against this database) holds no session to end.\n const watermark = await client.query(\n \"SELECT 1 FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 AND column_name = 'tokens_valid_after'\",\n [usersSchema, tableName]\n );\n if (watermark.rows.length > 0) {\n await client.query(\"UPDATE \" + users + \" SET tokens_valid_after = NOW() WHERE \" + idColumn + \" = $1\", [uid]);\n }\n for (const table of [\"refresh_tokens\", \"password_reset_tokens\"]) {\n const qualified = quote(authSchema) + \".\" + quote(table);\n const present = await client.query(\"SELECT to_regclass($1) IS NOT NULL AS present\", [qualified]);\n if (present.rows[0]?.present) {\n await client.query(\"DELETE FROM \" + qualified + \" WHERE uid = $1\", [uid]);\n }\n }\n await client.query(\"COMMIT\");\n\n console.log(\"✅ Password reset for: \" + updated.rows[0].email);\n console.log(\" Every session of this account has ended.\");\n ${echoPassword ? 'console.log(\" Password: \" + newPassword);' : \"\"}\n process.exit(0);\n } catch (error) {\n await client.query(\"ROLLBACK\").catch(() => undefined);\n throw error;\n } finally {\n client.release();\n }\n}\n\n// A thrown error is a failed reset, and exits like one — a refused connection\n// that exits 0 reads as a reset to anything checking the exit code.\nresetPassword().catch((error) => {\n console.error(error);\n process.exit(1);\n});\n`;\n}\n\nfunction printAuthHelp() {\n console.log(`\n${chalk.bold(\"rebase auth\")} — Authentication management commands\n\n${chalk.green.bold(\"Usage\")}\n rebase auth ${chalk.blue(\"<command>\")} [options]\n\n${chalk.green.bold(\"Commands\")}\n ${chalk.blue.bold(\"reset-password\")} Reset a user's password\n\n${chalk.green.bold(\"reset-password Options\")}\n ${chalk.blue(\"--email, -e\")} User's email address\n ${chalk.blue(\"--password, -p\")} New password (default: one is generated and printed)\n\n${chalk.green.bold(\"Examples\")}\n rebase auth reset-password user@example.com\n rebase auth reset-password --email user@example.com --password MyNewPass!\n`);\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { C as writeActiveBranch, S as readActiveBranch, _ as branchDatabaseName, b as clearActiveBranch, x as databaseNameOf } from "./prepare-CDN6lSpz.js";
2
+ export { branchDatabaseName, clearActiveBranch, databaseNameOf, readActiveBranch, writeActiveBranch };