@buildinternet/uploads 0.4.0 → 0.5.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/README.md +2 -2
- package/dist/agent.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/client.d.ts +3 -0
- package/dist/client.js +30 -11
- package/dist/commands/admin-enrollment.d.ts +2 -0
- package/dist/commands/admin-enrollment.js +55 -8
- package/dist/commands.js +8 -0
- package/dist/mcp/tools.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,11 +4,11 @@ CLI and client for **uploads.sh** — upload files, get public URLs, and produce
|
|
|
4
4
|
|
|
5
5
|
## CLI
|
|
6
6
|
|
|
7
|
-
Binary: **`uploads`**. Install globally (or use
|
|
7
|
+
Binary: **`uploads`**. Install globally (or use an `npx` one-shot):
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npm install --global @buildinternet/uploads
|
|
11
|
-
npx @buildinternet/uploads
|
|
11
|
+
npx @buildinternet/uploads --help
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
```bash
|
package/dist/agent.js
CHANGED
|
@@ -15,7 +15,7 @@ export function createUploadsWorkerFileTools(files, opts = {}) {
|
|
|
15
15
|
},
|
|
16
16
|
overrides: {
|
|
17
17
|
uploadFile: {
|
|
18
|
-
description: "Upload a file for public hosting (e.g. GitHub embeds).
|
|
18
|
+
description: "Upload a non-sensitive file for public hosting (e.g. GitHub embeds). GitHub repository visibility does not restrict access; prefer keys under screenshots/.",
|
|
19
19
|
},
|
|
20
20
|
...overrides,
|
|
21
21
|
},
|
package/dist/cli.js
CHANGED
|
@@ -45,7 +45,7 @@ Commands:
|
|
|
45
45
|
setup Inspect/configure advanced CLI settings
|
|
46
46
|
install Install the agent skill + register the remote MCP server
|
|
47
47
|
login Exchange an enrollment code and configure credentials
|
|
48
|
-
admin Admin
|
|
48
|
+
admin Admin invitation management
|
|
49
49
|
config Show path, init, or set shared config
|
|
50
50
|
doctor Health + auth + workspace checks
|
|
51
51
|
health API liveness (no auth)
|
package/dist/client.d.ts
CHANGED
|
@@ -101,14 +101,17 @@ export interface EnrollmentExchangeResult {
|
|
|
101
101
|
expiresAt?: string;
|
|
102
102
|
}
|
|
103
103
|
export interface EnrollmentCreateResult {
|
|
104
|
+
pageId: string;
|
|
104
105
|
code: string;
|
|
105
106
|
expiresAt: string;
|
|
106
107
|
tokenExpiresAt: string;
|
|
108
|
+
emailed?: boolean;
|
|
107
109
|
}
|
|
108
110
|
export declare function exchangeEnrollment(apiUrl: string, code: string): Promise<EnrollmentExchangeResult>;
|
|
109
111
|
export declare function createEnrollment(apiUrl: string, adminToken: string, input: {
|
|
110
112
|
workspace?: string;
|
|
111
113
|
label?: string;
|
|
114
|
+
email?: string;
|
|
112
115
|
enrollmentSeconds?: number;
|
|
113
116
|
tokenExpiresInSeconds?: number;
|
|
114
117
|
scopes?: Array<"files:read" | "files:write" | "files:delete">;
|
package/dist/client.js
CHANGED
|
@@ -38,19 +38,19 @@ function usageBase(config) {
|
|
|
38
38
|
}
|
|
39
39
|
function mapApiError(status, error, code) {
|
|
40
40
|
const normalized = error.toLowerCase();
|
|
41
|
-
if (status === 401 || normalized === "unauthorized") {
|
|
41
|
+
if (status === 401 || code === "unauthorized" || normalized === "unauthorized") {
|
|
42
42
|
return new UploadsError(error, "UNAUTHORIZED", status);
|
|
43
43
|
}
|
|
44
|
-
if (status === 404 || normalized === "not found") {
|
|
44
|
+
if (status === 404 || code === "not_found" || normalized === "not found") {
|
|
45
45
|
return new UploadsError(error, "NOT_FOUND", status);
|
|
46
46
|
}
|
|
47
|
-
if (status === 400 && normalized === "invalid key") {
|
|
47
|
+
if (code === "invalid_key" || (status === 400 && normalized === "invalid key")) {
|
|
48
48
|
return new UploadsError(error, "INVALID_KEY", status);
|
|
49
49
|
}
|
|
50
50
|
if (code === "key_prefix_not_allowed" || code === "key_too_deep") {
|
|
51
51
|
return new UploadsError(error, "KEY_POLICY", status);
|
|
52
52
|
}
|
|
53
|
-
// Prefer stable body
|
|
53
|
+
// Prefer stable body code — bare 429 is also used for write rate limits.
|
|
54
54
|
if (status === 507 || code === "storage_quota_exceeded") {
|
|
55
55
|
return new UploadsError(error, "STORAGE_QUOTA", status);
|
|
56
56
|
}
|
|
@@ -59,15 +59,34 @@ function mapApiError(status, error, code) {
|
|
|
59
59
|
}
|
|
60
60
|
return new UploadsError(error, "API_ERROR", status);
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Parse API error bodies. Prefers the nested envelope
|
|
64
|
+
* `{ error: { code, type, message, details? } }`; still accepts the legacy
|
|
65
|
+
* flat `{ error: string, code?: string }` shape.
|
|
66
|
+
*/
|
|
67
|
+
function extractErrorFields(body) {
|
|
68
|
+
if (typeof body === "object" && body && "error" in body) {
|
|
69
|
+
const err = body.error;
|
|
70
|
+
if (typeof err === "object" && err && "message" in err) {
|
|
71
|
+
const nested = err;
|
|
72
|
+
return {
|
|
73
|
+
message: typeof nested.message === "string" ? nested.message : "request failed",
|
|
74
|
+
code: typeof nested.code === "string" ? nested.code : undefined,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (typeof err === "string") {
|
|
78
|
+
const code = "code" in body && typeof body.code === "string"
|
|
79
|
+
? body.code
|
|
80
|
+
: undefined;
|
|
81
|
+
return { message: err, code };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { message: "request failed" };
|
|
85
|
+
}
|
|
62
86
|
async function parseErrorResponse(res) {
|
|
63
87
|
const body = await res.json().catch(() => ({}));
|
|
64
|
-
const message
|
|
65
|
-
|
|
66
|
-
: res.statusText || "request failed";
|
|
67
|
-
const code = typeof body === "object" && body && "code" in body && typeof body.code === "string"
|
|
68
|
-
? body.code
|
|
69
|
-
: undefined;
|
|
70
|
-
return mapApiError(res.status, message, code);
|
|
88
|
+
const { message, code } = extractErrorFields(body);
|
|
89
|
+
return mapApiError(res.status, message || res.statusText || "request failed", code);
|
|
71
90
|
}
|
|
72
91
|
export function createUploadsClient(config) {
|
|
73
92
|
async function request(method, path, opts) {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
type FileScope = "files:read" | "files:write" | "files:delete";
|
|
2
|
+
export declare function invitePageUrl(apiUrl: string, pageId: string, webUrl?: string): string;
|
|
3
|
+
export declare function inviteMagicLink(pageUrl: string, code: string): string;
|
|
2
4
|
export declare function parseScopes(raw: string | undefined): FileScope[] | undefined;
|
|
3
5
|
export declare function runAdmin(args: string[], opts: {
|
|
4
6
|
json?: boolean;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { createEnrollment } from "../client.js";
|
|
2
|
-
import { flagInt, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
|
|
3
|
-
const HELP = `uploads admin
|
|
2
|
+
import { flagBool, flagInt, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
|
|
3
|
+
const HELP = `uploads admin invite create [options]
|
|
4
4
|
|
|
5
|
-
Admin-only: create a short-lived
|
|
5
|
+
Admin-only: create a short-lived invitation for an existing workspace.
|
|
6
|
+
Prints one magic link whose URL fragment carries the single-use code — treat the
|
|
7
|
+
link like a password. Pass --separate-code for the legacy two-channel output (a
|
|
8
|
+
non-secret page URL plus a code you share separately). The legacy
|
|
9
|
+
"admin enrollment create" spelling is accepted.
|
|
6
10
|
|
|
7
11
|
Options:
|
|
8
12
|
--admin-token <token> Or ADMIN_TOKEN (UPLOADS_ADMIN_TOKEN is a legacy alias)
|
|
@@ -11,9 +15,34 @@ Options:
|
|
|
11
15
|
--expires-in <seconds> Default: server policy
|
|
12
16
|
--token-expires-in <seconds> Upload token lifetime (default: server policy)
|
|
13
17
|
--scopes <list> Comma-separated files:read,files:write,files:delete
|
|
18
|
+
--email <address> Email the invite link to this recipient (from invites@uploads.sh)
|
|
19
|
+
--separate-code Two-channel output: non-secret page URL + separate code
|
|
14
20
|
--api-url <url> Default: https://api.uploads.sh
|
|
21
|
+
--web-url <url> Invite-page origin (defaults from --api-url)
|
|
15
22
|
`;
|
|
16
23
|
const FILE_SCOPES = new Set(["files:read", "files:write", "files:delete"]);
|
|
24
|
+
export function invitePageUrl(apiUrl, pageId, webUrl) {
|
|
25
|
+
let url;
|
|
26
|
+
try {
|
|
27
|
+
url = new URL(webUrl ?? apiUrl);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new UsageError("invalid invite web URL");
|
|
31
|
+
}
|
|
32
|
+
if (!webUrl && url.hostname.startsWith("api."))
|
|
33
|
+
url.hostname = url.hostname.slice(4);
|
|
34
|
+
url.pathname = "/invite";
|
|
35
|
+
url.search = "";
|
|
36
|
+
url.hash = "";
|
|
37
|
+
url.searchParams.set("id", pageId);
|
|
38
|
+
return url.toString();
|
|
39
|
+
}
|
|
40
|
+
// Compose the self-contained magic link. The one-time code rides in the URL
|
|
41
|
+
// fragment (#code=…), which browsers never send to the server, so opening the
|
|
42
|
+
// page neither leaks nor consumes it — only the CLI's exchange call redeems it.
|
|
43
|
+
export function inviteMagicLink(pageUrl, code) {
|
|
44
|
+
return `${pageUrl}#code=${encodeURIComponent(code)}`;
|
|
45
|
+
}
|
|
17
46
|
export function parseScopes(raw) {
|
|
18
47
|
if (raw === undefined)
|
|
19
48
|
return undefined;
|
|
@@ -34,26 +63,44 @@ export async function runAdmin(args, opts, help = false) {
|
|
|
34
63
|
process.stderr.write(HELP);
|
|
35
64
|
return 0;
|
|
36
65
|
}
|
|
37
|
-
if (
|
|
38
|
-
|
|
66
|
+
if (!["invite", "enrollment"].includes(parsed.positionals[0] ?? "") ||
|
|
67
|
+
parsed.positionals[1] !== "create")
|
|
68
|
+
throw new UsageError("expected: uploads admin invite create");
|
|
39
69
|
const adminToken = flagString(parsed.flags, "--admin-token") ??
|
|
40
70
|
process.env.ADMIN_TOKEN ??
|
|
41
71
|
process.env.UPLOADS_ADMIN_TOKEN;
|
|
42
72
|
if (!adminToken)
|
|
43
73
|
throw new UsageError("ADMIN_TOKEN is required for admin enrollment creation");
|
|
44
74
|
const apiUrl = flagString(parsed.flags, "--api-url") ?? opts.apiUrl ?? "https://api.uploads.sh";
|
|
75
|
+
const webUrl = flagString(parsed.flags, "--web-url");
|
|
45
76
|
const workspace = flagString(parsed.flags, "--workspace") ?? "default";
|
|
46
77
|
const label = flagString(parsed.flags, "--label");
|
|
78
|
+
const email = flagString(parsed.flags, "--email");
|
|
47
79
|
const result = await createEnrollment(apiUrl, adminToken, {
|
|
48
80
|
workspace,
|
|
49
81
|
label,
|
|
82
|
+
email,
|
|
50
83
|
enrollmentSeconds: flagInt(parsed.flags, "--expires-in", "--expires-in"),
|
|
51
84
|
tokenExpiresInSeconds: flagInt(parsed.flags, "--token-expires-in", "--token-expires-in"),
|
|
52
85
|
scopes: parseScopes(flagString(parsed.flags, "--scopes")),
|
|
53
86
|
});
|
|
54
|
-
|
|
55
|
-
|
|
87
|
+
const separateCode = flagBool(parsed.flags, "--separate-code");
|
|
88
|
+
const pageUrl = invitePageUrl(apiUrl, result.pageId, webUrl);
|
|
89
|
+
const link = separateCode ? pageUrl : inviteMagicLink(pageUrl, result.code);
|
|
90
|
+
const footer = `workspace: ${workspace}\nexpires: ${result.expiresAt}\n`;
|
|
91
|
+
if (opts.json) {
|
|
92
|
+
process.stdout.write(JSON.stringify({ workspace, label: label ?? null, url: link, ...result }, null, 2) + "\n");
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
if (email && result.emailed) {
|
|
96
|
+
process.stdout.write(`Invite emailed to ${email}\n${footer}`);
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
if (email && result.emailed === false)
|
|
100
|
+
process.stderr.write("warning: email delivery failed; share this link instead\n");
|
|
101
|
+
if (separateCode)
|
|
102
|
+
process.stdout.write(`Invite page: ${pageUrl}\nOne-time code (share separately): ${result.code}\n${footer}`);
|
|
56
103
|
else
|
|
57
|
-
process.stdout.write(`
|
|
104
|
+
process.stdout.write(`Invite link (contains the one-time code — treat like a password):\n${link}\n${footer}`);
|
|
58
105
|
return 0;
|
|
59
106
|
}
|
package/dist/commands.js
CHANGED
|
@@ -25,6 +25,10 @@ upload as-is, or --keep-exif when image metadata matters for the discussion.
|
|
|
25
25
|
Optional --frame wraps the image in a device/browser chrome before optimize
|
|
26
26
|
(default off). See: uploads put --help frames
|
|
27
27
|
|
|
28
|
+
Uploads are public. --pr/--issue keys include the repo, number, and filename and
|
|
29
|
+
remain public even for private/internal GitHub repositories. Upload only media
|
|
30
|
+
that is safe at a predictable public URL.
|
|
31
|
+
|
|
28
32
|
Options:
|
|
29
33
|
--key <key> Object key (default: <prefix>/<repo>/<ref>/<name>-<hash>.<ext>)
|
|
30
34
|
--destination <id> Typed root: screenshots | gh | f (sets --prefix)
|
|
@@ -166,6 +170,10 @@ const ATTACH_HELP = `uploads attach <file...> [options]
|
|
|
166
170
|
Upload one or more stable PR/issue attachments and maintain a single GitHub
|
|
167
171
|
comment. With no target, uses the pull request for the current branch.
|
|
168
172
|
|
|
173
|
+
Attachments are public and their repo/number/filename keys are predictable.
|
|
174
|
+
Private/internal GitHub repository visibility does not restrict access; upload
|
|
175
|
+
only media that is safe at a public URL.
|
|
176
|
+
|
|
169
177
|
Still images are optimized to WebP by default (same as put). Use --no-optimize
|
|
170
178
|
to upload originals. Optional --frame wraps images in device/browser chrome.
|
|
171
179
|
|
package/dist/mcp/tools.js
CHANGED
|
@@ -138,7 +138,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
138
138
|
return [
|
|
139
139
|
{
|
|
140
140
|
name: "put",
|
|
141
|
-
description: "Upload a file to uploads.sh and get a public URL plus GitHub-ready embed markdown (the returned `markdown` is ready to paste into a PR or issue). Pass `file` (a local path) or `contentBase64` + `filename` for in-memory content; with `pr`/`issue` the key is stable (same filename → same URL) and `comment` syncs the managed attachments comment.",
|
|
141
|
+
description: "Upload a file to uploads.sh and get a public URL plus GitHub-ready embed markdown (the returned `markdown` is ready to paste into a PR or issue). Pass `file` (a local path) or `contentBase64` + `filename` for in-memory content; with `pr`/`issue` the key is stable (same filename → same URL) and `comment` syncs the managed attachments comment. All uploads are public; pr/issue keys are predictable and remain public for private/internal GitHub repositories, so upload only non-sensitive media.",
|
|
142
142
|
inputSchema: {
|
|
143
143
|
type: "object",
|
|
144
144
|
properties: {
|
|
@@ -302,7 +302,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
302
302
|
},
|
|
303
303
|
{
|
|
304
304
|
name: "attach",
|
|
305
|
-
description: "Upload one or more files as stable PR/issue attachments and maintain a single managed GitHub comment listing them (each upload's `markdown` is ready to paste into GitHub). With no pr/issue, targets the pull request for the current branch.",
|
|
305
|
+
description: "Upload one or more files as stable PR/issue attachments and maintain a single managed GitHub comment listing them (each upload's `markdown` is ready to paste into GitHub). With no pr/issue, targets the pull request for the current branch. Attachments are public and their repo/number/filename keys are predictable even for private/internal GitHub repositories; upload only non-sensitive media.",
|
|
306
306
|
inputSchema: {
|
|
307
307
|
type: "object",
|
|
308
308
|
properties: {
|