libretto 0.6.31 → 0.6.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.template.md +1 -1
- package/dist/cli/commands/auth.js +119 -268
- package/dist/cli/commands/browser.js +1 -0
- package/dist/cli/commands/cloud-credentials.js +5 -17
- package/dist/cli/commands/cloud-jobs.js +125 -0
- package/dist/cli/commands/cloud-schedules.js +128 -0
- package/dist/cli/commands/cloud-settings.js +75 -0
- package/dist/cli/commands/cloud-sharing.js +13 -27
- package/dist/cli/commands/deploy.js +7 -16
- package/dist/cli/commands/execution.js +3 -2
- package/dist/cli/commands/profiles.js +8 -21
- package/dist/cli/commands/shared.js +17 -0
- package/dist/cli/core/browser.js +2 -1
- package/dist/cli/core/daemon/daemon.js +2 -1
- package/dist/cli/core/daemon/ipc.js +23 -16
- package/dist/cli/core/deploy-artifact.js +41 -16
- package/dist/cli/core/providers/kernel.js +3 -2
- package/dist/cli/core/providers/libretto-cloud.js +2 -1
- package/dist/cli/core/telemetry.js +14 -2
- package/dist/cli/router.js +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/shared/workflow/workflow.d.ts +18 -0
- package/dist/shared/workflow/workflow.js +9 -0
- package/package.json +1 -1
- package/skills/libretto/SKILL.md +17 -2
- package/skills/libretto/references/website-authentication.md +18 -2
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/commands/auth.ts +169 -382
- package/src/cli/commands/browser.ts +1 -0
- package/src/cli/commands/cloud-credentials.ts +6 -18
- package/src/cli/commands/cloud-jobs.ts +157 -0
- package/src/cli/commands/cloud-schedules.ts +164 -0
- package/src/cli/commands/cloud-settings.ts +101 -0
- package/src/cli/commands/cloud-sharing.ts +20 -28
- package/src/cli/commands/deploy.ts +8 -19
- package/src/cli/commands/execution.ts +2 -1
- package/src/cli/commands/profiles.ts +10 -22
- package/src/cli/commands/shared.ts +29 -0
- package/src/cli/core/browser.ts +2 -0
- package/src/cli/core/daemon/config.ts +1 -0
- package/src/cli/core/daemon/daemon.ts +1 -0
- package/src/cli/core/daemon/ipc.ts +27 -18
- package/src/cli/core/deploy-artifact.ts +63 -14
- package/src/cli/core/providers/kernel.ts +3 -2
- package/src/cli/core/providers/libretto-cloud.ts +1 -0
- package/src/cli/core/providers/types.ts +1 -0
- package/src/cli/core/telemetry.ts +15 -1
- package/src/cli/router.ts +6 -0
- package/src/index.ts +1 -0
- package/src/shared/workflow/workflow.ts +22 -0
package/src/cli/commands/auth.ts
CHANGED
|
@@ -3,10 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* libretto cloud auth signup
|
|
5
5
|
* libretto cloud auth login
|
|
6
|
-
* libretto cloud auth forgot-password
|
|
7
6
|
* libretto cloud auth logout
|
|
8
|
-
* libretto cloud auth invite <email> [--role member|
|
|
9
|
-
* libretto cloud auth accept-invite <tenantSlug> <invitationId>
|
|
7
|
+
* libretto cloud auth invite <email> [--role member|owner]
|
|
10
8
|
* libretto cloud auth api-key issue [--label <label>]
|
|
11
9
|
* libretto cloud auth api-key list
|
|
12
10
|
* libretto cloud auth api-key revoke <id>
|
|
@@ -17,10 +15,10 @@
|
|
|
17
15
|
* available, with LIBRETTO_API_KEY winning when set.
|
|
18
16
|
*/
|
|
19
17
|
|
|
18
|
+
import { spawn } from "node:child_process";
|
|
20
19
|
import { z } from "zod";
|
|
21
20
|
import { SimpleCLI } from "affordance";
|
|
22
21
|
import {
|
|
23
|
-
ApiCallError,
|
|
24
22
|
betterAuthCall,
|
|
25
23
|
NOT_AUTHENTICATED_MESSAGE,
|
|
26
24
|
orpcCall,
|
|
@@ -32,34 +30,14 @@ import {
|
|
|
32
30
|
authStatePath,
|
|
33
31
|
clearAuthState,
|
|
34
32
|
readAuthState,
|
|
35
|
-
setCookieToCookieHeader,
|
|
36
33
|
writeAuthState,
|
|
37
34
|
type AuthState,
|
|
38
35
|
} from "../core/auth-storage.js";
|
|
39
|
-
import { prompt, promptPassword, slugify } from "../core/prompt.js";
|
|
40
36
|
|
|
41
37
|
// ---------------------------------------------------------------------------
|
|
42
38
|
// Shared helpers
|
|
43
39
|
// ---------------------------------------------------------------------------
|
|
44
40
|
|
|
45
|
-
function isSlugTakenData(data: unknown): boolean {
|
|
46
|
-
return (
|
|
47
|
-
!!data &&
|
|
48
|
-
typeof data === "object" &&
|
|
49
|
-
(data as { reason?: unknown }).reason === "slug_taken"
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
type SignupResponse = {
|
|
54
|
-
userId: string;
|
|
55
|
-
email: string;
|
|
56
|
-
organizationId: string;
|
|
57
|
-
organizationSlug: string | null;
|
|
58
|
-
sessionToken: string | null;
|
|
59
|
-
setCookie: string[];
|
|
60
|
-
emailVerified: boolean;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
41
|
type Session = {
|
|
64
42
|
user: { id: string; email: string; emailVerified: boolean; name?: string };
|
|
65
43
|
session: { id: string; expiresAt: string };
|
|
@@ -83,6 +61,57 @@ type ApiKeyListItem = {
|
|
|
83
61
|
lastRequest?: string | null;
|
|
84
62
|
};
|
|
85
63
|
|
|
64
|
+
type CliLoginCreateResponse = {
|
|
65
|
+
requestId: string;
|
|
66
|
+
secret: string;
|
|
67
|
+
expiresAt: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
type CliLoginPollResponse =
|
|
71
|
+
| { status: "pending" }
|
|
72
|
+
| { status: "expired" }
|
|
73
|
+
| {
|
|
74
|
+
status: "approved";
|
|
75
|
+
cookieHeader: string;
|
|
76
|
+
userId: string;
|
|
77
|
+
email: string;
|
|
78
|
+
emailVerified: boolean;
|
|
79
|
+
sessionExpiresAt: string | null;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function resolveHostedWebsiteUrl(): string {
|
|
83
|
+
return process.env.LIBRETTO_WEBSITE_URL?.trim() || "https://libretto.sh";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sleep(ms: number): Promise<void> {
|
|
87
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function openBrowser(url: string): boolean {
|
|
91
|
+
const command =
|
|
92
|
+
process.platform === "darwin"
|
|
93
|
+
? "open"
|
|
94
|
+
: process.platform === "win32"
|
|
95
|
+
? "cmd"
|
|
96
|
+
: "xdg-open";
|
|
97
|
+
const args =
|
|
98
|
+
process.platform === "win32"
|
|
99
|
+
? ["/c", "start", "", url]
|
|
100
|
+
: [url];
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const child = spawn(command, args, {
|
|
104
|
+
detached: true,
|
|
105
|
+
stdio: "ignore",
|
|
106
|
+
});
|
|
107
|
+
child.on("error", () => {});
|
|
108
|
+
child.unref();
|
|
109
|
+
return true;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
86
115
|
async function getCurrentSession(
|
|
87
116
|
apiUrl: string,
|
|
88
117
|
/**
|
|
@@ -106,44 +135,106 @@ async function getCurrentSession(
|
|
|
106
135
|
}
|
|
107
136
|
}
|
|
108
137
|
|
|
109
|
-
async function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
): Promise<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
138
|
+
async function runBrowserAuthFlow(options: {
|
|
139
|
+
mode: "login" | "signup";
|
|
140
|
+
apiUrl: string;
|
|
141
|
+
websiteUrl: string;
|
|
142
|
+
}): Promise<void> {
|
|
143
|
+
const login = await orpcCall<CliLoginCreateResponse>({
|
|
144
|
+
apiUrl: options.apiUrl,
|
|
145
|
+
path: "/v1/auth/cliLoginCreate",
|
|
146
|
+
unauthenticated: true,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const loginUrl = new URL("/signin", options.websiteUrl);
|
|
150
|
+
loginUrl.searchParams.set("cliLoginId", login.requestId);
|
|
151
|
+
loginUrl.searchParams.set("cliLoginSecret", login.secret);
|
|
152
|
+
if (options.mode === "signup") {
|
|
153
|
+
loginUrl.searchParams.set("mode", "signup");
|
|
123
154
|
}
|
|
155
|
+
|
|
156
|
+
console.log(
|
|
157
|
+
options.mode === "signup"
|
|
158
|
+
? "Sign up for Libretto Cloud in your browser:"
|
|
159
|
+
: "Sign in to Libretto Cloud in your browser:",
|
|
160
|
+
);
|
|
161
|
+
console.log(` ${loginUrl.toString()}`);
|
|
124
162
|
console.log();
|
|
125
|
-
|
|
126
|
-
|
|
163
|
+
if (openBrowser(loginUrl.toString())) {
|
|
164
|
+
console.log("Opened the page in your default browser.");
|
|
165
|
+
console.log("If it didn't open, copy the link above into your browser.");
|
|
166
|
+
console.log();
|
|
167
|
+
} else {
|
|
168
|
+
console.log("Copy the link above into your browser.");
|
|
169
|
+
console.log();
|
|
170
|
+
}
|
|
171
|
+
console.log(
|
|
172
|
+
options.mode === "signup"
|
|
173
|
+
? "Waiting for browser sign-up"
|
|
174
|
+
: "Waiting for browser sign-in",
|
|
175
|
+
);
|
|
127
176
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
177
|
+
const expiresAt = new Date(login.expiresAt).getTime();
|
|
178
|
+
let verificationHintShown = false;
|
|
179
|
+
while (Date.now() < expiresAt) {
|
|
180
|
+
const result = await orpcCall<CliLoginPollResponse>({
|
|
181
|
+
apiUrl: options.apiUrl,
|
|
182
|
+
path: "/v1/auth/cliLoginPoll",
|
|
183
|
+
input: {
|
|
184
|
+
requestId: login.requestId,
|
|
185
|
+
secret: login.secret,
|
|
186
|
+
},
|
|
187
|
+
unauthenticated: true,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (result.status === "expired") {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Auth request expired. Run \`libretto cloud auth ${options.mode}\` again.`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (result.status === "approved") {
|
|
197
|
+
const session = await getCurrentSession(options.apiUrl, result.cookieHeader);
|
|
198
|
+
if (!session?.user?.id) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
"Browser auth succeeded, but the returned session could not be verified.",
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const next: AuthState = {
|
|
205
|
+
apiUrl: options.apiUrl,
|
|
206
|
+
session: {
|
|
207
|
+
cookie: result.cookieHeader,
|
|
208
|
+
userId: result.userId,
|
|
209
|
+
email: result.email,
|
|
210
|
+
expiresAt: session.session.expiresAt ?? result.sessionExpiresAt,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
await writeAuthState(next);
|
|
214
|
+
|
|
215
|
+
console.log();
|
|
216
|
+
console.log(`Logged in as ${result.email}.`);
|
|
217
|
+
if (!result.emailVerified) {
|
|
218
|
+
console.log(
|
|
219
|
+
"Heads up: your email isn't verified yet. Click the verification link in your inbox to finish setup.",
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (options.mode === "signup" && !verificationHintShown) {
|
|
226
|
+
console.log();
|
|
227
|
+
console.log("After signing up with email/password, verify your email to finish CLI auth.");
|
|
228
|
+
verificationHintShown = true;
|
|
229
|
+
}
|
|
230
|
+
process.stdout.write(".");
|
|
231
|
+
await sleep(2000);
|
|
135
232
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
email: result.email,
|
|
142
|
-
expiresAt: null,
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
await writeAuthState(next);
|
|
146
|
-
return next;
|
|
233
|
+
|
|
234
|
+
console.log();
|
|
235
|
+
throw new Error(
|
|
236
|
+
`Auth request expired. Run \`libretto cloud auth ${options.mode}\` again.`,
|
|
237
|
+
);
|
|
147
238
|
}
|
|
148
239
|
|
|
149
240
|
/**
|
|
@@ -198,104 +289,15 @@ async function issueApiKey(
|
|
|
198
289
|
// ---------------------------------------------------------------------------
|
|
199
290
|
|
|
200
291
|
export const signupCommand = SimpleCLI.command({
|
|
201
|
-
description: "
|
|
292
|
+
description: "Open the hosted-platform sign-up page",
|
|
202
293
|
})
|
|
203
294
|
.input(SimpleCLI.input({ positionals: [], named: {} }))
|
|
204
295
|
.handle(async () => {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
"If your team already has a libretto org, ask a teammate for an invite instead — switching orgs later isn't supported.",
|
|
211
|
-
);
|
|
212
|
-
console.log("Type 'q' at the name prompt to quit if that applies to you.");
|
|
213
|
-
console.log();
|
|
214
|
-
|
|
215
|
-
const name = await prompt("Your name:");
|
|
216
|
-
if (name.toLowerCase() === "q" || name.length === 0) {
|
|
217
|
-
console.log(
|
|
218
|
-
"OK — ask an existing teammate to run `libretto cloud auth invite <your-email>` and then run `libretto cloud auth accept-invite <slug> <invitation-id>` from this machine.",
|
|
219
|
-
);
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const email = await prompt("Your email:");
|
|
224
|
-
const password = await promptPassword("Choose a password (8+ chars):");
|
|
225
|
-
|
|
226
|
-
const orgName = await prompt("Organization name:");
|
|
227
|
-
const defaultSlug = slugify(orgName);
|
|
228
|
-
let orgSlug = (await prompt("Organization slug:", { defaultValue: defaultSlug })).toLowerCase();
|
|
229
|
-
const debugNotificationEmail = await prompt(
|
|
230
|
-
"Alert email (for hosted workflow failures):",
|
|
231
|
-
{ defaultValue: email },
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
console.log();
|
|
235
|
-
console.log("Creating account...");
|
|
236
|
-
|
|
237
|
-
// Retry loop: if the server reports slug-taken (data.reason === "slug_taken"),
|
|
238
|
-
// re-prompt for just the slug and try again — keeping the entered name,
|
|
239
|
-
// email, and password. Other errors propagate.
|
|
240
|
-
//
|
|
241
|
-
// The server's slug pre-check (added to /v1/auth/signupAndCreateOrg)
|
|
242
|
-
// catches the conflict before any user is created, so retrying doesn't
|
|
243
|
-
// leave dangling user rows behind. The transaction-level unique-violation
|
|
244
|
-
// catch carries the same `data.reason` so a race-loser is also handled.
|
|
245
|
-
let result: SignupResponse;
|
|
246
|
-
while (true) {
|
|
247
|
-
try {
|
|
248
|
-
result = await orpcCall<SignupResponse>({
|
|
249
|
-
apiUrl,
|
|
250
|
-
path: "/v1/auth/signupAndCreateOrg",
|
|
251
|
-
input: {
|
|
252
|
-
name,
|
|
253
|
-
email,
|
|
254
|
-
password,
|
|
255
|
-
organizationName: orgName,
|
|
256
|
-
organizationSlug: orgSlug,
|
|
257
|
-
debugNotificationEmail,
|
|
258
|
-
},
|
|
259
|
-
unauthenticated: true,
|
|
260
|
-
});
|
|
261
|
-
break;
|
|
262
|
-
} catch (e) {
|
|
263
|
-
if (
|
|
264
|
-
e instanceof ApiCallError &&
|
|
265
|
-
e.code === "CONFLICT" &&
|
|
266
|
-
isSlugTakenData(e.data)
|
|
267
|
-
) {
|
|
268
|
-
console.log();
|
|
269
|
-
console.log(e.message);
|
|
270
|
-
orgSlug = (await prompt("Organization slug:")).toLowerCase();
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
throw e;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
await persistSignupSession(apiUrl, result);
|
|
278
|
-
|
|
279
|
-
console.log(`Account created. Verification email sent to ${result.email}.`);
|
|
280
|
-
console.log("Click the link in the email to verify, then return here.");
|
|
281
|
-
console.log("Waiting for verification");
|
|
282
|
-
|
|
283
|
-
const verified = await pollForVerification(apiUrl);
|
|
284
|
-
if (!verified) {
|
|
285
|
-
console.log();
|
|
286
|
-
console.log(
|
|
287
|
-
"Timed out waiting for email verification. Click the link in the email when you're ready — your CLI session is already saved, no need to re-run signup.",
|
|
288
|
-
);
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
console.log();
|
|
293
|
-
console.log("Email verified. You're logged in.");
|
|
294
|
-
console.log(`Session saved to ${authStatePath()}`);
|
|
295
|
-
console.log();
|
|
296
|
-
console.log("To generate an API key, run:");
|
|
297
|
-
console.log(" libretto cloud auth api-key issue --label <label>");
|
|
298
|
-
console.log("Then add LIBRETTO_API_KEY=<key> to your project's .env file.");
|
|
296
|
+
await runBrowserAuthFlow({
|
|
297
|
+
mode: "signup",
|
|
298
|
+
apiUrl: resolveHostedApiUrl(),
|
|
299
|
+
websiteUrl: resolveHostedWebsiteUrl(),
|
|
300
|
+
});
|
|
299
301
|
});
|
|
300
302
|
|
|
301
303
|
// ---------------------------------------------------------------------------
|
|
@@ -303,95 +305,17 @@ export const signupCommand = SimpleCLI.command({
|
|
|
303
305
|
// ---------------------------------------------------------------------------
|
|
304
306
|
|
|
305
307
|
export const loginCommand = SimpleCLI.command({
|
|
306
|
-
description: "
|
|
308
|
+
description: "Open the hosted-platform sign-in page",
|
|
307
309
|
})
|
|
308
310
|
.input(SimpleCLI.input({ positionals: [], named: {} }))
|
|
309
311
|
.handle(async () => {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const { data, setCookie } = await betterAuthCall<{
|
|
316
|
-
token: string;
|
|
317
|
-
user: { id: string; email: string; emailVerified: boolean };
|
|
318
|
-
}>({
|
|
319
|
-
apiUrl,
|
|
320
|
-
path: "/api/auth/sign-in/email",
|
|
321
|
-
input: { email, password },
|
|
322
|
-
unauthenticated: true,
|
|
312
|
+
await runBrowserAuthFlow({
|
|
313
|
+
mode: "login",
|
|
314
|
+
apiUrl: resolveHostedApiUrl(),
|
|
315
|
+
websiteUrl: resolveHostedWebsiteUrl(),
|
|
323
316
|
});
|
|
324
|
-
|
|
325
|
-
const cookie = setCookieToCookieHeader(setCookie);
|
|
326
|
-
if (!cookie) {
|
|
327
|
-
throw new Error("Login response did not include a session cookie.");
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Pass the just-issued cookie explicitly — at this point we haven't
|
|
331
|
-
// persisted it yet, so a default credential pick would read the stale
|
|
332
|
-
// (or missing) cookie from disk.
|
|
333
|
-
const session = await getCurrentSession(apiUrl, cookie);
|
|
334
|
-
|
|
335
|
-
const next: AuthState = {
|
|
336
|
-
apiUrl,
|
|
337
|
-
session: {
|
|
338
|
-
cookie,
|
|
339
|
-
userId: data.user.id,
|
|
340
|
-
email: data.user.email,
|
|
341
|
-
expiresAt: session?.session.expiresAt ?? null,
|
|
342
|
-
},
|
|
343
|
-
};
|
|
344
|
-
await writeAuthState(next);
|
|
345
|
-
|
|
346
|
-
console.log(`Logged in as ${data.user.email}.`);
|
|
347
|
-
if (!data.user.emailVerified) {
|
|
348
|
-
console.log(
|
|
349
|
-
"Heads up: your email isn't verified yet. Re-sending the verification link to your inbox — click it to finish setup.",
|
|
350
|
-
);
|
|
351
|
-
try {
|
|
352
|
-
await betterAuthCall({
|
|
353
|
-
apiUrl,
|
|
354
|
-
path: "/api/auth/send-verification-email",
|
|
355
|
-
input: {
|
|
356
|
-
email: data.user.email,
|
|
357
|
-
callbackURL: `${apiUrl}/auth/verified`,
|
|
358
|
-
},
|
|
359
|
-
unauthenticated: true,
|
|
360
|
-
});
|
|
361
|
-
console.log(`Verification email sent to ${data.user.email}.`);
|
|
362
|
-
} catch (error) {
|
|
363
|
-
const message = error instanceof Error ? error.message : "unknown error";
|
|
364
|
-
console.log(
|
|
365
|
-
`Couldn't resend the verification email (${message}). Try again, or hit /api/auth/send-verification-email directly.`,
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
317
|
});
|
|
370
318
|
|
|
371
|
-
export const forgotPasswordCommand = SimpleCLI.command({
|
|
372
|
-
description: "Send a password reset email",
|
|
373
|
-
})
|
|
374
|
-
.input(SimpleCLI.input({ positionals: [], named: {} }))
|
|
375
|
-
.handle(async () => {
|
|
376
|
-
const apiUrl = resolveHostedApiUrl();
|
|
377
|
-
const email = await prompt("Email:");
|
|
378
|
-
const result = await orpcCall<{ status: "sent" | "not_found" }>({
|
|
379
|
-
apiUrl,
|
|
380
|
-
path: "/v1/auth/requestPasswordReset",
|
|
381
|
-
input: { email },
|
|
382
|
-
unauthenticated: true,
|
|
383
|
-
});
|
|
384
|
-
if (result.status === "not_found") {
|
|
385
|
-
console.log(`No Libretto account exists for ${email}.`);
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
console.log(`Password reset link sent to ${email}.`);
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
// ---------------------------------------------------------------------------
|
|
392
|
-
// logout
|
|
393
|
-
// ---------------------------------------------------------------------------
|
|
394
|
-
|
|
395
319
|
export const logoutCommand = SimpleCLI.command({
|
|
396
320
|
description: "Clear local libretto credentials",
|
|
397
321
|
})
|
|
@@ -429,7 +353,7 @@ export const inviteCommand = SimpleCLI.command({
|
|
|
429
353
|
named: {
|
|
430
354
|
role: SimpleCLI.option(
|
|
431
355
|
z
|
|
432
|
-
.enum(["member", "
|
|
356
|
+
.enum(["member", "owner"])
|
|
433
357
|
.default("member"),
|
|
434
358
|
{ help: "Role to assign (default: member)." },
|
|
435
359
|
),
|
|
@@ -469,10 +393,8 @@ export const inviteCommand = SimpleCLI.command({
|
|
|
469
393
|
credential,
|
|
470
394
|
});
|
|
471
395
|
|
|
472
|
-
// Fetch the inviter's org so we can print the
|
|
473
|
-
//
|
|
474
|
-
// (slug is uniquely indexed; name is not), so showing it here helps
|
|
475
|
-
// the inviter share the right command.
|
|
396
|
+
// Fetch the inviter's org so we can print the website invite link for
|
|
397
|
+
// manual testing and support cases where the email is unavailable.
|
|
476
398
|
const { data: orgs } = await betterAuthCall<
|
|
477
399
|
Array<{ id: string; name: string; slug: string | null }>
|
|
478
400
|
>({
|
|
@@ -484,151 +406,18 @@ export const inviteCommand = SimpleCLI.command({
|
|
|
484
406
|
const org = orgs?.find((o) => o.id === data.organizationId);
|
|
485
407
|
const orgName = org?.name ?? "<your-org-name>";
|
|
486
408
|
const orgSlug = org?.slug ?? "<your-org-slug>";
|
|
409
|
+
const inviteUrl = new URL("/invite", resolveHostedWebsiteUrl());
|
|
410
|
+
inviteUrl.searchParams.set("tenantSlug", orgSlug);
|
|
411
|
+
inviteUrl.searchParams.set("invitationId", data.id);
|
|
412
|
+
inviteUrl.searchParams.set("accept", "1");
|
|
487
413
|
|
|
488
414
|
console.log(`Invitation sent to ${data.email}.`);
|
|
489
415
|
console.log(`Invitation id: ${data.id}`);
|
|
490
416
|
console.log(`Organization: ${orgName} (${orgSlug})`);
|
|
491
417
|
console.log(`Expires at: ${data.expiresAt}`);
|
|
492
418
|
console.log();
|
|
493
|
-
console.log("
|
|
494
|
-
console.log(
|
|
495
|
-
` libretto cloud auth accept-invite ${orgSlug} ${data.id}`,
|
|
496
|
-
);
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
// ---------------------------------------------------------------------------
|
|
500
|
-
// accept-invite
|
|
501
|
-
// ---------------------------------------------------------------------------
|
|
502
|
-
|
|
503
|
-
export const acceptInviteCommand = SimpleCLI.command({
|
|
504
|
-
description: "Accept an organization invitation",
|
|
505
|
-
})
|
|
506
|
-
.input(
|
|
507
|
-
SimpleCLI.input({
|
|
508
|
-
positionals: [
|
|
509
|
-
SimpleCLI.positional(
|
|
510
|
-
"tenantSlug",
|
|
511
|
-
z
|
|
512
|
-
.string()
|
|
513
|
-
.min(2)
|
|
514
|
-
.max(60)
|
|
515
|
-
.regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, {
|
|
516
|
-
message:
|
|
517
|
-
"Slug must be lowercase letters, numbers, and hyphens (no leading/trailing hyphen).",
|
|
518
|
-
}),
|
|
519
|
-
{
|
|
520
|
-
help:
|
|
521
|
-
"Slug of the organization you're joining. Must match the org slug in the invitation email — acts as a confirmation step.",
|
|
522
|
-
},
|
|
523
|
-
),
|
|
524
|
-
SimpleCLI.positional("invitationId", z.string().min(1), {
|
|
525
|
-
help: "Invitation id from the invite email.",
|
|
526
|
-
}),
|
|
527
|
-
],
|
|
528
|
-
named: {},
|
|
529
|
-
}),
|
|
530
|
-
)
|
|
531
|
-
.handle(async ({ input }) => {
|
|
532
|
-
const stored = await readAuthState();
|
|
533
|
-
const apiUrl = resolveHostedApiUrl();
|
|
534
|
-
const credential = pickCredential(stored);
|
|
535
|
-
const expectedTenantSlug = input.tenantSlug;
|
|
536
|
-
|
|
537
|
-
if (credential.source !== "none") {
|
|
538
|
-
// Path A — already signed in. Better Auth will try to insert a row
|
|
539
|
-
// into `members` for the new org, but `members.userId` is UNIQUE
|
|
540
|
-
// (one libretto user = one organization). Pre-check the user's
|
|
541
|
-
// existing memberships and refuse with a clear message rather than
|
|
542
|
-
// letting it 500 with a Postgres constraint error.
|
|
543
|
-
const { data: existingOrgs } = await betterAuthCall<Array<{ id: string }>>({
|
|
544
|
-
apiUrl,
|
|
545
|
-
path: "/api/auth/organization/list",
|
|
546
|
-
method: "GET",
|
|
547
|
-
credential,
|
|
548
|
-
});
|
|
549
|
-
if (Array.isArray(existingOrgs) && existingOrgs.length > 0) {
|
|
550
|
-
throw new Error(
|
|
551
|
-
[
|
|
552
|
-
"You're already a member of an organization.",
|
|
553
|
-
"A libretto user can only belong to one organization at a time.",
|
|
554
|
-
"To accept this invite: log out, delete the existing account, and re-run `libretto cloud auth accept-invite` with a new account (or a fresh email).",
|
|
555
|
-
].join("\n"),
|
|
556
|
-
);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// Confirmation step: fetch the invitation and require the user to
|
|
560
|
-
// have typed the matching organization slug. Same lightweight
|
|
561
|
-
// second-factor check that the public ORPC route enforces for
|
|
562
|
-
// Path B. Slug is the right field here because `tenants.slug` is
|
|
563
|
-
// uniquely indexed; `tenants.name` is not, so a name-based check
|
|
564
|
-
// could be bypassed by a colliding lowercase name.
|
|
565
|
-
const { data: invitation } = await betterAuthCall<{
|
|
566
|
-
organizationName: string;
|
|
567
|
-
organizationSlug: string | null;
|
|
568
|
-
organizationId: string;
|
|
569
|
-
}>({
|
|
570
|
-
apiUrl,
|
|
571
|
-
path: `/api/auth/organization/get-invitation?id=${encodeURIComponent(input.invitationId)}`,
|
|
572
|
-
method: "GET",
|
|
573
|
-
credential,
|
|
574
|
-
});
|
|
575
|
-
if (
|
|
576
|
-
!invitation?.organizationSlug ||
|
|
577
|
-
invitation.organizationSlug !== expectedTenantSlug
|
|
578
|
-
) {
|
|
579
|
-
throw new Error(
|
|
580
|
-
"Organization slug doesn't match this invitation. Double-check the slug shown in the invitation email.",
|
|
581
|
-
);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
await betterAuthCall<{ member: { organizationId: string } }>({
|
|
585
|
-
apiUrl,
|
|
586
|
-
path: "/api/auth/organization/accept-invitation",
|
|
587
|
-
input: { invitationId: input.invitationId },
|
|
588
|
-
credential,
|
|
589
|
-
});
|
|
590
|
-
console.log(`Invitation accepted. You're now a member of ${invitation.organizationName}.`);
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// Not signed in: collect a name + password and call the public ORPC route.
|
|
595
|
-
// The server validates tenantSlug against the invitation server-side too.
|
|
596
|
-
console.log("Accepting invite — let's create your account.");
|
|
597
|
-
const name = await prompt("Your name:");
|
|
598
|
-
const password = await promptPassword("Choose a password (8+ chars):");
|
|
599
|
-
|
|
600
|
-
const result = await orpcCall<SignupResponse>({
|
|
601
|
-
apiUrl,
|
|
602
|
-
path: "/v1/auth/acceptInviteAndSignup",
|
|
603
|
-
input: {
|
|
604
|
-
invitationId: input.invitationId,
|
|
605
|
-
tenantSlug: input.tenantSlug,
|
|
606
|
-
name,
|
|
607
|
-
password,
|
|
608
|
-
},
|
|
609
|
-
unauthenticated: true,
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
await persistSignupSession(apiUrl, result);
|
|
613
|
-
|
|
614
|
-
console.log(`Account created. Verification email sent to ${result.email}.`);
|
|
615
|
-
console.log("Click the link in the email and return here.");
|
|
616
|
-
console.log("Waiting for verification");
|
|
617
|
-
|
|
618
|
-
const verified = await pollForVerification(apiUrl);
|
|
619
|
-
if (!verified) {
|
|
620
|
-
console.log();
|
|
621
|
-
console.log(
|
|
622
|
-
"Timed out waiting for email verification. Click the link in the email when ready — your CLI session is already saved.",
|
|
623
|
-
);
|
|
624
|
-
return;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
console.log();
|
|
628
|
-
console.log("Email verified. You're logged in and a member of the organization.");
|
|
629
|
-
console.log("To generate an API key, run:");
|
|
630
|
-
console.log(" libretto cloud auth api-key issue --label <label>");
|
|
631
|
-
console.log("Then add LIBRETTO_API_KEY=<key> to your project's .env file.");
|
|
419
|
+
console.log("Invite link:");
|
|
420
|
+
console.log(` ${inviteUrl.toString()}`);
|
|
632
421
|
});
|
|
633
422
|
|
|
634
423
|
// ---------------------------------------------------------------------------
|
|
@@ -783,10 +572,8 @@ export const authCommands = SimpleCLI.group({
|
|
|
783
572
|
routes: {
|
|
784
573
|
signup: signupCommand,
|
|
785
574
|
login: loginCommand,
|
|
786
|
-
"forgot-password": forgotPasswordCommand,
|
|
787
575
|
logout: logoutCommand,
|
|
788
576
|
invite: inviteCommand,
|
|
789
|
-
"accept-invite": acceptInviteCommand,
|
|
790
577
|
whoami: whoamiCommand,
|
|
791
578
|
"api-key": SimpleCLI.group({
|
|
792
579
|
description: "Manage API keys",
|