@buildinternet/uploads 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/client.d.ts +68 -0
- package/dist/client.js +81 -0
- package/dist/commands/login.d.ts +14 -1
- package/dist/commands/login.js +164 -12
- package/dist/commands/setup.js +4 -2
- package/dist/commands.js +5 -1
- package/dist/github.d.ts +2 -1
- package/dist/github.js +2 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -48,7 +48,7 @@ Commands:
|
|
|
48
48
|
purge-expired Delete objects past retentionDays
|
|
49
49
|
setup Inspect/configure advanced CLI settings
|
|
50
50
|
install Install the agent skill + register the remote MCP server
|
|
51
|
-
login
|
|
51
|
+
login Sign in via browser (or an enrollment code) and save credentials
|
|
52
52
|
admin Admin invitation management
|
|
53
53
|
config Show path, init, or set shared config
|
|
54
54
|
doctor Health + auth + workspace checks
|
package/dist/client.d.ts
CHANGED
|
@@ -63,6 +63,8 @@ export interface GalleryItem {
|
|
|
63
63
|
createdAt: string;
|
|
64
64
|
status: "available" | "missing";
|
|
65
65
|
url: string | null;
|
|
66
|
+
/** Standalone web page for this item (gallery URL + item id). Absent on older API deployments. */
|
|
67
|
+
pageUrl?: string;
|
|
66
68
|
contentType: string | null;
|
|
67
69
|
size: number | null;
|
|
68
70
|
}
|
|
@@ -189,6 +191,72 @@ export declare function createEnrollment(apiUrl: string, adminToken: string, inp
|
|
|
189
191
|
tokenExpiresInSeconds?: number;
|
|
190
192
|
scopes?: Array<"files:read" | "files:write" | "files:delete">;
|
|
191
193
|
}): Promise<EnrollmentCreateResult>;
|
|
194
|
+
/** Static OAuth client id allowlisted by the auth worker's `validateClient`. */
|
|
195
|
+
export declare const DEVICE_CLIENT_ID = "uploads-cli";
|
|
196
|
+
export interface DeviceCodeResponse {
|
|
197
|
+
device_code: string;
|
|
198
|
+
user_code: string;
|
|
199
|
+
verification_uri: string;
|
|
200
|
+
verification_uri_complete?: string;
|
|
201
|
+
expires_in: number;
|
|
202
|
+
interval: number;
|
|
203
|
+
}
|
|
204
|
+
/** POST /api/auth/device/code — start a device flow. Throws on a non-2xx. */
|
|
205
|
+
export declare function requestDeviceCode(authUrl: string, clientId?: string): Promise<DeviceCodeResponse>;
|
|
206
|
+
/**
|
|
207
|
+
* One poll of POST /api/auth/device/token. Unlike most calls, the "not ready
|
|
208
|
+
* yet" outcomes (`authorization_pending`, `slow_down`) are EXPECTED 400s, so
|
|
209
|
+
* this returns a discriminated result instead of throwing — the caller's poll
|
|
210
|
+
* loop branches on `status`.
|
|
211
|
+
*/
|
|
212
|
+
export type DeviceTokenResult = {
|
|
213
|
+
status: "ok";
|
|
214
|
+
accessToken: string;
|
|
215
|
+
tokenType: string;
|
|
216
|
+
expiresIn: number;
|
|
217
|
+
scope: string;
|
|
218
|
+
} | {
|
|
219
|
+
status: "pending";
|
|
220
|
+
} | {
|
|
221
|
+
status: "slow_down";
|
|
222
|
+
} | {
|
|
223
|
+
status: "expired";
|
|
224
|
+
} | {
|
|
225
|
+
status: "denied";
|
|
226
|
+
} | {
|
|
227
|
+
status: "error";
|
|
228
|
+
error: string;
|
|
229
|
+
description?: string;
|
|
230
|
+
};
|
|
231
|
+
export declare function requestDeviceToken(authUrl: string, input: {
|
|
232
|
+
deviceCode: string;
|
|
233
|
+
clientId?: string;
|
|
234
|
+
}): Promise<DeviceTokenResult>;
|
|
235
|
+
export interface MintWorkspaceSummary {
|
|
236
|
+
workspace: string;
|
|
237
|
+
role: string;
|
|
238
|
+
}
|
|
239
|
+
/** GET /v1/tokens — workspaces the signed-in user can mint tokens for. */
|
|
240
|
+
export declare function listMintWorkspaces(apiUrl: string, accessToken: string): Promise<{
|
|
241
|
+
workspaces: MintWorkspaceSummary[];
|
|
242
|
+
}>;
|
|
243
|
+
export interface MintTokenResult {
|
|
244
|
+
token: string;
|
|
245
|
+
workspace: string;
|
|
246
|
+
scopes: Array<"files:read" | "files:write" | "files:delete">;
|
|
247
|
+
label: string | null;
|
|
248
|
+
expiresAt: string | null;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* POST /v1/tokens — mint a `up_<workspace>_…` workspace token from a device-flow
|
|
252
|
+
* session (presented as a bearer). v1 sends exactly one grant.
|
|
253
|
+
*/
|
|
254
|
+
export declare function mintWorkspaceToken(apiUrl: string, accessToken: string, input: {
|
|
255
|
+
workspace: string;
|
|
256
|
+
scopes?: Array<"files:read" | "files:write" | "files:delete">;
|
|
257
|
+
label?: string;
|
|
258
|
+
ttlSeconds?: number;
|
|
259
|
+
}): Promise<MintTokenResult>;
|
|
192
260
|
export declare function createUploadsClient(config: UploadsClientConfig): {
|
|
193
261
|
put(body: Uint8Array, opts: PutOptions & {
|
|
194
262
|
filename: string;
|
package/dist/client.js
CHANGED
|
@@ -27,6 +27,87 @@ export function createEnrollment(apiUrl, adminToken, input) {
|
|
|
27
27
|
body: JSON.stringify(input),
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
|
+
// --- Device authorization (RFC 8628) — the `uploads login` device flow ---
|
|
31
|
+
//
|
|
32
|
+
// The CLI speaks the auth worker's OAuth-shaped endpoints directly with plain
|
|
33
|
+
// `fetch` (no better-auth client dependency in the published package, per plan
|
|
34
|
+
// D5). Better Auth's `device.code`/`device.token` endpoints take
|
|
35
|
+
// `application/json` bodies, NOT the RFC's form-encoding — the JSON shapes
|
|
36
|
+
// below are what the worker expects.
|
|
37
|
+
/** Static OAuth client id allowlisted by the auth worker's `validateClient`. */
|
|
38
|
+
export const DEVICE_CLIENT_ID = "uploads-cli";
|
|
39
|
+
/** POST /api/auth/device/code — start a device flow. Throws on a non-2xx. */
|
|
40
|
+
export function requestDeviceCode(authUrl, clientId = DEVICE_CLIENT_ID) {
|
|
41
|
+
return jsonRequest(`${authUrl.replace(/\/$/, "")}/api/auth/device/code`, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: { "Content-Type": "application/json" },
|
|
44
|
+
body: JSON.stringify({ client_id: clientId }),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export async function requestDeviceToken(authUrl, input) {
|
|
48
|
+
let res;
|
|
49
|
+
try {
|
|
50
|
+
res = await fetch(`${authUrl.replace(/\/$/, "")}/api/auth/device/token`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "Content-Type": "application/json" },
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
55
|
+
device_code: input.deviceCode,
|
|
56
|
+
client_id: input.clientId ?? DEVICE_CLIENT_ID,
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
throw new UploadsError(err instanceof Error ? err.message : "network request failed", "NETWORK");
|
|
62
|
+
}
|
|
63
|
+
const body = (await res.json().catch(() => null));
|
|
64
|
+
if (res.ok && body?.access_token) {
|
|
65
|
+
return {
|
|
66
|
+
status: "ok",
|
|
67
|
+
accessToken: body.access_token,
|
|
68
|
+
tokenType: body.token_type ?? "Bearer",
|
|
69
|
+
expiresIn: typeof body.expires_in === "number" ? body.expires_in : 0,
|
|
70
|
+
scope: body.scope ?? "",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
switch (body?.error) {
|
|
74
|
+
case "authorization_pending":
|
|
75
|
+
return { status: "pending" };
|
|
76
|
+
case "slow_down":
|
|
77
|
+
return { status: "slow_down" };
|
|
78
|
+
case "expired_token":
|
|
79
|
+
return { status: "expired" };
|
|
80
|
+
case "access_denied":
|
|
81
|
+
return { status: "denied" };
|
|
82
|
+
default:
|
|
83
|
+
return {
|
|
84
|
+
status: "error",
|
|
85
|
+
error: body?.error ?? "unknown",
|
|
86
|
+
description: body?.error_description,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** GET /v1/tokens — workspaces the signed-in user can mint tokens for. */
|
|
91
|
+
export function listMintWorkspaces(apiUrl, accessToken) {
|
|
92
|
+
return jsonRequest(`${apiUrl.replace(/\/$/, "")}/v1/tokens`, {
|
|
93
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* POST /v1/tokens — mint a `up_<workspace>_…` workspace token from a device-flow
|
|
98
|
+
* session (presented as a bearer). v1 sends exactly one grant.
|
|
99
|
+
*/
|
|
100
|
+
export function mintWorkspaceToken(apiUrl, accessToken, input) {
|
|
101
|
+
return jsonRequest(`${apiUrl.replace(/\/$/, "")}/v1/tokens`, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
|
|
104
|
+
body: JSON.stringify({
|
|
105
|
+
grants: [{ workspace: input.workspace, ...(input.scopes ? { scopes: input.scopes } : {}) }],
|
|
106
|
+
...(input.label ? { label: input.label } : {}),
|
|
107
|
+
...(input.ttlSeconds ? { ttlSeconds: input.ttlSeconds } : {}),
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
30
111
|
function encodeKeyPath(key) {
|
|
31
112
|
return key.split("/").map(encodeURIComponent).join("/");
|
|
32
113
|
}
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -5,7 +5,20 @@ export declare function resolveEnrollmentCode(parsed: ReturnType<typeof parseCom
|
|
|
5
5
|
readLine: () => Promise<string>;
|
|
6
6
|
hiddenPrompt: () => Promise<string>;
|
|
7
7
|
}): Promise<string>;
|
|
8
|
+
/**
|
|
9
|
+
* Auth worker base URL: explicit flag > UPLOADS_AUTH_URL > swap an `api.` host
|
|
10
|
+
* label for `auth.` > the production default. Local multi-worker dev (where
|
|
11
|
+
* auth runs on a different loopback port than the API) needs an explicit
|
|
12
|
+
* --auth-url / UPLOADS_AUTH_URL.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveAuthUrl(parsed: ReturnType<typeof parseCommandArgs>, apiUrl: string): string;
|
|
15
|
+
export interface DeviceLoginIo {
|
|
16
|
+
sleep: (ms: number) => Promise<void>;
|
|
17
|
+
now: () => number;
|
|
18
|
+
openUrl: (url: string) => void;
|
|
19
|
+
write: (text: string) => void;
|
|
20
|
+
}
|
|
8
21
|
export declare function runLogin(args: string[], opts: {
|
|
9
22
|
json?: boolean;
|
|
10
23
|
apiUrl?: string;
|
|
11
|
-
}, help?: boolean): Promise<number>;
|
|
24
|
+
}, help?: boolean, deviceIo?: DeviceLoginIo): Promise<number>;
|
package/dist/commands/login.js
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
|
+
import { hostname } from "node:os";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
1
3
|
import { stdin, stdout } from "node:process";
|
|
2
4
|
import { loadConfigFile, redactToken, resolveConfigPath, writeConfigKeys, workspaceFromToken, } from "../config.js";
|
|
3
|
-
import { exchangeEnrollment,
|
|
5
|
+
import { createUploadsClient, exchangeEnrollment, listMintWorkspaces, mintWorkspaceToken, requestDeviceCode, requestDeviceToken, } from "../client.js";
|
|
4
6
|
import { flagBool, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
|
|
7
|
+
import { parseScopes } from "./admin-enrollment.js";
|
|
5
8
|
const HELP = `uploads login [options]
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
Sign in and save workspace credentials. With no flags, opens a browser to
|
|
11
|
+
authorize this device — the recommended way to sign in. Pass an enrollment
|
|
12
|
+
code only if you were given one from before device login (fallback path).
|
|
9
13
|
|
|
10
14
|
Options:
|
|
11
|
-
--
|
|
12
|
-
|
|
15
|
+
--workspace <name> Workspace to mint a token for (device flow; required if
|
|
16
|
+
your account can access more than one)
|
|
17
|
+
--scopes <list> Comma-separated scopes (default: files:read,files:write)
|
|
18
|
+
--label <text> Token label (default: this machine's hostname)
|
|
19
|
+
--auth-url <url> Auth base (default: https://auth.uploads.sh)
|
|
20
|
+
--no-open Don't try to open a browser automatically
|
|
21
|
+
--code <code> Fallback: use a pre-existing enrollment code instead of
|
|
22
|
+
device login (visible in shell history)
|
|
23
|
+
--code-stdin Fallback: read a pre-existing enrollment code from stdin
|
|
13
24
|
--non-interactive Never prompt
|
|
14
25
|
--api-url <url> API base (default: https://api.uploads.sh)
|
|
15
26
|
--path <file> Config destination
|
|
@@ -17,10 +28,10 @@ Options:
|
|
|
17
28
|
--no-check Skip doctor verification
|
|
18
29
|
|
|
19
30
|
Examples:
|
|
20
|
-
uploads login
|
|
21
|
-
uploads login --
|
|
31
|
+
uploads login
|
|
32
|
+
uploads login --workspace acme
|
|
33
|
+
uploads login --code upe_… --force # fallback: pre-existing invite
|
|
22
34
|
printf '%s' upe_… | uploads login --code-stdin --non-interactive
|
|
23
|
-
uploads login --code upe_… --force --no-check
|
|
24
35
|
`;
|
|
25
36
|
export function validateEnrollmentCode(raw) {
|
|
26
37
|
const code = raw.trim();
|
|
@@ -104,7 +115,135 @@ export async function resolveEnrollmentCode(parsed, io = {
|
|
|
104
115
|
return validateEnrollmentCode(await io.readLine());
|
|
105
116
|
return validateEnrollmentCode(await io.hiddenPrompt());
|
|
106
117
|
}
|
|
107
|
-
|
|
118
|
+
/** True when the caller supplied an enrollment code (via flag, stdin, or env). */
|
|
119
|
+
function hasEnrollmentSource(parsed) {
|
|
120
|
+
return (Boolean(flagString(parsed.flags, "--code")) ||
|
|
121
|
+
flagBool(parsed.flags, "--code-stdin") ||
|
|
122
|
+
Boolean(process.env.UPLOADS_ENROLLMENT_CODE));
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Auth worker base URL: explicit flag > UPLOADS_AUTH_URL > swap an `api.` host
|
|
126
|
+
* label for `auth.` > the production default. Local multi-worker dev (where
|
|
127
|
+
* auth runs on a different loopback port than the API) needs an explicit
|
|
128
|
+
* --auth-url / UPLOADS_AUTH_URL.
|
|
129
|
+
*/
|
|
130
|
+
export function resolveAuthUrl(parsed, apiUrl) {
|
|
131
|
+
const explicit = flagString(parsed.flags, "--auth-url") ?? process.env.UPLOADS_AUTH_URL;
|
|
132
|
+
if (explicit)
|
|
133
|
+
return explicit.replace(/\/$/, "");
|
|
134
|
+
try {
|
|
135
|
+
const url = new URL(apiUrl);
|
|
136
|
+
if (url.hostname.startsWith("api.")) {
|
|
137
|
+
url.hostname = `auth.${url.hostname.slice(4)}`;
|
|
138
|
+
return url.origin;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// fall through to the default
|
|
143
|
+
}
|
|
144
|
+
return "https://auth.uploads.sh";
|
|
145
|
+
}
|
|
146
|
+
/** Best-effort browser open. The URL is always printed too, so failures are silent. */
|
|
147
|
+
function openUrl(url) {
|
|
148
|
+
try {
|
|
149
|
+
const isWin = process.platform === "win32";
|
|
150
|
+
const command = process.platform === "darwin" ? "open" : isWin ? "cmd" : "xdg-open";
|
|
151
|
+
const args = isWin ? ["/c", "start", "", url] : [url];
|
|
152
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
153
|
+
child.on("error", () => { });
|
|
154
|
+
child.unref();
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// ignore — the URL is printed for manual navigation.
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const defaultDeviceIo = {
|
|
161
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
162
|
+
now: () => Date.now(),
|
|
163
|
+
openUrl,
|
|
164
|
+
write: (text) => {
|
|
165
|
+
process.stderr.write(text);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Device-authorization login (RFC 8628): request a code, have the user approve
|
|
170
|
+
* it in a browser, poll for the session token, then mint a workspace token.
|
|
171
|
+
*/
|
|
172
|
+
async function runDeviceLogin(parsed, opts, io) {
|
|
173
|
+
const scopes = parseScopes(flagString(parsed.flags, "--scopes"));
|
|
174
|
+
const label = flagString(parsed.flags, "--label") ?? safeHostname();
|
|
175
|
+
const requestedWorkspace = flagString(parsed.flags, "--workspace");
|
|
176
|
+
const code = await requestDeviceCode(opts.authUrl);
|
|
177
|
+
const verifyUrl = code.verification_uri_complete ?? code.verification_uri;
|
|
178
|
+
io.write(`To sign in, open:\n\n ${verifyUrl}\n\nand confirm this code:\n\n ${code.user_code}\n\n`);
|
|
179
|
+
if (!opts.noOpen)
|
|
180
|
+
io.openUrl(verifyUrl);
|
|
181
|
+
io.write("Waiting for approval…\n");
|
|
182
|
+
const accessToken = await pollForDeviceToken(opts.authUrl, code, io);
|
|
183
|
+
const workspace = await resolveMintWorkspace(opts.apiUrl, accessToken, requestedWorkspace);
|
|
184
|
+
const minted = await mintWorkspaceToken(opts.apiUrl, accessToken, { workspace, scopes, label });
|
|
185
|
+
return { workspace: minted.workspace, token: minted.token, apiUrl: opts.apiUrl };
|
|
186
|
+
}
|
|
187
|
+
function safeHostname() {
|
|
188
|
+
try {
|
|
189
|
+
return hostname() || "cli";
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return "cli";
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** Poll device/token honoring interval / slow_down / pending until approved or expired. */
|
|
196
|
+
async function pollForDeviceToken(authUrl, code, io) {
|
|
197
|
+
let intervalMs = Math.max(1, code.interval) * 1000;
|
|
198
|
+
const deadline = io.now() + Math.max(1, code.expires_in) * 1000;
|
|
199
|
+
while (io.now() < deadline) {
|
|
200
|
+
await io.sleep(intervalMs);
|
|
201
|
+
let result;
|
|
202
|
+
try {
|
|
203
|
+
result = await requestDeviceToken(authUrl, { deviceCode: code.device_code });
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// A transient network blip mid-poll shouldn't abort a login the user may
|
|
207
|
+
// already have approved — keep polling until the device code's deadline.
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
switch (result.status) {
|
|
211
|
+
case "ok":
|
|
212
|
+
return result.accessToken;
|
|
213
|
+
case "pending":
|
|
214
|
+
continue;
|
|
215
|
+
case "slow_down":
|
|
216
|
+
// RFC 8628 §3.5: back off by 5s and keep polling.
|
|
217
|
+
intervalMs += 5000;
|
|
218
|
+
continue;
|
|
219
|
+
case "denied":
|
|
220
|
+
throw new UsageError("device authorization was denied");
|
|
221
|
+
case "expired":
|
|
222
|
+
throw new UsageError("the device code expired before it was approved");
|
|
223
|
+
default:
|
|
224
|
+
throw new UsageError(`device authorization failed: ${result.error}${result.description ? ` — ${result.description}` : ""}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
throw new UsageError("timed out waiting for device authorization");
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Pick the workspace to mint for. An explicit --workspace wins; otherwise, if
|
|
231
|
+
* the account can access exactly one workspace, use it — and if it can access
|
|
232
|
+
* several, require the flag rather than guessing.
|
|
233
|
+
*/
|
|
234
|
+
async function resolveMintWorkspace(apiUrl, accessToken, requested) {
|
|
235
|
+
if (requested)
|
|
236
|
+
return requested;
|
|
237
|
+
const { workspaces } = await listMintWorkspaces(apiUrl, accessToken);
|
|
238
|
+
if (workspaces.length === 1)
|
|
239
|
+
return workspaces[0].workspace;
|
|
240
|
+
if (workspaces.length === 0) {
|
|
241
|
+
throw new UsageError("your account has no workspace access yet — ask an administrator for an invitation");
|
|
242
|
+
}
|
|
243
|
+
const names = workspaces.map((w) => w.workspace).join(", ");
|
|
244
|
+
throw new UsageError(`multiple workspaces available (${names}); pass --workspace <name>`);
|
|
245
|
+
}
|
|
246
|
+
export async function runLogin(args, opts, help = false, deviceIo = defaultDeviceIo) {
|
|
108
247
|
const parsed = parseCommandArgs(args);
|
|
109
248
|
if (help || parsed.help) {
|
|
110
249
|
process.stderr.write(HELP);
|
|
@@ -118,11 +257,24 @@ export async function runLogin(args, opts, help = false) {
|
|
|
118
257
|
throw new UsageError(`credentials already exist in ${path}; use --force to replace them`);
|
|
119
258
|
if (process.env.UPLOADS_TOKEN && !force)
|
|
120
259
|
throw new UsageError("UPLOADS_TOKEN is already set in the environment; unset it or use --force");
|
|
121
|
-
|
|
122
|
-
|
|
260
|
+
let result;
|
|
261
|
+
if (hasEnrollmentSource(parsed)) {
|
|
262
|
+
const code = await resolveEnrollmentCode(parsed);
|
|
263
|
+
result = await exchangeEnrollment(apiUrl, code);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
// The device flow is inherently interactive (browser approval, then a poll
|
|
267
|
+
// that runs to the device code's ~30-min deadline). Fail fast rather than
|
|
268
|
+
// hanging a non-interactive/CI invocation that has no code to fall back on.
|
|
269
|
+
if (flagBool(parsed.flags, "--non-interactive")) {
|
|
270
|
+
throw new UsageError("device login requires a browser; run interactively, or pass --code for the enrollment path");
|
|
271
|
+
}
|
|
272
|
+
const authUrl = resolveAuthUrl(parsed, apiUrl);
|
|
273
|
+
result = await runDeviceLogin(parsed, { apiUrl, authUrl, noOpen: flagBool(parsed.flags, "--no-open") }, deviceIo);
|
|
274
|
+
}
|
|
123
275
|
const encoded = workspaceFromToken(result.token);
|
|
124
276
|
if (!encoded || encoded !== result.workspace || /[\r\n]/.test(result.token))
|
|
125
|
-
throw new UsageError("
|
|
277
|
+
throw new UsageError("login returned invalid credentials");
|
|
126
278
|
const savedApiUrl = result.apiUrl ?? apiUrl;
|
|
127
279
|
const write = writeConfigKeys(path, {
|
|
128
280
|
UPLOADS_API_URL: savedApiUrl,
|
package/dist/commands/setup.js
CHANGED
|
@@ -64,8 +64,10 @@ function formatWizard(status) {
|
|
|
64
64
|
lines.push("");
|
|
65
65
|
if (!status.token) {
|
|
66
66
|
lines.push("Step 1 — Sign in");
|
|
67
|
-
lines.push("
|
|
67
|
+
lines.push(" Run:");
|
|
68
68
|
lines.push(" uploads login");
|
|
69
|
+
lines.push(" This opens a browser to authorize this device.");
|
|
70
|
+
lines.push(" Have a pre-existing enrollment code instead? uploads login --code upe_…");
|
|
69
71
|
lines.push(" If you already have a bearer token:");
|
|
70
72
|
lines.push(" uploads setup --token up_<workspace>_…");
|
|
71
73
|
lines.push("");
|
|
@@ -207,7 +209,7 @@ export async function runSetup(args, opts, help = false) {
|
|
|
207
209
|
process.stderr.write("hint: run uploads doctor to verify\n");
|
|
208
210
|
}
|
|
209
211
|
else {
|
|
210
|
-
process.stderr.write("hint: run uploads login
|
|
212
|
+
process.stderr.write("hint: run uploads login (or uploads login --code <code> if you have one)\n");
|
|
211
213
|
}
|
|
212
214
|
return doctorOk === false ? 1 : 0;
|
|
213
215
|
}
|
package/dist/commands.js
CHANGED
|
@@ -181,7 +181,11 @@ export async function syncAttachmentsComment(client, target, run) {
|
|
|
181
181
|
previews: detail.items
|
|
182
182
|
.filter((item) => item.status === "available" && item.url && item.contentType?.startsWith("image/"))
|
|
183
183
|
.slice(0, 3)
|
|
184
|
-
.map((item) => ({
|
|
184
|
+
.map((item) => ({
|
|
185
|
+
url: item.url,
|
|
186
|
+
alt: item.altText ?? item.objectKey,
|
|
187
|
+
itemUrl: item.pageUrl,
|
|
188
|
+
})),
|
|
185
189
|
};
|
|
186
190
|
}
|
|
187
191
|
catch {
|
package/dist/github.d.ts
CHANGED
|
@@ -33,10 +33,11 @@ export interface GalleryCommentItem {
|
|
|
33
33
|
title: string;
|
|
34
34
|
/** Canonical URL returned by the API; callers must not synthesize it. */
|
|
35
35
|
url: string;
|
|
36
|
-
/** A bounded set of available images
|
|
36
|
+
/** A bounded set of available images; each links to its item page when known, else the gallery. */
|
|
37
37
|
previews?: {
|
|
38
38
|
url: string;
|
|
39
39
|
alt: string;
|
|
40
|
+
itemUrl?: string;
|
|
40
41
|
}[];
|
|
41
42
|
}
|
|
42
43
|
/** Default max width for images in the managed attachments comment (HTML img). */
|
package/dist/github.js
CHANGED
|
@@ -106,7 +106,8 @@ export function attachmentsCommentBody(items, galleries = []) {
|
|
|
106
106
|
const href = escapeHtmlAttr(gallery.url);
|
|
107
107
|
lines.push(`#### <a href="${href}">${escapeHtmlText(gallery.title)}</a>`);
|
|
108
108
|
for (const preview of gallery.previews ?? []) {
|
|
109
|
-
|
|
109
|
+
const previewHref = preview.itemUrl ? escapeHtmlAttr(preview.itemUrl) : href;
|
|
110
|
+
lines.push(`<a href="${previewHref}"><img width="320" alt="${escapeHtmlAttr(preview.alt)}" src="${escapeHtmlAttr(preview.url)}"></a>`);
|
|
110
111
|
}
|
|
111
112
|
lines.push(`<sub><a href="${href}">Open gallery</a></sub>`, "");
|
|
112
113
|
}
|